Articles, podcasts and news about Swift development, by John Sundell.

Combine

Published on 16 Apr 2020

Introduced during WWDC 2019, Apple’s Combine framework lets us model all sorts of asynchronous events and operations as “values over time”. While that’s a phrase that’s commonly used within the reactive programming world, as a concept and way of constructing logic, it can initially be quite difficult to fully grasp.

So in this article, let’s take a look at the basics of Combine, what some of the core principles of reactive programming are, and how they can become really useful in practice.

Let’s start with Publishers, which are observable objects that emit values whenever a given event occurred. Publishers can either be active indefinitely or eventually be completed, and can also optionally fail when an error was encountered.

When introducing Combine, Apple also went through some of their core libraries in order to retrofit them with Combine support. For example, here’s how we could use Foundation’s URLSession type to create a publisher for making a network request to a given URL:

let url = URL(string: "https://api.github.com/repos/johnsundell/publish")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)

Once we’ve created a publisher, we can then attach subscriptions to it, for example by using the sink API — which lets us pass a closure to be called whenever a new value was received, as well as one that’ll be called once the publisher was completed:

let cancellable = publisher.sink(
    receiveCompletion: { completion in
        // Called once, when the publisher was completed.
        print(completion)
    },
    receiveValue: { value in
        // Can be called multiple times, each time that a
        // new value was emitted by the publisher.
        print(value)
    }
)

Note how our above call to sink returns a value that we store as cancellable. When attaching a new subscriber, a Combine publisher always returns an object that conforms to the Cancellable protocol, which acts as a token for the new subscription. We then need to retain that token for as long as we want the subscription to remain active, since once it gets deallocated, our subscription will automatically get cancelled (we can also cancel it manually by calling cancel() on the token).

Next, let’s fill in our above closures with a bit more logic, starting with the one for receiveCompletion, which will get passed an enum containing two cases — one for any error that was encountered, and one for when the publisher successfully completed:

let cancellable = publisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print(error)
        case .finished:
            print("Success")
        }
    },
    receiveValue: { value in
        print(value)
    }
)

Before we start filling in our receiveValue closure, let’s define a simple data model that we’ll decode our downloaded data into. Since we’re using a URL that points to a GitHub API endpoint for a repository (Publish, to be exact), let’s declare our model as a Codable struct that has two properties can be found in the JSON that we’ll download:

struct Repository: Codable {
    var name: String
    var url: URL
}

With the above model in place, let’s now implement our receiveValue logic — in which we’ll create a JSONDecoder in order to decode the data that was downloaded into a Repository value, like this:

let cancellable = publisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print(error)
        case .finished:
            print("Success")
        }
    },
    receiveValue: { value in
        let decoder = JSONDecoder()

        do {
            // Since each value passed into our closure will be a tuple
            // containing the downloaded data, as well as the network
            // response itself, we're accessing the 'data' property here:
            let repo = try decoder.decode(Repository.self, from: value.data)
            print(repo)
        } catch {
            print(error)
        }
    }
)

While the above works, we’ve sort of written our code the same way as we would when using a standard closure-based API — in that we’re nesting our logic within completion handlers. There’s nothing wrong with that, but the true power of Combine (and reactive programming in general) lies in constructing chains of operations that our data gets streamed through.

To get started, let’s take a look at the map operator, which works the same way as it does on collections — as it lets us transform each value that a publisher emits into a new form. Such a transform can be as simple as accessing a property on each value — for example, here we’re transforming each of our network result values by extracting their data property, which now gives us a publisher that emits Data values instead:

let dataPublisher = publisher.map(\.data)

Above we’re accessing the data property using a key path, which you can read more about in “The power of key paths in Swift”.

Besides map, Combine also ships with a number of other operators that we can use to transform our data in various ways. It even includes an operator that lets us decode our data directly within our chain — like this:

let repoPublisher = publisher
    .map(\.data)
    .decode(
        type: Repository.self,
        decoder: JSONDecoder()
    )

While we started out with a publisher that was emitting (Data, URLResponse) values, through our above chain, we’ve now transformed that publisher into one that emits Repository values directly — which lets us heavily simplify our subscription code, as we no longer need to perform any form of data decoding within our closures:

let cancellable = repoPublisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print(error)
        case .finished:
            print("Success")
        }
    },
    receiveValue: { repo in
        print(repo)
    }
)

A general rule of thumb is to try to keep all subscription closures as simple as possible — and to instead construct the above sort of reactive chains that our data can flow through in order to get transformed into its final form.

Since Combine is primarily used to handle asynchronous events and values, it’s quite common to run into threading issues when using it — especially when we want to use a received value within our UI code. Since Apple’s UI frameworks (UIKit, AppKit, SwiftUI, etc.) can — for the most part — only be updated from the main thread, we’re going to run into issues when writing code like this:

// Two labels that we want to render our data using:
let nameLabel = UILabel()
let errorLabel = UILabel()

let cancellable = repoPublisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            // Rendering a description of the error that was encountered:
            errorLabel.text = error.localizedDescription
        case .finished:
            break
        }
    },
    receiveValue: { repo in
        // Rendering the downloaded repository's name:
        nameLabel.text = repo.name
    }
)

The problem is that, since URLSession performs its work on a background thread, our subscriptions will also be triggered on that same background thread by default — which in turn makes us violate the rule of only performing UI updates on the main thread.

The good news is that it’s really easy to fix the above sort of issues when using Combine, as it also includes an operator that lets us switch which thread (or DispatchQueue) that a publisher will emit its events on — which we in this case can use to jump over to the main queue, and thus, the main thread:

let repoPublisher = publisher
    .map(\.data)
    .decode(
        type: Repository.self,
        decoder: JSONDecoder()
    )
    .receive(on: DispatchQueue.main)

So that’s the basics of using Combine to subscribe to a publisher, and using operators to transform its values. Next, let’s take a look at how we can create our very own publishers as well, and a few things that can be good to keep in mind when doing that.

Let’s say that we’re working on a simple Counter class that keeps track of a value that can be incremented by calling an increment() method — like this:

class Counter {
    // Using 'private(set)', we ensure that our value can only
    // be modified within the Counter class itself, while still
    // enabling external code to read it:
    private(set) var value = 0

    func increment() {
        value += 1
    }
}

Now let’s make it possible to use Combine to subscribe to changes in our counter’s value. To get started, we could use Combine’s built-in PassthroughSubject type, which both acts as a publisher, and as a subject — an object that new values can be sent using:

class Counter {
    let publisher = PassthroughSubject<Int, Never>()

    private(set) var value = 0 {
        // Whenever our property was set, we send its new value
        // to our subject/publisher:
        didSet { publisher.send(value) }
    }

    func increment() {
        value += 1
    }
}

We’re using Never as our publisher’s error type, which means that it’ll never be able to throw any errors — which is perfect in this case, since we’re only sending new Int values to it.

With the above in place, we can now subscribe to our new publisher just like we did earlier when performing network requests using URLSession — for example like this:

let counter = Counter()

let cancellable = counter.publisher
    .filter { $0 > 2 }
    .sink { value in
        print(value)
    }

// Since we're filtering out all values below 3, only our final
// increment call will result in a value being printed:
counter.increment()
counter.increment()
counter.increment()

Note how we’re able to just pass a single closure into our above call to sink, since our publisher can’t throw any errors, which means that we don’t need to handle its completion event (if we don’t want to).

However, while the above approach works, it does come with a quite major downside. Since our PassthroughSubject is both a publisher and a subject, any code can send new values to it, even if that code lives outside of our Counter class — simply by calling send():

counter.publisher.send(17)

That isn’t great, as ideally we’d like to enforce that only Counter can send new values — to avoid multiple sources of truth. Thankfully, that can quite easily be done, by creating two separate properties — one that only exposes the publisher part of our PassthroughSubject, and a private one that lets us access it as a subject as well:

class Counter {
    var publisher: AnyPublisher<Int, Never> {
        // Here we're "erasing" the information of which type
        // that our subject actually is, only letting our outside
        // code know that it's a read-only publisher:
        subject.eraseToAnyPublisher()
    }

    private(set) var value = 0 {
        didSet { subject.send(value) }
    }

    // By storing our subject in a private property, we'll only
    // be able to send new values to it from within this class:
    private let subject = PassthroughSubject<Int, Never>()

    func increment() {
        value += 1
    }
}

Much better. We now have a strong guarantee that the values that our publisher will emit will always be completely in-sync with the actual state of our Counter class.

Another option that would let us achieve the same thing would be to use the @Published property wrapper — check out “Published properties in Swift” for more info on that approach.

To recap, these are five of the key pieces of Combine’s overall terminology:

Combine is an exciting framework that lets us use the power of reactive programming without having to bring in any third-party dependencies — which in turn enables us to construct logic that automatically reacts to changes in values over time.

While this article only covered the very basics of Combine, I hope that you found it useful, and we’ll dive into much more advanced ways of using Combine within the main weekly article series over the coming months and years.

Thanks for reading! 🚀