Getting started with Combine

Published on: January 6, 2020

The Combine framework. Silently introduced, yet hugely important for iOS. It didn't get any attention during the big Keynote at WWDC 2019, but as soon as folks were in the sessions they knew that Combine was going to be huge. It implements a Functional Reactive Programming (FRP) paradigm that's similar to that of Rx which is implemented by RxSwift, except it's made by Apple and has native support on all Apple platforms as long as they are running iOS 13+, iPadOS 13+, macOS 10.15+, watchOS 6+ or tvOS 13+.

The fact that Apple created their own FRP framework is a big deal, and it gives off a signal to the developer community. SwiftUI makes heavy use of Combine and Apple has integrated Combine in several existing APIs as well. And since Combine is created and owned by Apple, it can be used without any third-party dependencies, and we can rest assured that Apple will continue to support Combine for the foreseeable future.

In today's post, I would like to help you get started with Combine and show you the basics of what it is, how it works, and what it can do. You will learn the following topics:

  • Understanding what Functional Reactive Programming is
  • Understanding publishers and subscribers
  • Transforming publishers

There is a lot to cover in this post, so make sure you're comfortable, grab yourself something to drink and put on your learning hat.

Understanding what Functional Reactive Programming is

In the world of FRP, your code is written in a way where data flows from one place to the other automatically through subscriptions. It uses the building blocks of Functional Programming like, for example, the ability to map one dataflow into another. FRP is particularly useful in applications that have data that changes over time.

For example, if you have a label that displays the value of a slider, you can use FRP to push the value of your slider through a stream, or publisher which will then send the new value of the slider to all subscribers of the stream, which could be the label that shows the slider value, or anything else.

In addition to driving UI updates, FRP is also incredibly useful in asynchronous programming. Consider a network request. When you make the request, you expect to get a result back eventually. Usually, you would pass a completion closure to your request which is then executed when the request is finished. In FRP, the method you call to make a request would return a publisher that will publish a result once the request is finished. The benefit here is that if you want to transform the result of the network request, or maybe chain it together with another request, your code will typically be easier to reason about than a heavily nested tree of completion closures.

I won't cover networking or UI updates in today's post. Instead, we'll go over some more basic examples of publishers and transforming values to prepare them for display. I will cover networking and UI updates in future posts.

Understanding publishers and subscribers

A Combine publisher is an object that sends values to its subscribers over time. Sometimes this is a single value, and other times a publisher can transmit multiple values or no values at all. While a publisher can publish a variable number of values, it can only emit a single completion or error event. Since it's common to represent the flow of a publisher as a so-called marble diagram, let's examine one now.

Example marble diagrams. Top exits normally, bottom with arrow

The image above contains two marble diagrams. Each diagram is shown as an arrow. Each arrow represents a publisher. The circles, or marbles, on each line, represent the values that the publisher emits. The top arrow has a line at the end. This line represents a completion event. After this line, the publisher will no longer publish any new values.

The bottom diagram ends with a cross. The cross represents an error event. Errors end the stream of values, similar to a completion event. In other words, something went wrong and the publisher will now no longer publish any new events. Every publisher in the Combine framework uses these same rules, with no exceptions. Even publishers that publish only a single value must publish a completion event after publishing their single value.

Subscribing to a simple publisher

We can model the stream of values that are published by a publisher as an array. In fact, we can even use arrays to drive simple publishers. Let's create a simple publisher that publishes a list of integers:

[1, 2, 3]
  .publisher
  .sink(receiveCompletion: { completion in
    switch completion {
    case .failure(let error):
      print("Something went wrong: \(error)")
    case .finished:
      print("Received Completion")
    }
  }, receiveValue: { value in
    print("Received value \(value)")
  })

There is a lot to unpack in the snippet above. The Combine framework adds a publisher property to Array. We can use this property to turn an array of values into a publisher that will publish all values in the array to the subscribers of the publisher.

The type of the created publisher is Publishers.Sequence<[Int], Never>. Based on this type signature, we can derive that there is a Publishers object in the Combine framework. You can look up the Publishers object in the documentation and you'll find this page. We can find the following short description for Publishers there:

A namespace for types that serve as publishers.

In other words, all the built-in publishers in the Combine framework are grouped under the Publishers enum. Each of the publishers that exist in this namespace conforms to the Publisher protocol and has a specific role. You will rarely have the need to directly create instances of the publishers contained in the Publishers enum. Instead, you'll often create them by calling methods or properties on other objects. Similar to how we created a Publishers.Sequence<[Int], Never> publisher by calling publisher on our array.

The generic types in the definition of Publishers.Sequence<[Int], Never> are [Int] and Never in the preceding example. This means that the publisher will use a sequence of type [Int] to publish values and that its failure type is Never. This tells us that the Sequence publisher will always complete successfully. This means that in the example above, the .failure case in the switch will never be hit and we can always assume success in the receiveCompletion closure of a sink where the failure type is Never. In fact, there is a special version of sink available on publishers that have a failure type of Never where you only supply a receiveValue closure.

Every publisher in Combine has an Output and a Failure type. The Output is the type of value that a publisher will push to its subscribers. In the case of our Sequence, the Output will be Int. The Failure type is Never because the publisher cannot finish with an error. Publishers that can fail will often use an object that conforms to Error as their Failure type, but you're free to specify any type you want.

Tip:
If you want to learn more about generics, check out some of the posts I have written on that topic:

You can subscribe to a publisher using the sink(receiveCompletion:receiveValue:) method. This method creates a subscriber that is subscribed to the publisher that the method was called on. It's important to note that publishers only publish values when they have subscribers. Calling sink(receiveCompletion:receiveValue:) on a publisher creates a subscriber immediately and enables the publisher to begin streaming values.

For posterity, the output of the preceding code snippet is the following:

Received value 1
Received value 2
Received value 3
Received Completion

The receiveValue closure is called whenever a new value is published by the publisher. This closure receives the latest value of the publisher as its single argument. In the completion handler, we can check whether a subscription failed with an error or if it completed normally. You can use a switch statement and pattern matching to extract the error and handle it as needed. Combine has more advanced error handling mechanisms that I won't go into in today's post. For now, it's important that you understand how publishers and subscriptions work at the surface.

Let's take a closer look at the subscription object that is created when you call sink(receiveCompletion:receiveValue:).

Keeping track of subscriptions

In the previous subsection, you learned a bit about subscribing to a publisher by calling sink(receiveCompletion:receiveValue:) on the publisher itself. In the example code, we did not store the object that's returned by sink(receiveCompletion:receiveValue:), which is perfectly fine in a Playground. However, in your applications, you need to hold on to the subscriptions you create. If you don't do this, the subscription object will be discarded as soon as the scope where you create the subscription is exited. So if you were to call sink in a function, the created subscription would cease to exist at the end of the function.

If you examine the return type of sink, you will find that it's AnyCancellable. An AnyCancellable object is a type-erased wrapper around a Cancellable subscription that you can hold on to in your view controller. You can safely hold on to your AnyCancellable objects and be assured that any subscriptions are canceled when the object that's holding on to the AnyCancellable is deallocated. You only need to call cancel yourself if you explicitly want to discard a given subscription.

Note:
If you have used RxSwift in the past, you may have worked with an object called DisposeBag, and you would have added Disposable objects to the DisposeBag. Combine does not have an equivalent of DisposeBag, but it does have an equivalent of Disposable which is the Cancellable protocol.

Consider the following example:

var subscription: AnyCancellable?

func subscribe() {
  let notification = UIApplication.keyboardDidShowNotification
  let publisher = NotificationCenter.default.publisher(for: notification)
  subscription = publisher.sink(receiveCompletion: { _ in
    print("Completion")
  }, receiveValue: { notification in
    print("Received notification: \(notification)")
  })
}

subscribe()
NotificationCenter.default.post(Notification(name: UIApplication.keyboardDidShowNotification))

In the preceding code, we use the convenient publisher(for:) method that was added to NotificationCenter to subscribe to the UIApplication.keyboardDidShowNotification notification. If you place this code in a Playground, you'll find that the print statement in the receiveValue closure is executed, but the receiveCompletion is never called. The reason for this is that NotificationCenter publisher can send an infinite number of notifications to its subscribers.

If you remove the assignment of subscription by removing subscription = before publisher.sink you will find that the receiveValue closure is never called due to the subscription being discarded as soon as the subscribe() function is done executing.

In addition to automatic cancellation of subscriptions when an AnyCancellable is deallocated, you can also explicitly cancel a subscription by calling cancel() on the AnyCancellable that contains the subscription, or directly on any object that conforms to Cancellable. To try this, you can add the following two lines after the code snippet I just showed you:

subscription?.cancel()
NotificationCenter.default.post(Notification(name: UIApplication.keyboardDidShowNotification))

If you put this in a playground, you'll find that the receiveValue closure is only called once because the subscription is canceled after the first notification is posted.

If you examine the type of object that is returned by publisher(for:), you'll find that it's a NotificationCenter.Publisher. This doesn't tell us much about the type of object and error it might publish. When you call sink(receiveCompletion:receiveValue:) on the publisher, you'll notice that the receiveCompletion closure has a single argument of type Subscribers.Completion<Never> and the receiveValue has an argument of type Notification. In other words, the Output of NotificationCenter.Publisher is Notification, and its Failure is Never. You can confirm this by looking up NotificationCenter.Publisher in the documentation and examing the Output and Failure type aliases.

Tip:
Speaking of type aliases, if you want to learn more about how and when to use them, check out my five ways to improve code with type aliases.

At this point, you know that Combine revolves around publishers and subscribers. You know that publishers only publish values when they have active subscribers and that you can quickly subscribe to publishers using the sink(receiveCompletion:receiveValue:) method. You saw two publishers in this section, a Publishers.Sequence<[Int], Never> publisher and a NotificationCenter.Publisher which publishes Notification objects and has Never as its error type. You also know that publishers will publish values until they emit either an error or a completion event. While this is really cool and useful already, let's look at another important key feature of Combine; transforming publishers.

Transforming publishers

When you subscribe to a publisher, you often don't want to use the values that it emits directly. Sometimes you'll want to format the output of a publisher so you can use it to update your UI, other times you need to extract some values from, for example, a notification for the published value to be useful.

Because Combine is a Functional Reactive Programming framework, it supports some of the foundational features that you might know from functional programming. Publishers support several transforming operators, like map or flatMap. We can use these transforming operators to transform every value that is emitted by a stream, into another value. Let's look at a marble diagram that describes how map works in Combine:

Example of a "map" marble diagram

The marble diagram above describes a publisher that emits values over time. By using the map operator on the publisher, a new publisher is created. Its type is Publisher.Map<Upstream, Output>. The Upstream generic must be another publisher, and the Output generic is the output of this new publisher. So when we use the earlier example where we subscribed to the UIApplication.keyboardDidShowNotification, we can extract the keyboard height and push that to subscribers instead of the full Notification object using the following code:

let publisher = NotificationCenter.default
  .publisher(for: notification)
  .map { (notification) -> CGFloat in
    guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
      return 0.0
    }

    return endFrame.cgRectValue.height
}

When you subscribe to the created publisher object you will receive CGFloat values rather than Notification objects. The type of publisher in the above example is Publishers.Map<NotificationCenter.Publisher, CGFloat>. In other words, its a map publisher that takes NotificationCenter.Publisher as its Upstream and CGFloat as its Output.

The general pattern in Combine is that every time you apply a transformation to a publisher, you create a new publisher that wraps the original publisher and has a new output. Sounds confusing? I know. Let me show you another example. This time we'll look at the collect operator. Let's look at the marble diagram first:

Example of a "collect" marble diagram

The pictured marble diagram shows that the collect operator takes values from a publisher, collects them into an array and sends them to its subscribers when a threshold is met. Let's look at this in a code example:

[1, 2, 3]
  .publisher
  .collect(2)
  .sink(receiveValue: { value in
    print("Received value \(value)")
  })

Note that I have omitted the receiveCompletion closure in the call to sink above. This is perfectly fine if you're not interested in completion events from a publisher, or if you know that it will never emit an error, which the Publishers.Sequence doesn't. After creating the sequence publisher, .collect(2) is called on the publisher which transforms it into a Publishers.CollectByCount publisher that wraps the original sequence publisher. This publisher uses the threshold that we supply and emits values whenever the threshold is met. The above code produces the following output in a playground:

Received value [1, 2]
Received value [3]

When a publisher completes before the threshold is met, the buffer is sent to the subscriber with the items that have been collected so far. If you don't specify a threshold at all, and call collect() on a publisher, the publisher is transformed into a Result<Success, Failure>.Publisher. This publisher uses an array of a publisher's output as the Success value of Result, and the Failure is the publisher's Failure. When the upstream publisher has completed, either with success or an error, the Result<Success, Failure>.Publisher will emit a single value that contains all values that were published, or an error.

Note that the collect() method with no threshold could cause your app to use an unbounded amount of memory if a publisher emits many events before completion. You'll usually want to specify a sensible threshold for your combine operations.

At the beginning of this section, I mentioned the flatMap operator. I'm not going to show how flatMap works in today's post. The reason is simple. Using flatMap is a fairly advanced concept where you can take a publisher that publishes other publishers, and you can use flatMap to publish the values from all of these publishers on a single new publisher. I will demonstrate flatMap in next week's post where we'll convert an existing networking layer to make use of Combine.

In summary

Today's post taught you the very basics of Combine. You learned what publishers are, and the basics of how they work. You learned that publishers push values to their subscribers until they have no more values to emit and are completed, or until they emit an error. You also learned that you can subscribe to a publisher using the sink(receiveCompletion:receiveValue:) method, and that this method creates an AnyCancellable object that contains the subscription and must be retained for as long as you need it to make sure your subscription stays alive.

After learning the basics of publishers and subscribers, I showed you the basics of how you can transform publishers of one type, into publishers of a different type, much like how map works on an array. The ability to chain together publishers, and transform them using different functions is one of the most powerful features of FRP and you will find that these transformations truly are the heart of Combine once you start using it more often.

This post is part of an ongoing series I'm planning to do where I will gradually teach you more and more about Combine using practical examples and sometimes more theoretical overviews. You can find all the posts in this series right here. This post is the first post in this series so if you're reading this post early after I published it, there probably isn't much else to read yet. Make sure to subscribe to my newsletter below, and to follow me on Twitter to be notified when I publish new content.

Categories

Combine

Subscribe to my newsletter