Getting started with the Combine framework in Swift

Combine was introduced as a new framework by Apple at WWDC 2019. The framework provides a declarative Swift API for processing values over time and can be seen as a 1st party alternative to popular frameworks like RxSwift and ReactiveSwift.

If you’ve been trying out SwiftUI, you’ve likely been using Combine quite a lot already. Types like ObservableObject and Property Wrappers like @Published all use Combine under the hood. It’s a powerful framework to dynamically respond to value changes over time.

At first, “processing values over time” might sound quite overwhelming. What does it actually mean? What can I do with it, and when should I use it?

Apple describes the framework as follows:

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

Hopefully, after reading this blog post, you should be able to answer the above questions, and you’re able to understand what Combine can do.

What is Combine?

You can compare the Combine framework to frameworks like RxSwift and ReactiveSwift (formally known as ReactiveCocoa). It allows you to write functional 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.

In other words, a FRP sequence could be described as follows:

  • Once a network response is received
  • I want to map it’s data to a JSON model
  • And assign it to my View.

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 understand better 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 Publishers’ examples 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 use Publishers from 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) 

The label is now a “Subscriber” to the notification “Publisher” and waits for new values to process. 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 receiving 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 UIKit
 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!

Just before we dive into the rules of subscriptions, I’d like to point out that the above code example is creating a Subscriber directly. Combine comes with a lot of convenient APIs which allows you to “Subscribe” the notification “Publisher” to the label as follows:

 let lastPostLabel = UILabel()
 blogPostPublisher.assign(to: \.text, on: lastPostLabel)

The assign(to:on:) operator subscribes to the notification publisher and links its lifetime to the lifetime of the label. Once the label gets released, its subscription gets released too.

The rules of a subscription

Now that 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. You can receive zero, or more notifications, but it’s never really ending.

An example of a completing publisher is the URLSessionTask Publisher that completes a data response or a request error. The 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.

These rules are important to remember to understand the lifetime of a subscription.

@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

The 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. Note that you can only use this @Published property wrapper on a class instance.

Memory management in Combine

Memory management is an important part of Combine. Subscribers need to retain a subscription for as long as it needs to receive and process values. However, once a subscription is no longer needed, it should release all references correctly.

RxSwift comes with a DisposeBag and Combine comes with a AnyCancellable. The AnyCancellable class calls cancel() on deinit and makes sure subscriptions terminate early. Without using this class correctly, 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()
         
         /// Save the cancellable subscription.
         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 of the subscription is called.

Storing multiple subscriptions

In some cases you might have multiple subscriptions to retain. In this case, you could use the store(in:) operator to save each subscriber to a collection of cancellables:

 final class FormViewController: UIViewController {
 
     @Published var isSubmitAllowed: Bool = false
     private var subscribers: [AnyCancellable] = []
 
     @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)
             .store(in: &subscribers)
     }
 
     @IBAction func didSwitch(_ sender: UISwitch) {
         isSubmitAllowed = sender.isOn
     }
 } 

As you can see, the submit allowed subscription is stored in a collection of subscribers. Once the FormViewController is released, the collection is released, and its subscribers get cancelled.

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 as a result. Like we used the map operator in our notification example, you can use operators to recover or react to 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, and replaceError

You can learn more about error handling in Combine in my blog post Error handling in Combine explained with code examples.

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 a reason for many developers not to 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 the 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

You can read more about this in detail in my blog post Combine debugging using operators in Swift.

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:

 final class 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 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 is required.

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. 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:

To read more about Swift Combine, take a look at my other Combine blog posts: