Getting Started with the Combine framework in Swift

Santosh Botre
8 min readApr 20, 2021

--

Foundation of Combine

Reactive programming is not a new concept. It has been around for over a decade. However, it became popular or started trending in 2009 when Microsoft launched the library, called Reactive Extensions for .NET and open source in 2012. Since then many different languages have started to use its concepts, and we have reactive support for JS, Kotlin, Swift, Scala, PHP, etc.

For Apple’s platform, there have been 3rd party reactive frameworks like RxSwift and Reactive Swift.

If you have not used one of them previously — do not worry. Apple has brought Reactive programming support to its platform via the built-in system framework, Combine.

All are aware of how the Apple ecosystem works, newly introduced feature support works from the released OS version onwards i.e., iOS 13/macOS Catalina and later. It might create doubt in your mind to reach your application that supports iOS 13 and later.

You should take look at the statistics of iOS and iPadOS Usage measured by the App Store in Feb 2021. Your application is available to use for at least 92% and up to 98% of Apple users for sure.

developer.apple.com

Combine Basics

With the Combine, objects do not have to push or pull the data, no callback mechanism to know the status of an operation you want to perform. Simply, Asynchronous programming will be easy peasy.

Key Points

  • Combine is a declarative, reactive framework to process asynchronous events.
  • Simplify and minimise the code for asynchronous programming

Only a couple of core concepts every developer needs to know to start using the Combine framework effectively. However, understanding them is more important to use them efficiently.

These core concepts are:

  • Publisher
  • Subscriber
  • Operators

The high-level view of chaining of parties in the reactive programming.

Dig. 1.0

Let’s Understand

Dig. 2.0

Publishers

As the name suggests, Publishers are the type that can emit values over time to one or more consumers/interested parties such as operator, subscriber.

The possible logic in the publisher can be user events, asynchronous data requests from an offline data store or network, intensive processing, etc. then emit events to the consumers.

Simplest way to create publisher,

Just — Create publisher from a primitive value type. Just emits a single value that you provide and immediately finishes.

Fail — doesn’t emit any values and immediately fails with a provided error.

Empty — doesn’t emit any values simply return completion event.

Note: You will get to know sink keyword and it’s functionality as we move further.

What all events publishers might publish,

  1. Output — data value. i.e., you expect to return, in case of success.
  2. Failure — error value. i.e., you expect to return, in case of failure.
  3. Completion event — just to inform execution is complete.

NOTE: A publisher can emit zero or more values but only one completion event that can either be a normal completion event or an error.

You can create your own publishers by implementing ‘Publisher’ protocol yourself. Let us have a contract of expectation. The publisher is a protocol with two generic types you might have noticed in the diagram 1.0 earlier.

  • Publisher.Output is the type of output value of the publisher. Once the publisher is specialized to one type it cannot emit data of other types.
  • Publisher.Failure is the type of error values the publisher will throw on fail. If the publisher can never fail, specify that by using a Never failure type.

Subscribers

Subscriber is the end of the subscription chain as they either receive data, error, or completion event. Like, display alert to the user, populate the response on the screen, etc.

The subscription by designed gets invoked every time something happens on publishers.

Combine built-in subscribers,

  • sink — Allows you to provide closures with code that will receive output values and completions.

Once you subscribe you will receive receiveCompletion event or receiveValue.

  • assign — Without any custom code, directly bind/assign value to the property on any data model or UI control.

As soon as you add a subscriber at the end of a subscription, it activates the publisher.

NOTE: Publishers do not emit any values if there are no Subscribers attached to them.

To know what exactly happening behind the scene user our friend in debug i.e., print()

Operators

Operators are methods on the Publisher protocol that return either the same or a new publisher. You can call a bunch of operators one after the other by chaining them together.

Operators always have input and output. i.e., input-stream and output-stream. Their goal is to work on input received from earlier operators, act, and supply their output to the next in the chain.

— Transforming Operators: Subscribers receive value from a publisher. However, we often want to transform those values into some form that is ideal for use by the subscribers.

  • collect: Buffer a stream of individual values into an array of those values from the publisher and emit to downstream. You can specify a certain number of values. Example: collect(2)
  • map: just like Swift’s standard map.
  • tryMap: in case of error, it will emit an error downstream.
  • flatMap: flatten multiple upstream into single downstream.

Using two subject we can demonstrate but, you are not aware with subject yet.

  • replaceNil: replace nil value received from the upstream with a new non-nil value.
  • replaceEmpty: insert a value in case a publisher completes without emitting values. Mostly, used for testing purposes where you expect completion calls.
  • scan: ability to return current value with previously stored value. Initial value need to be passed as an argument, here it is 5.

Filtering Operators: Sometimes we do not want to send all the values to subscribers and filter values and send only values subscribers care about.

  • filter: passed down values that return passes the provided predicate. i.e., true.
  • removeDuplicate: remove duplicate values from the upstream.

NOTE: Duplicate should be adjacent in array.

  • compactMap: omits the optional values which are nil.
  • ignoreOutput: omits all values and emits only the completion event to the consumer.
  • first: find and emit only the first value matching the predicate.
  • last: find and emit only the last value matching the predicate.
  • dropFirst: default omit first value and ignores the count values.
  • drop(while:): omits all the values till match the predicate.
  • drop(untilOutputFrom:): skip any values emitted by a publisher until a second publisher starts emitting value.
  • prefix: simply emit a prefix count value. As soon as two values are emitted, the publisher completes.
  • prefix(while:): emitting a values till match the predicate.
  • prefix(untilOutputFrom:): Emits values until a second publisher emits.

— Combining Operators: Allow to combine multiple publishers and establish logical relationship between them.

  • prepend: prepend the values with the new publisher values.
  • prepend(stride:): prepend the values from and to by a difference.
  • prepend(publisher): The operators prepended lists of elements to an existing publisher.
  • append: appends its value after the original publisher.
  • append(publisher): The operators prepend the values to existing publisher.
  • switchToLatest: Switch to new publisher by cancelling the old publisher or pending publishers.
  • merge(with:): Merge the emit values from two publishers of same type.
  • combineLatest: Combine the emit values from the two publishers with different type with values of the publishers which is latest.
  • zip: Emit the tuple/paired values in the same indexes. It waits to both publishers to emit the values.

— Time Manipulation Operators: Allow perform complex time-based tasks that would be hard to do without Combine.

  • debounce: Wait for the specified interval and emit the latest value.

Example: user input in the search field and fire an API for a result.

  • throttle: waits for the specified interval and then emit the first or latest value received during interval

— Sequence operators: Sequence the values received from the publishers.

  • min: find the minimum value emitted from publisher after send .finished completion event.
  • max: find the maximum value emitted from publisher after send .finished completion event.
  • first: first emitted value and will not wait publisher to finish.
  • last: last emitted value and must wait publisher to finish.
  • output(at:): emits value at specific index.
  • output(in:): emits value in specific range.
  • count: emit the total number of values emitted by the publisher.
  • contains: emit bool if the specified value emitted by the publisher.
  • reduce: emit the accumulated value on publisher completion event.

share: operator is to let you obtain a publisher by reference rather than by value.

Memory Management

As soon as someone subscribes to the publisher it means the reference is increasing, which leads to memory management. Thanks to Cancellable protocol by Combine we do not need to do any heavy lifting.

The system-provided subscribers conform to Cancellable protocol, which means that your subscription code returns a Cancellable object. That means once an object is released from memory, it cancels the whole subscription and releases its resources from memory.

What does it mean to you?

When a subscriber is done and no longer wants to receive values from a publisher, it’s good idea to cancel the subscription to free up resources and stop any corresponding activities from occurring, such as network calls.

If you don’t call cancel() explicitly then,

The lifespan of a subscription can managed by storing a subscription in the property of your class. i.e., view controller, the business class, etc. As soon as the view controller is dismissed or the business class object scope is over, that will deinitalize its properties and will also cancel the subscription.

else just have a [AnyCancellable] collection property and add subscriptions inside. They all will be canceled and released automatically when the property is released from memory.

Subject

A subject enables to non-Combine imperative code to send values to Combine subscribers.

  • PassthroughSubject: Start receiving value after change only
  • CurrentValueSubject: Receive current value and each time change in the value. That means, subject must have initial value.

NOTE: Make sure you do not expose the subject for subscription. As it will allow subscriber class to send the data too. To restrict that use eraseToAnyPublisher it returns a publisher and it does not have a send()method available.

Benefit Of Combine

The frameworks are for convenient, efficient, proven, and taking out all the burden from the developers.

  1. You do not have to spend time to know if Combine is a great fit for your project.
  2. Combine integrated at system level and do not have any external third-party dependency.
  3. Combine supported by many APIs out of the box i.e., NotificationCenter, URLSession, etc. which used by project on regular basis.
  4. Get rid of old writing of async code via delegate pattern, closures, or blocks or 3rd party reactive platform.
  5. No need to worry about deprecation, maintenance, future proof, and continuity of the framework in future as its directly coming from Apple ecosystem.
  6. Testing asynchronous code with combine will be easy than testing asynchronous normal code.

Combine will not affect the existing structure of the application.

Reading material:

--

--

Santosh Botre
Santosh Botre

Written by Santosh Botre

Take your time to learn before develop, examine to make it better, and eventually blog your learnings.

No responses yet