Blog

  • Home / IOS / Getting started with the Combine framework in Swift

Getting started with the Combine framework in Swift

  • April 21, 2021

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.

 

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:

 

  1. Publishers are the same as Observables
  2. 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:

 

  1. A URLSessionTask Publisher that publishes the data response or request error
  2. Operators for easy JSON decoding
  3. 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:

 

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 retrycatch, 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 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

 

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:

 

 

  

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 } } 

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:

 

 

-Metacoder