Getting started with the Combine framework in Swift

Combine is a new framework by Apple introduced at WWDC 2019. The framework provides a declarative Swift API for processing values over time. In other words, as described by Apple itself:

Customize handling of asynchronous events by combining event-processing operators.

At first, this might sound quite overwhelming. What does it actually mean? What can I do with it and when should I use it?

Hopefully, after reading this blog post, you should be able to answer these questions.

What is Combine?

The Combine framework can be compared to frameworks like RxSwift and ReactiveSwift (formally known as ReactiveCocoa). It allows you to write function reactive code by providing a declarative Swift API. Functional Reactive Programming (FRP) languages allow you to process values over time. Examples of these kinds of values include network responses, user interface events, and other types of asynchronous data.

The basic principles of Combine

The basic principles of Combine make you understand how it works and how you can use it. Before we dive straight into the code examples, it’s better to start with some background information. This will help you to better understand how the code works and behaves.

Publishers and subscribers

The Combine framework comes with so-called Publishers and subscribers. If you’re familiar with RxSwift:

  • Publishers are the same as Observables
  • Subscribers are the same as Observers

Different namings, but they both give the same understanding. A Publisher exposes values that can change on which a subscriber subscribes to receive all those updates. Keep this in mind while we go over some examples of Publishers available in the Foundation framework while working with Combine.

The Foundation framework and Combine

The Foundation framework contains a lot of extensions to work with Combine. It allows you to work with common types you’re already familiar with. Examples include:

  • A URLSessionTask Publisher that publishes the data response or request error
  • Operators for easy JSON decoding
  • A Publisher for a specific Notification.Name that publishes the notification

Taking the last example we can explain the concept of a Publisher and a Subscriber.

In the following code example, we create a new Publisher for our new blog post notification.

import Combine

extension Notification.Name {
    static let newBlogPost = Notification.Name("new_blog_post")
}

struct BlogPost {
    let title: String
    let url: URL
}

let blogPostPublisher = NotificationCenter.Publisher(center: .default, name: .newBlogPost, object: nil)

This publisher will listen for incoming notifications for the newBlogPost notification name. However, this will only happen as soon as there is a subscriber.

We could, for example, create a lastPostTitleLabel which subscribes to the publisher.

let lastPostLabel = UILabel()
let lastPostLabelSubscriber = Subscribers.Assign(object: lastPostLabel, keyPath: \.text)
blogPostPublisher.subscribe(lastPostLabelSubscriber)

Trying out this code you might already notice that this doesn’t work yet. It results in the following error:

Instance method ‘subscribe’ requires the types ‘NotificationCenter.Publisher.Output’ (aka ‘Notification’) and ‘String?’ be equivalent

The text property of the label requires to receive a String? value while the stream publishes a Notification. Therefore, we need to use an operator you might be familiar with already: map. Using this operator we can change the output value from a Notification to the required String? type.

let blogPostPublisher = NotificationCenter.Publisher(center: .default, name: .newBlogPost, object: nil)
    .map { (notification) -> String? in
        return (notification.object as? BlogPost)?.title ?? ""
    }

This will result in the following complete code example:

import Combine

extension Notification.Name {
    static let newBlogPost = Notification.Name("new_blog_post")
}

struct BlogPost {
    let title: String
    let url: URL
}

let blogPostPublisher = NotificationCenter.Publisher(center: .default, name: .newBlogPost, object: nil)
    .map { (notification) -> String? in
        return (notification.object as? BlogPost)?.title ?? ""
    }

let lastPostLabel = UILabel()
let lastPostLabelSubscriber = Subscribers.Assign(object: lastPostLabel, keyPath: \.text)
blogPostPublisher.subscribe(lastPostLabelSubscriber)

let blogPost = BlogPost(title: "Getting started with the Combine framework in Swift", url: URL(string: "https://www.avanderlee.com/swift/combine/")!)
NotificationCenter.default.post(name: .newBlogPost, object: blogPost)
print("Last post is: \(lastPostLabel.text!)")
// Last post is: Getting started with the Combine framework in Swift

Whenever a new blog post is “Published”, the label “Subscriber” will update its text value. Great!

The rules of a subscription

Now you’ve seen a basic example of a Publisher and a subscriber in Combine, it’s time to go over the rules that come with a subscription:

  • A subscriber can only have one subscription
  • Zero or more values can be published
  • At most one completion will be called

That’s right, subscriptions can come with completion, but not always. Our Notification example is such a Publisher which will never complete. An example of a completing publisher is the URLSessionTask Publisher which will complete either with the data response or the request error. Fact is that whenever an error is thrown on a stream, the subscription is dismissed. Even if the stream allows multiple values to pass through.

@Published usage to bind values to changes

Now that we know the basics we can jump into the @Published keyword. This keyword is a property wrapper and adds a Publisher to any property. A simple example can be a boolean which we assign to the enabled state of a UIButton:

final class FormViewController: UIViewController {
    
    @Published var isSubmitAllowed: Bool = false

    @IBOutlet private weak var acceptTermsSwitch: UISwitch!
    @IBOutlet private weak var submitButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        $isSubmitAllowed.receive(on: DispatchQueue.main).assign(to: \.isEnabled, on: submitButton)
    }

    @IBAction func didSwitch(_ sender: UISwitch) {
        isSubmitAllowed = sender.isOn
    }
}

To break this down:

  • The UISwitch will trigger the didSwitch method and change the isSubmitAllowed value to either true or false
  • The value of the submitButton.isEnabled is bound to the isSubmitAllowed property
  • Any changes to isSubmitAllowed are assigned to this isEnabled property on the main queue as we’re working with UI

This first thing you might notice is the dollar sign in front of isSubmitAllowed. It allows you to access the wrapped Publisher value. From that, you can access all the operators or, like we did in the example, subscribe to it.

Memory management in Combine

RxSwift comes with a DisposeBag and Combine comes with a AnyCancellable. This class calls cancel() on deinit and makes sure subscriptions terminate early. Without implementing this you can end up with retain cycles. Taking the above example, we can add it as followed to make sure our submit button subscription is released correctly:

final class FormViewController: UIViewController {

    @Published var isSubmitAllowed: Bool = false
    private var switchSubscriber: AnyCancellable?

    @IBOutlet private weak var acceptTermsSwitch: UISwitch!
    @IBOutlet private weak var submitButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        switchSubscriber = $isSubmitAllowed.receive(on: DispatchQueue.main).assign(to: \.isEnabled, on: submitButton)
    }

    @IBAction func didSwitch(_ sender: UISwitch) {
        isSubmitAllowed = sender.isOn
    }
}

The lifecycle of the switchSubscriber is linked to the lifecycle of the FormViewController. Whenever the view controller is released, the property is released as well and the cancel() method is called.

Error types and streams

As soon as you start working with Combine you’ll run into errors about mismatching error types. Every Publisher describes how they can fail and which error type can be expected. Just like we used the map operator in our notification example, you can use operators to recover or react from errors. Common operators you might want to try out:

  • assertNoFailure() which will change the error type to Never and calls an assert when an error occurs.
  • mapError() which allows you to change the error type
  • Other operators like retry, catch, abortOnError, and replaceError

Debugging Combine streams

Debugging functional reactive languages can be hard. It often results in long error descriptions and unreadable stack traces in Xcode. This is often a reason for developers to not use frameworks like RxSwift and ReactiveSwift. Looking at Combine, it seems that it isn’t a really different experience.

Fortunately, there are ways to debug in Combine with use of the following operators:

  • print() to print log messages for all publishing events
  • breakpoint() which raises the debugger signal when a provided closure needs to stop the process in the debugger
  • breakpointOnError() which only raises the debugger upon receiving a failure

A list of all Publisher operators

Unfortunately, it’s hard to list all Publisher operators here and keep them up to date. The best way to find them is by diving into the documentation topics. However, to give you some idea here’s a word web from WWDC:

Combine Operators in Swift

Combine Operators in Swift

Using Combine with MVVM

The Combine framework is perfectly suitable to work in combination with MVVM. In fact, it’s a lot better with Combine! I’m not going into too much depth, but the example from before can be converted into an MVVM example as followed:

struct FormViewModel {
    @Published var isSubmitAllowed: Bool = false
}

final class FormViewController: UIViewController {

    private var switchSubscriber: AnyCancellable?
    private var viewModel = FormViewModel()

    @IBOutlet private weak var acceptTermsSwitch: UISwitch!
    @IBOutlet private weak var submitButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        switchSubscriber = viewModel.$isSubmitAllowed.receive(on: DispatchQueue.main).assign(to: \.isEnabled, on: submitButton)
    }

    @IBAction func didSwitch(_ sender: UISwitch) {
        viewModel.isSubmitAllowed = sender.isOn
    }
}

When should I use Combine?

Now that you know the basic principles of Combine, there’s an important question left: when should you use it?

If we take a look at a quote from the Apple docs, Combine gives you the following:

By adopting Combine, you’ll make your code easier to read and maintain, by centralizing your event-processing code and eliminating troublesome techniques like nested closures and convention-based callbacks.

Although this is definitely true, we just saw that debugging can be quite hard. Another downside is that it comes with a learning curve. Not only you, but all your colleagues need to get into Combine and know how to work with it. It’s not uncommon to end up with a project full of streams and subscribers ending up with quite a difficult codebase if you’re not familiar with a framework like Combine.

Therefore, before you start using Combine, make sure to try it out in a somewhat smaller project. Discuss the framework with your colleagues and ask yourself whether it’s actually required to write your code solutions with Combine. If you go for it, great! But keep an eye sharp and don’t end up with a project full of wires and hard to debug code, because that could eventually slow you down. Go for composition first and benefit from Combine if state changes a lot and asynchronous code are required a lot.

Swift Playgrounds

The whole framework is even better explained with its own Swift Playground. Therefore, I’ve created one to guide you through some basic principles of Combine. You can check it out here.

Conclusion

You should be able to get yourself started with Combine. The basic principles are explained and there’s a lot more to cover. Many of the above topics can be written in a blog post on its own. Therefore, I encourage you to start exploring Combine yourself!

Also, the following WWDC sessions are a great start to give yourself some more background information:

Thanks!

Collect by WeTransfer Collect by WeTransfer is the best way to organize your ideas. Save content from across your apps and bring it together for your friends, your team, or just for yourself.