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

Using Combine’s futures and subjects

Published on 23 Mar 2021
Discover page available: Combine

Although Combine is mainly focused around the concept of publishers that emit sequences of values over time, it also includes a set of convenience APIs that enable us to leverage the full power of the framework without necessarily having to write completely custom publisher implementations from scratch.

For example, let’s say that we wanted to add Combine support to an existing, closure-based API — such as this ImageProcessor, which uses the classic completion handler pattern to asynchronously notify its callers when the processing for a given image either finished or failed:

struct ImageProcessor {
    func process(
        _ image: UIImage,
        then handler: @escaping (Result<UIImage, Error>) -> Void
    ) {
        // Process the image and call the handler when done
        ...
    }
}

The above API uses Swift’s built-in Result type to encapsulate either a successfully processed image, or any error that was encountered. To learn more, check out this Basics article.

Now, rather than rewriting our ImageProcessor, let’s actually add a new, Combine-powered API to it in a completely additive way. Not only will that keep all of our existing code intact, but it’ll also let us choose whether to use Combine or a completion handler on a case-by-case basis even as we write new code.

Retrofitting with futures

To make that happen, let’s use Combine’s Future type, which is an adaptation of the Futures/Promises pattern that’s very commonly used across a wide range of different programming languages. Essentially, a Combine Future gives us a promise closure that we can call when an asynchronous operation was completed, and Combine will then automatically map the Result that we give that closure into proper publisher events.

What’s really convenient in this case is that our existing completion handler closure already uses a Result type as its input, so all that we have to do within our Future-based implementation is to simply pass the promise closure that Combine gives us into a call to our existing process API — like this:

extension ImageProcessor {
    func process(_ image: UIImage) -> Future<UIImage, Error> {
        Future { promise in
            process(image, then: promise)
        }
    }
}

Just to illustrate, here’s what the above implementation would’ve looked like if we were to instead use a dedicated completion handler closure and then manually pass its result to our promise:

extension ImageProcessor {
    func process(_ image: UIImage) -> Future<UIImage, Error> {
        Future { promise in
            process(image) { result in
    promise(result)
}
        }
    }
}

So the Future type offers a very neat way to convert existing, closure-based APIs into ones that can be used within the reactive world of Combine — and, since those futures are really just publishers like any other, that means that we can use Combine’s entire suite of operators to transform and observe them:

processor.process(image)
    .replaceError(with: .errorIcon)
    .map { $0.withRenderingMode(.alwaysTemplate) }
    .receive(on: DispatchQueue.main)
    .assign(to: \.image, on: imageView)
    .store(in: &cancellables)

However, just like Futures and Promises in general, Combine’s Future type can only emit a single result, as it will immediately complete and get closed down once its promise closure was called.

So what if we instead wanted to emit multiple values over time, which is really what Combine was mainly designed to do?

Handling multiple output values

Let’s go back to our closure-based ImageProcessor API from before and imagine that it instead accepted two closures — one that gets called periodically with progress updates as an image is being processed, and one that’s called once the operation was fully completed:

struct ImageProcessor {
    typealias CompletionRatio = Double
    typealias ProgressHandler = (CompletionRatio) -> Void
    typealias CompletionHandler = (Result<UIImage, Error>) -> Void

    func process(
        _ image: UIImage,
        onProgress: @escaping ProgressHandler,
        onComplete: @escaping CompletionHandler
    ) {
        // Process the image and call the progress handler to
        // report the operation's ongoing progress, and then
        // call the completion handler once the image has finished
        // processing, or if an error was encountered.
        ...
    }
}

Above we’re using type aliases, both to make our actual method definition a bit easier to read, and to contextualize the Double that gets passed into our ProgressHandler. To learn more, check out “The power of type aliases in Swift”.

Before we start updating our Combine-based extension for the above new API, let’s introduce an enum called ProgressEvent, which we’ll use as the output type for the Combine publishers that we’ll create (since publishers can only emit a single type of values). It’ll include two cases, one for update events, and one for completion events:

extension ImageProcessor {
    enum ProgressEvent {
        case updated(completionRatio: CompletionRatio)
        case completed(UIImage)
    }
}

An initial idea on how to update our Combine API might be to keep using the Future type, just like we did before, but to now call its promise closure multiple times to report both update and completion events — for example like this:

extension ImageProcessor {
    func process(_ image: UIImage) -> Future<ProgressEvent, Error> {
        Future { promise in
            process(image,
                onProgress: { ratio in
                    promise(.success(
    .updated(completionRatio: ratio)
))
                },
                onComplete: { result in
                    promise(result.map(ProgressEvent.completed))
                }
            )
        }
    }
}

However, the above won’t work, since — as mentioned earlier — Combine futures can only emit a single value, which means that with the above setup, we’ll only receive the very first updated event before our entire pipeline will complete.

Sending values using subjects

Instead, this is a great use case for a subject — which lets us send as many values as we’d like before manually completing it. Combine ships with two main subject implementations: PassthroughSubject and CurrentValueSubject. Let’s start by using the former, which doesn’t hold on to any of the values that we’ll send it, but will rather pass them through to each of its subscribers.

Here’s how we could use such a subject to update our Combine-powered ImageProcessing API to now fully support both progress updates and completion events:

extension ImageProcessor {
    func process(_ image: UIImage) -> AnyPublisher<ProgressEvent, Error> {
        // First, we create our subject:
        let subject = PassthroughSubject<ProgressEvent, Error>()

        // Then, we call our closure-based API, and whenever it
        // sends us a new event, then we'll pass that along to
        // our subject. Finally, when our operation was finished,
        // then we'll send a competion event to our subject:
        process(image,
            onProgress: { ratio in
                subject.send(.updated(completionRatio: ratio))
            },
            onComplete: { result in
                switch result {
                case .success(let image):
                    subject.send(.completed(image))
                    subject.send(completion: .finished)
                case .failure(let error):
                    subject.send(completion: .failure(error))
                }
            }
        )
        
        // To avoid returning a mutable object, we convert our
        // subject into a type-erased publisher before returning it:
        return subject.eraseToAnyPublisher()
    }
}

Now all that we have to do is to update our call site from before to handle ProgressEvent values, rather than just UIImage instances — like this:

processor.process(image)
    .replaceError(with: .completed(.errorIcon))
    .receive(on: DispatchQueue.main)
    .sink { event in
        switch event {
        case .updated(let completionRatio):
            progressView.completionRatio = completionRatio
        case .completed(let image):
            imageView.image = image.withRenderingMode(
                .alwaysTemplate
            )
        }
    }
    .store(in: &cancellables)

However, one thing to keep in mind when using PassthroughSubject is that each subscriber that attaches to it will only receive the values that were sent after that subscription became active.

So in our case, since we’re starting each image processing operation immediately, and since we don’t know whether a caller might apply some form of delay to the way it handles our emitted values, we might instead want to use a CurrentValueSubject. Like its name implies, such a subject will keep track of the current (or last) value that we sent to it, and will in turn send that to all new subscribers once they connect to our subject.

Thankfully, switching between those two subject variants is typically really simple, since the only difference is that we have to initialize a CurrentValueSubject with the initial current value that we’d like it to keep track of:

extension ImageProcessor {
    func process(_ image: UIImage) -> AnyPublisher<ProgressEvent, Error> {
        let subject = CurrentValueSubject<ProgressEvent, Error>(
    .updated(completionRatio: 0)
)

        ...

        return subject.eraseToAnyPublisher()
    }
}

Worth pointing out, though, is that the above new implementation will also cause that initial ProgressEvent value to be immediately emitted when our subject is created, which may or may not be what we want, depending on the situation. But, in this case, it’ll actually be really nice, as that’ll ensure that all of our progress handling code will always be reset to zero when connected to our subject.

Conclusion

Combine’s Future and subject types are definitely worth keeping in mind when building Combine-powered APIs — and often act as much simpler alternatives to building completely custom publishers from scratch. There’s of course also Published properties, which offer another way to emit values through stored properties, which were covered in depth in this article — so Combine really does offer a quite comprehensive suite of tools that we can choose from depending on what we’re looking to build.

You’re more than welcome to reach out via either Twitter or email if you have any questions, comments or feedback.

Thanks for reading!