Daily coverage of WWDC21.
A Swift by Sundell spin-off.

What Swift’s new concurrency features might mean for the future of Combine

Published at 10:15 GMT, 11 Jun 2021
Written by: John Sundell

Looking at many of the new concurrency features that are being introduced in Swift 5.5, especially APIs like AsyncSequence and AsyncStream (which still hasn’t completely made it through the Swift Evolution process), many developers have noticed that there’s a quite clear overlap between the functionality that these new features offer and what Combine does.

So, what might the introduction of these new features mean for the future of Combine as a framework? Is Combine still something worth investing in, or is it likely that it’ll be deprecated and made obsolete within the near future? Let me share my thoughts on that.

First, a quick recap — Combine is Apple’s built-in reactive programming framework, which lets us build data pipelines that consist of a series of operators that each perform some form of either asynchronous or synchronous task — such as performing a network call, decoding JSON, or otherwise transforming or handling some form of value. For example, here we’re using Combine to facilitate the loading and decoding of a Model value:

struct ModelLoader<Model: Decodable> {
    var session = URLSession.shared

    func load(from url: URL) -> AnyPublisher<Model, Error> {
        session
            .dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: Model.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

Now, using Swift 5.5’s async/await feature, and the new async-marked APIs that URLSession now ships with, we can actually accomplish the exact same thing just using Swift’s built-in language features:

struct ModelLoader<Model: Decodable> {
    var session = URLSession.shared

    func load(from url: URL) async throws -> Model {
        let (data, _) = try await session.data(from: url)
        let decoder = JSONDecoder()
        return try decoder.decode(Model.self, from: data)
    }
}

Just looking at the above example, we might then draw the conclusion that Combine is now completely unnecessary, as using async/await gives us an arguably much simpler, more straightforward implementation that doesn’t require us to manually manage any publishers or AnyCancellable tokens in any way.

However, there are some additional details that we have to keep in mind. The most important of which, at least in my opinion, is that Combine is not a general-purpose asynchronous programming framework, nor was it ever intended to be. Instead, it’s a reactive programming framework, which is a very specific, quite opinionated style of programming. I’m not saying that it’s a bad style, not at all, but I think it’s important to acknowledge that reactive programming is not something that every developer is into, nor is it a “silver bullet”-style tool that’s meant to solve all kinds of asynchronous problems.

But, the way that most of us have been using Combine since it was introduced at WWDC 2019 is not actually to do reactive programming at all. Instead, in many apps, Combine has simply been used like above — to perform single, stand-alone tasks, such as network calls. Again, that’s not a bad thing, but this is the type of problem that async/await is probably a better (and more lightweight) tool for, at least for most teams and code bases.

On the other hand, certain problems will still likely be easier to solve using Combine and its very comprehensive suite of operators that let us perform tasks such as debouncing, duplicate removal, transformations, and more. Here’s an example from the Swift by Sundell article “Connecting and merging Combine publishers in Swift”, which really illustrates just how powerful Combine can be when we’re able to describe a certain set of functionality as a completely reactive pipeline:

private extension SearchViewModel {
    func configureDataPipeline() {
        $query
            .dropFirst()
            .debounce(for: 0.5, scheduler: DispatchQueue.main)
            .removeDuplicates()
            .combineLatest($filter)
            .map { [loader] query, filter in
                loader.loadResults(
                    forQuery: query,
                    filter: filter
                )
                .asResult()
            }
            .switchToLatest()
            .receive(on: DispatchQueue.main)
            .assign(to: &$output)
    }
}

Now, could the above instead be written using Swift’s AsyncSequence and its upcoming AsyncStream companion? Of course! Could it also be written using an OperationQueue, or using Grand Central Dispatch directly? Absolutely! But that doesn’t mean that there isn’t a place for Combine going forward, since each of these tools have their own sets of specializations and tradeoffs.

Another important factor to consider here is that these new concurrency APIs are (at least currently) exclusive to Apple’s latest set of operating system versions (so iOS 15, macOS Monterey, and so on). That means that, for any team that works on a project that needs to support either iOS 13 or 14, Combine will likely remain the prime candidate for the above kind of asynchronous programming, even as Swift’s built-in concurrency system keeps getting more comprehensive and powerful.

Add to that the fact that many of SwiftUI’s state management mechanisms, such as ObservableObject and @Published, are all powered by Combine, and I think it’s fair to say that Combine is here to stay, at least for the time being.

Of course, that doesn’t mean that Combine will never go away, nor does it mean that we shouldn’t look into Swift’s new concurrency features. All it means is that there’s no need to panic. Our Combine-based code won’t stop working tomorrow, and it’s still a fantastic framework to learn. Over time, more and more asynchronous APIs are likely to adopt async/await, actors, and the other structured concurrency features that Swift 5.5 is introducing, and when that happens — and when we can update our deployment targets accordingly — then we can start looking into porting our Combine-based code into this exciting new world of concurrency.

At least that’s how I see it 🙂

Thanks for reading!

Written by: John Sundell
RevenueCatRevenueCat

Easily build and manage iOS and Android in-app purchases. With just a few lines of code RevenueCat provides IAP infrastructure, customer analytics, data integrations, and gives you time back from dealing with edge cases and updates across all platforms.