21 JUNE 2022/SWIFTUI, COMBINE

Combine 101: Introduction

Combine 101: Introduction

If you have been around in the Swift world for a long time, or you are new to iOS development, odds are you have heard of the Combine framework. The combine framework was introduced by Apple at WWDC19, and was Apple's native answer to RxSwift, ReactiveSwift or any other functional reactive programming frameworks you may have heard of/used before.

So what is it?

Combine is a first party, declarative framework for asynchronous processing values (streams of data) over time. Instead of having multiple callbacks, completion handlers, or delegations, you can create a singular processing chain for your stream of data.

The key part here is the framework is declarative, meaning you describe what the program does & not the flow of control. This is compared to imperative styles of programming where our code is how the program works & implementing tasks to mutate some state. Combine is has functional reactive principles as well. This means our program reacts to each stream of data, and is based in functions with a set input & set output. As you will see in some code examples, Combine is both declarative, functional and reactive because our data flows from one function to another, with each function having a defined input and output.

We can think of Combine having 3 main players: Publishers, Subscribers, and Operators. Let's get familiar with each one!

Publishers

The Publisher type, is a protocol defined in Swift. Swift Publishers are used to transmit sequences of values over time. You can think of this as a man shouting information from a rooftop at intermittent periods of time. He is a publisher.

Publishers have 2 associatedtypes representing the Output of the Publisher, and how the Publisher will fail. We can define each of these. Also the Publisher protocol has a receive(subscriber:) function hook a subscriber up to itself. Per the contract for the this function, a publisher’s output must match subscriber’s input, and so should the failure types.

Most Swift foundation objects/functions support built in publishers as of 2022. For example, NotificationCenter & NSTimer:

let notificationPublisher = NotificationCenter.Publisher(center: .default, name: .init("Test"), object: nil) let timerPublisher = Timer.TimerPublisher(interval: 2, runLoop: RunLoop.main, mode: .default)

We have created these objects and now they can stream and emit values over time, we can also react to these values however we wish.

As opposed to creating our own objects that implement the Publisher protocol, Apple suggests we use one of their built-in types whenever we need a custom publisher. We can either:

  1. Use a concrete subclass of Subject like PassThroughSubject for example, to manually send values
  2. Use a CurrentValueSubject
  3. Add the @Published annotation wrapper to a property we wish

What are Subjects?

A subject is a type of Publisher that exposes functions, in order for outside callers to Publish events into the stream of data. Swift gives us 2 built-in Subjects for free. These are namely CurrentValueSubject and PassThroughSubject.

A CurrentValueSubject starts with an initial value, and publishes every time that value changes. We can also access the inner value with this type of Subject.

let subject = CurrentValueSubject<Int, Never>("Test") // initial value of Test print(subject.value) // prints 'Test' subject.send("Blog") print(subject.value) // prints 'Blog'

A PassThroughSubject does not have an initial value, but broadcasts data downstream. This type also drops values if there are no subscribers, or the current demand is zero. We are unable to access any values within this object.

let subject = PassthroughSubject<String, Never>() // no initial value // subscribe to events subject.sink(receiveCompletion: { _ in print("finished") }, receiveValue: { value in print(value) }) subject.send("Alex") subject.send("is Blogging!") subject.send(completion: .finished) // done! // Alex // is Blogging! // finished

@Published property wrapper

Swift allows us to add a publisher to any property by using the @Published property wrapper. You can access the inner publisher by using the $ symbol in front of your property. It is important to note the @Published property wrapper is class-constrained, we cannot use them in structs. Let's see a code example (Note, we will learn about AnyCancellable soon!)

final class FooClass { private var subscriber: AnyCancellable? @Published var isOn: Bool = false // published init() { // this will print false, then true subscriber = $isOn .sink(receiveValue: { value in print(value) }) change() } func change() { isOn.toggle() } }

Also worth noting, the publishing will happen in the object's willSet block, so anyone downstream will get the new value before it is set.

Subscribers

Up until now we have been referring to those listening to publishers as "Downstream". But they have a formal name, and that name is Subscriber. A Subscriber listens to events sent from a Publisher. A subscriber can receive either an input value, or a completion value with some indicator of a success/failure.

Swift gives us 2 built in Subscribers: Subscribers.Sink & Subscribers.Assign.

Subscribers.Sink is a simple Subscriber class that requests an unlimited amount of values upon its subscription.

Subscribers.Assign assigns received elements to a property indicated by a key path.

let publisher = Just(false) publisher.sink(receiveCompletion: { _ in print("finished") }, receiveValue: { value in print(value) }) // false // finished

Publisher + Subscriber Lifecycle

Let's explore what happens under the hood when we build a subscription between a publisher & subscriber

let subject = PassthroughSubject<Int, Never>() let token = subject .print() .sink(receiveValue: { print("received by subscriber: \($0)") }) subject.send(1) // receive subscription: (PassthroughSubject) [Subscriber connects to a publisher] [Publisher creates a subscription] [Publisher acknowledges subscription request] // request unlimited [Subscriber requests number of elements it want to receive] [Our Demand is currently unlimited] // receive value: (1) [The Publisher sends values by calling receive(_:) on the subscriber] // received by subscriber: 1 [Returns how many items are expected to be received] [Subscriber can only increase or leave demand the same] // receive cancel [Subscription either ends in a cancel, successful finish, or fail with an error]

Conclusion

This concludes our intro into combine! Hope you learned a good foundation for our next few articles!

Helpful links:

WWDC19 Intro video

Alex Stevens

Alex Stevens

iOS Software Engineer