Getting Started with Combine in Swift

Minhaz Panara
Technology14 mins read
getting-started-with-combine-in-swift-min.webp

Getting Started with Combine in Swift

The Combine Framework is huge and as such, impossible to go over in a single article. For the sake of brevity, I’ll split this into a series of posts. Consider this first one to be just an introduction to Combine — here, you’ll learn how Publishers & Subscribers work in Combine, create subscriptions for arrays, Notification Center, and finally how to create a custom publisher.

Before we dive in, make sure you can follow along! You’ll need access to:

  • macOS 10.15+
  • iOS 13.0+
  • Swift 5
  • Xcode 11

What is Combine?

Combine was introduced as a new framework by Apple at WWDC-2019. It provides a declarative Swift API for processing values over time. The framework can be compared 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. You can consider examples of these kinds of values — network responses, user interface events, other types of asynchronous data.

Here’s an example of an FRP sequence:

  • A network response is received
  • Its data is mapped to a JSON model
  • It is then assigned to the View

What is FRP (Functional Reactive Programming)?

In the FRP context, data flows from one place to another automatically through subscriptions. It uses the building blocks of Functional Programming, like the ability to map one data flow into another. FRP is very useful when data changes over time.

As an example, if you have a String variable and you want to update the text of a UILabel, this is how you’d do it with FRP:

  • Create a Subscription for the UILabel for new text values
  • Push the value of the variable through the Stream (Subscription). Eventually, all the Subscribers will be notified of the new values. In our case, UILabel (one of our Subscribers) will update the received text on the UI.

FRP is also very useful in asynchronous programming and hence in UI rendering, which is based on the asynchronous response data. You generally get a response back and pass it in a completion closure. In FRP, the method you call to make a request would return a publisher that will publish a result once the request is finished. What’s the benefit here? You get rid of a heavily nested tree of completion closures.

Publishers and Subscribers

A Publisher exposes values (that can change) on which a subscriber subscribes to receive all those updates. If you relate them to RxSwift :

  • Publishers are like Observables
  • Subscribers are like Observers

A Combine publisher is an object that sends values to its subscribers over time. Sometimes this is a single value, and other times a publisher can transmit multiple values or no values at all.

In the below diagram:

  • Each row represents a publisher
  • The Circles on each line represent the values that the publisher emits
Loading ...

Let’s examine both of them.

  • The first arrow has a line at the end. This represents a completion event. After this line, the publisher will no longer publish any new values.
  • The bottom arrow ends with a cross. This represents an error event. That means, something went wrong and the publisher will now no longer publish any new events.

Let’s summarise:

  • Every publisher in the Combine framework uses these same rules, with no exceptions.
  • Even publishers that publish only a single value must publish a completion event after publishing their single value.

Subscribing to a Simple Publisher

The Combine Framework adds a publisher property to an Array.We can use this property to turn an array of values into a publisher that will publish all values in the array to the subscribers of the publisher.

[1, 2, 3]
        .publisher
        .sink(receiveCompletion: { completion in
            switch completion {
            case .failure(let error):
                print("Something went wrong: \(error)")
            case .finished:
                print("Received Completion")
            }
        }, receiveValue: { value in
            print("Received value \(value)")
        })

Foundation Framework and Combine

The Foundation Framework has extensions to work with Combine. You are already familiar with some publishers.

  1. A URLSessionTask: it publishes the data response or request error. Operators for JSON decoding. Notification: a publisher for a specific
  2. Notification.Name which publishes the notification.
  3. Let’s take a look at an example. Launch Swift Playground in your Xcode, and let’s get started.

NotificationCenter.Publisher

Let’s create a new publisher for a new-event notification.

extension Notification.Name {
    static let newEvent = Notification.Name("new_event")
}
 
struct Event {
    let title: String
    let scheduledOn: Date
}
 
let eventPublisher = NotificationCenter.Publisher(center: .default, name: .newEvent, object: nil)

This publisher will listen for incoming notifications for the newEvent notification name. Now we need a subscriber.

We can create a variable called theEventTitleLabel that subscribes to the publisher.

let theEventTitleLabel = UILabel()

let newEventLabelSubscriber = Subscribers.Assign(object: theEventTitleLabel, keyPath: \.text)
eventPublisher.subscribe(newEventLabelSubscriber)

The above code still has some errors (when you build), like:

  • Swift compilation error: No exact matches in call to instance method subscribe

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

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

Now, the compilation error should go away.

At this point, the code looks like this:

import UIKit
import Combine

extension Notification.Name {
    static let newEvent = Notification.Name("new_event")
}

struct Event {
    let title: String
    let scheduledOn: Date
}

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

let theEventTitleLabel = UILabel()

let newEventLabelSubscriber = Subscribers.Assign(object: theEventTitleLabel, keyPath: \.text)
eventPublisher.subscribe(newEventLabelSubscriber)

let event = Event(title: "Introduction to Combine Framework", scheduledOn: Date())
NotificationCenter.default.post(name: .newEvent, object: event)
print("Recent event notified is: \(theEventTitleLabel.text!)")

Whenever a new event notification is received, the label “Subscriber” will update its text value. It is great to see a working example (of Publisher-Subscriber)!

Before we go to the next example(?), it is important to note that Combine comes with a lot of convenient APIs that allow us to subscribe to the publisher with fewer lines of code.

let theEventTitleLabel = UILabel()
/*
let newEventLabelSubscriber = Subscribers.Assign(object: theEventTitleLabel, keyPath: \.text)
eventPublisher.subscribe(newEventLabelSubscriber)
*/
eventPublisher.assign(to: \.text, on: theEventTitleLabel) // <-- [new code line]

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

Timer Subscription

Creating a Subscription

import Combine
import Foundation
import PlaygroundSupport

let subscription = Timer.publish(every: 1, on: .main, in: .common)
    .autoconnect()
    .sink { output in
        print("finished stream with : \(output)")
    } receiveValue: { value in
        print("receive value: \(value)")
    }

When you run the above code in Swift Playground, the output will be:

receive value: 2021–12–16 07:59:23 +0000
receive value: 2021–12–16 07:59:24 +0000
receive value: 2021–12–16 07:59:25 +0000
receive value: 2021–12–16 07:59:26 +0000
receive value: 2021–12–16 07:59:27 +0000
…

Explanation

  • Timer.publish() : Returns a publisher that repeatedly emits the current date on the given interval.
  • .autoconnect : Starts the timer when the Timer object is created.
  • sink {}:
    • output block: called when the subscription is finished.
    • receiveValue block: called when any value is published.

But, we want this to be stopped later. I.e. we should cancel the subscription when the task is done.

Cancelling the Subscription

1. Call cancel()

import Combine
import Foundation
import PlaygroundSupport

let subscription = Timer.publish(every: 1, on: .main, in: .common)
    .autoconnect()
    .print("data stream")
    .sink { output in
        print("finished stream with : \(output)")
    } receiveValue: { value in
        print("receive value: \(value)")
    }

RunLoop.main.schedule(after: .init(Date(timeIntervalSinceNow: 5))) {
    print(" - cancel subscription")
    subscription.cancel()
}

When you run the above code, the output is:

data stream: request unlimited
data stream: receive value: (2021–12–16 08:23:28 +0000)
receive value: 2021–12–16 08:23:28 +0000
data stream: receive value: (2021–12–16 08:23:29 +0000)
receive value: 2021–12–16 08:23:29 +0000
data stream: receive value: (2021–12–16 08:23:30 +0000)
receive value: 2021–12–16 08:23:30 +0000
data stream: receive value: (2021–12–16 08:23:31 +0000)
receive value: 2021–12–16 08:23:31 +0000
data stream: receive value: (2021–12–16 08:23:32 +0000)
receive value: 2021–12–16 08:23:32 +0000
— cancel subscription
data stream: receive cancel

Explanation subscription.cancel() : Cancel the timer to stop emitting the value.

2. Set subscription nil

import Combine
import Foundation
import PlaygroundSupport

var subscription: Cancellable? = Timer.publish(every: 1, on: .main, in: .common)
    .autoconnect()
    .print("data stream")
    .sink { output in
        print("finished stream with : \(output)")
    } receiveValue: { value in
        print("receive value: \(value)")
    }
    
RunLoop.main.schedule(after: .init(Date(timeIntervalSinceNow: 5))) {
    print(" - cancel subscription")
    // subscription.cancel()
    subscription = nil
}

This will also cancel the subscription. It is generally a use case of cancelling subscriptions when a View is going to be deallocated and a subscription is no longer needed.

Calling cancel() frees up any allocated resources. It also stops side effects such as timers, network access, or disk I/O.

Summarising the Rules of Subscriptions

Let’s look at the rules of subscriptions.

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

What? A Subscription with NO Completion?Yes, subscriptions can come with completion, but this is not always the case. Our Notification example is one such Publisher — it will never be complete. You can receive zero or more notifications, but there’s no real end to it.

So, what are completing publishers?The URLSessionTask is a Publisher that completes a data response or a request error. The fact is that whenever an error is thrown from a stream, the subscription is dismissed even if the stream allows multiple values to pass through.

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

Using @Published to bind values for the changes over time

@Published is a property wrapper and the keyword adds a Publisher to any property.

Let’s take a look at a simple example of a boolean which is assigned to the UIButton state (enabled or disabled).

class AgreementFormVC: UIViewController {

    @Published var isNextEnabled: Bool = false
    @IBOutlet private weak var acceptAgreementSwitch: UISwitch!
    @IBOutlet private weak var nextButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        $isNextEnabled
            .receive(on: DispatchQueue.main)
            .assign(to: \.isEnabled, on: nextButton)
    }
    
    @IBAction func didSwitch(_ sender: UISwitch) {
        isNextEnabled = sender.isOn
    }
    
}

To break this down:

To break this down:

The UISwitch will trigger the didSwitch(_ sender: UISwitch) method and change the isNextEnabled value to either true or false. The value of the nextButton.isEnabled is bound to the isNextEnabled property. Any changes to isNextEnabled are assigned to this isEnabled property on the main queue as we’re working with UI.

property on the main queue as we’re working with UI.

You might notice the dollar sign in front of isNextEnabled. This allows you to access the wrapped Publisher value. From that, you can access all the operators or, like we did in the previous example, subscribe to it. Note that you can only use this @Published property wrapper on a class instance.

Combine & Memory Management

Subscribers can retain a subscription as far as they want to receive and process values. The subscription references should be released when it is no longer needed.

In RxSwift , a DisposeBag is used for memory management. → In Combine, AnyCancellable is used. The AnyCancellable class calls cancel()and makes sure subscriptions are terminated.“Cancelling subscriptions help avoid retain-cycles.”

Let’s update the last example to make sure the nextButton subscription is released correctly.

class AgreementFormVC: UIViewController {
    
    @Published var isNextEnabled: Bool = false
    private var switchSubscriber: AnyCancellable?
    @IBOutlet private weak var acceptAgreementSwitch: UISwitch!
    @IBOutlet private weak var nextButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
    
        // Save the Cancellable Subscription
        switchSubscriber = $isNextEnabled
            .receive(on: DispatchQueue.main)
            .assign(to: \.isEnabled, on: nextButton)
    }

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

The Lifecycle of the switchSubsriber is linked to the lifecycle of the AgreementFormVC.

I.e. Whenever the view controller is released, the property is released as well and the cancel() method of the subscription is called.

Storing Multiple Subscriptions

There is a good facility to handle multiple subscriptions in a class, which can be stored in a Set.

class AgreementFormVC: UIViewController {
    
    @Published var isNextEnabled: Bool = false
    // private var switchSubscriber: AnyCancellable?
    private var subscribers = Set<AnyCancellable>() // ← set of all subscriptions 
    @IBOutlet private weak var acceptAgreementSwitch: UISwitch!
    @IBOutlet private weak var nextButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Save the Cancellable Subscription
        $isNextEnabled
            .receive(on: DispatchQueue.main)
            .assign(to: \.isEnabled, on: nextButton)
            .store(in: &subscribers) // ← storing the subscription
    }
    
    @IBAction func didSwitch(_ sender: UISwitch) {
        isNextEnabled = sender.isOn
    }
    
}

In the above code, the isNextEnabled subscription is stored in a collection of subscribers. Once theAgreementFormVC is released, the collection is released and its subscribers get cancelled.

That wraps up our introduction to the Combine Framework. You can find all the code from this article here on GitHub. Next, we’ll take a

look at PassthroughSubject, CurrentValueSubject & Operatorswith some examples. Stay tuned!

References

Category
Technology
Published On
17 Feb, 2022
Share

Subscribe to our tech, design & culture updates at Proximity

Weekly updates, insights & news across all teams

Copyright © 2019-2023 Proximity Works™