Published on

A Combine Primer

Authors
What is Combine? Apple's reactive programming framework.

Combine is the new reactive programming framework from Apple. Like SwiftUI (which it pairs beautifully with), Combine is declarative. It provides tools for managing streams of your app's events over time, and react accordingly. Because it has the blessing of Apple, you know it's here to stay.

The Combine framework provides a declarative Swift API for processing values over time.

Apple Documentation

This is a another post in a series intended as a personal growth exercise. As I learn and digest new things, it's productive to write about them to solidify my understanding.

Feynman →

Traditional imperative programming relies on many patterns for passing information between objects and processing multi-threaded event sequences. These patterns include delegates and callbacks, blocks, target-action relationships, key-value observing and dispatch queues just to name a few.

These patterns (any many more) are tried and true in application development. However, as code reaches a certain level of complexity, or involves multiple delegates and asynchronous threads — the likleyhood of bugs increases by orders of magnitude. These environments can make the exact state of your data total guesswork. This can lead to whack-a-mole scenarios, placing endless breakpoints in your code to deduce the flow of data over time.

Combine operates above these traditional approaches to managing changing state in data flow. It uses functional reactive programming as first-class principles. Since it operates on multiple streams of data over time, it's built from the ground up for asynchronous programming — thus eliminating entire categories of software bugs and exhaustive error handling.

From what I've explored, Combine is not a all-or-nothing decision. It doesn't wholly replace imperative approaches, but rather augments them. You can determine where and when Combine makes sense in your situation. Use it to your advantage when faced with common tasks like networking and notifications.

Imperative approach

Here we assign the value of a quantity of photos in an album to the text property of a label. This is how we would update the UI to show the state of the model.

imageCountLabel.text = album.quantity(for: photo)

Although only one line of code, we now have to set up and manage a way to observe the changes in the model and update the UI, and vice-versa if considering user input.

Declarative approach

The relationship between the label and the model is assigned, or declared, instead of triggered by an event.

album.quantityPublisher(for: photo)
  .assign(to: \.text, on: imageCountLabel)

Any time the quantity of the album publisher changes, the value of the imageCountLabel changes. Once this relationship is established, it will persist. This allows the complexity of multiple streams of data to be greatly simplified. All data in a stream, regardless of origin, lifespan, length, or type are treated the same.

In an imperative approach, the more complexity you add, the more you open yourself up to bugs and unexpected behavior. With a declarative approach, data is easily transformed into an expected result, and managing its state over time becomes easier to understand and reason about.

Publisher, Operators & Subscribers

To successfully use Combine, you need a few things first:

  • Publishers expose values that can change over time. The Publisher protocol declares a type that delivers values over time.
  • Publishers have Operators to act on the values received from the upstream publishers, transform (if necessary), and republish them.
  • Finally, after the entire chain of operators, a Subscriber acts on elements as it receives them. Publishers only emit values when explicitly requested do so by subscribers. Without a subscriber, a publisher will never emit values. This puts your subscriber in control of how fast it receives events from its upstream publishers.

Various Foundation types support Combine out of the box through publishers like Timer, NotificationCenter and URLSession. If you're using key-value observing, Combine provides a default publisher for any KVO compliant property.

To get access to the data you need, it's trivial to chain together multiple publishers and coordinate their interaction. You can subscribe to updates from a TextField publisher and use them to perform a URLRequest. You could then use another publisher to process the responses from the request and use them to update your app.

By combining all your event driven processes, your code will become easier to read and reason about. No longer are you relegated to the bug-ridden complexity of nested closure callbacks and indeterminate application state.

Marble Diagrams

Marble diagrams are interactive diagrams that depict how reactive operators work with observable sequences. Let's look at some operators.

.map()

Consider a stream of values [1, 2, 3] with an opeartor of map { $0 * $0 }:

Marble diagram - Map

The top line is the upstream publisher.

The middle container is the operator that transforms values of the upstream publisher.

The bottom line is a publisher of new values, output from the operator.

Get to know the common operators. They are your secret weapons. The more you are familiar with, the better you're able to transform streams of data into the results you're looking for.

.merge()

The .merge() operator needs two upstream publishers that emit values of the same type. The error types also have to be the same.

Marble diagram - Merge

.first()

This one is pretty self explanitory. After the first value is emitted, a completion event is emitted and sent down to the subscriber.

Marble diagram - First

.last()

Very similar to .first(), but has one caveat. It is dependent on receiving a completion event from the upstream publisher to know that the sequence has finished, and to return the last emitted value. See how the output value is inline with the completion event in the upstream publisher.

Marble diagram - Last

.debounce()

In electrical engineering, when a switch opens and closes, the switch contacts can bounce off each other before the switch completely transitions to an on or off state. The bouncing action can produce transient signals that do not represent a true change in state. When reproducing switch logic, it's important to filter out transient signals by using debounce, which adds the element of time to the equation.

A time value is emitted if (0) isn't emitted immediately. If (1) is emitted with the time threshold (x * 10), the emitted value is now re-calculated and (1) appears shortly later in the downstream publisher.

Marble diagram - Debounce

.debounce() is great for returning live results. It can rate-limit the number of API calls between typed letters in input fields. It can provide a better experience when displaying search results when it's best for the user to expect them. This is a first-class operator in Combine.

.count()

.count() is another operator that emits after receiving an upstream completion event. Once that event is received, it gives a count of how many of the upstream values satisfy the criteria given, in this case, x > 10. Three values in the upstream publisher are greater than 10, so our result is 3.

Marble diagram - Count

.zip()

.zip() is a more complicated operator, but it is still first-class. It pairs elements of two streams together. Once it receives a matched pair, it emits the combined pair downstream. It will wait until it can emit a valid pair, even if it receives multiple values on a single stream. It keeps track of the order they were received. Once another comes along to satisfy the pair, it emits that pair downstream and keeps going until it receives a completion event from the upstream publisher. Any and all unpaired values are discarded.

Paris can be the same or different types. This is because the result of the operator creates a new compound type from both types. Strings and Ints, Floats and Doubles.

Marble diagram - Zip

.zip could be useful if you have two API calls to make, and you only want to continue when they are both present and valid.


These are a few of the standard operators possible with Combine. There are many others to play with and discover. Some you'll find interesting, others not so much. Check out RxMarbles.com for a comprehensive list of reactive operators.

***DRAFT***