You are on page 1of 11

Combine 101 | Scott Gardner

scotteg.github.io/combine-101

August 11, 2019

Back

Combine 101

Posted by scotteg on August 11, 2019

Contents

What is Combine?
Combine is a new reactive framework created by Apple that streamlines how you can
process asynchronous operations.

Combine code is declarative and succinct, making it easy to write and read once you
have a basic understanding of it.

Combine also allows you to neatly organize related code in one place instead of having
that code spread out across multiple files.

Publishers
The essence of Combine is: publishers send values to subscribers.

To become a publisher, a type must adopt and conform to the Publisher protocol,
which includes the following:

The Publisher’s Interface

associatedtype Output

associatedtype Failure : Error

These associatedtype s define the interface of the publisher. Specifically, a publisher


must define the type of values it will send, and its failure error type or Never if it will
never send an error.

The Subscriber Requests to Subscribe

public func subscribe<S>(_ subscriber: S) where S : Subscriber, Self.Failure ==


S.Failure, Self.Output == S.Input

The subscriber calls this method on the publisher to subscribe to it.

1/11
The Publisher Creates the Subscription

func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure,


Self.Output == S.Input

The publisher calls this method on itself to actually create the subscription.

A Publisher Can Finish or Fail


In addition to sending values, a publisher can send a single completion event.

Once a publisher sends a completion event, it’s done and can no longer send any more
values.

A completion event can either indicate that the publisher completed normally
( .finished ) or that an error has occurred ( .failure(Failure) ). If a publisher does
fail with an error, it will send the error.

How to Create Publishers

Combine is integrated throughout the iOS SDK and Swift standard library. For example,
this enables you to create a publisher from a NotificationCenter.Notification , or
even an array of primitive values.

let notificationPublisher = NotificationCenter.default.publisher(for:


Notification.Name("SomeNotification"))

let publisher = Just("Hello, world!")

You can create your own custom publisher types by adopting and conforming to the
Publisher protocol.

However there are two Publisher types that will most often suit your needs without
having to define a custom publisher: PassthroughSubject and
CurrentValueSubject .

Passthrough Subject

A passthrough subject enables you to send values through it. It will pass along values and
the completion event.

// 1

let passthroughSubject = PassthroughSubject<Int, Never>()

// 2

passthroughSubject.send(0)

passthroughSubject.send(1)

passthroughSubject.send(2)

1. Create a passthrough subject of type Int that will never send an error.
2. Send the values 0 , 1 , and 2 on the passthrough subject.

2/11
Current Value Subject

A current value subject is initialized with a starting value that it will send to new
subscribers, and you can also send new values through it in similar manner to a
passthrough subject. You can also ask a current value subject for its current value at any
time by accessing its value property.

// 1

let currentValueSubject = CurrentValueSubject<Character, Never>("A")

// 2

print(currentValueSubject.value)

// 3

currentValueSubject.send("B")

currentValueSubject.send("C")

1. Create a current value subject of type String that will never send an error, with an
initial value of "A" .
2. Print the current value subject’s value .
3. Send the values "B" and "C" on the current value subject.

This will print:

Subscribers
A subscriber attaches to a publisher to receive values from it. This is called a subscription.

Note: A publisher will not send values until it has a subscriber.

Subscribers must adopt and conform to the Subscriber protocol, which includes the
following:

The Subscriber’s Interface

associatedtype Input

associatedtype Failure: Error

These associatedtype s define the interface of the subscriber. Specifically, a subscriber


must define the type of values it will receive, and the failure error type it will receive or
Never if it will not accept an error.

The Publisher Issues the Subscription

func receive(subscription: Subscription)

The publisher calls this method on the subscriber to give it the subscription to the
subscriber.

3/11
The Publisher Sends New Values

func receive(_ input: Self.Input) -> Subscribers.Demand

The publisher calls this method on the subscriber to send a new value to the subscriber.
Notice that the return value is Subscribers.Demand . See the Handling
backpressure section for more info.

The Publisher Tells the Subscriber When It’s Done or Has Failed

func receive(completion: Subscribers.Completion<Self.Failure>)

The publisher calls this method to tell the subscriber that it has completed, either
normally or with an error.

How to Create Subscriptions

Note: In order for a subscription to be created between a publisher and a subscriber, the
publisher’s Output and Failure types must match the subscriber’s Input and
Failure types.

There are two ways to create a subscription to a publisher:

1. By using one of the sink operators.


2. By using one of the assign(to:on:) operators.

Note: A subscription returns an instance of AnyCancellable that represents the


subscription. You must save the subscription token or else the subscription will be canceled
as soon as program flow exits the current scope.

There are two ways to store a subscription token:

1. As an individual value of type AnyCancellable


2. In a collection of AnyCancellable .

Creating Subscriptions with sink

The sink operators include closure parameters to handle values or a completion event
received from a publisher.

// 1

let publisher = Just("Hello, world!")

// 2

let subscription = publisher

.sink(receiveValue: { print($0) })

1. Create a Just publisher that will send its value to each new subscriber and then
complete.
2. Subscribe and print out the received value.

4/11
This will print:

Hello, world!

You can also add subscriptions to a collection of AnyCancellable :

// 1

var subscriptions = Set<AnyCancellable>()

// 2

let publisher = Just("Hello, world!")

// 3

publisher

.sink(receiveValue: { print($0) })

.store(in: &subscriptions) // 4

1. Create a set of AnyCancellable to store subscriptions in.


2. Create a publisher.
3. Subscribe to the publisher and print out received values.
4. Store the subscription in subscriptions .

Creating Subscriptions with assign(to:on:)

// 1

class Player {

var score = 0 {

didSet {

print(score)

// 2

let player = Player()

// 3

let subscription = [10, 50, 100].publisher

.assign(to: \.score, on: player) // 4

1. Define a Player class with a score property that prints its new value when set.
2. Create an instance of Player .
3. Create a subscription to a publisher of an array of integers.
4. Use assign(to:on:) to assign each value received to the score property on
player .

This will print:

10
50
100

How to Stop Subscriptions

5/11
There are two ways to stop subscriptions:

1. Call cancel() on a subscription token.


2. Do nothing and let normal memory management rules to apply, i.e., the token or
collection of AnyCancellable will call cancel() on the subscriptions upon
deinitialization.

// 1

let passthroughSubject = PassthroughSubject<Int, Never>()

// 2

let subscription = passthroughSubject

.sink(receiveCompletion: { print($0) },

receiveValue: { print($0) })

// 3

passthroughSubject.send(0)

passthroughSubject.send(1)

passthroughSubject.send(2)

// 4

passthroughSubject.send(completion: .finished)

1. Create a passthrough subject of type Int that will never send an error.
2. Subscribe to the passthrough subject.
3. Send the values 0 , 1 , and 2 on the passthrough subject.
4. Send the completion event on the passthrough subject.

This will print:

finished

Handling Backpressure

Backpressure is the pressure caused by the stream of values being sent by a publisher to a
subscriber. If a publisher sends too many values to a subscriber, this can cause problems.
In order to manage that backpressure, every time a subscriber receives a new value, it
must tell the publisher what its willingness is to receive additional values, i.e., its
demand. Demand can only be adjusted additively. In other words, a subscriber can
increase its demand every time it receives a new value, but it cannot decrease it. There are
three levels of demand that a subscriber can return from receive(_:) ->
Subscribers.Demand :

.none
.max(value: Int)
.unlimited

6/11
The sink and assign(to:on:) operators both automatically return .unlimited for
demand. You can define custom subscribers to return a different demand, however this
goes beyond the scope of this introduction.

Operators
Operators are special methods that return a publisher.

Several operators are named and work similarly to methods found in the Swift Standard
Library, such as map , filter , and reduce .

They can receive values from an upstream publisher, perform some operation on those
values, and then send those values downstream.

Note: The terms upstream and downstream are typically used to describe the flow of a
subscription. For example, an operator receives values or a completion event from an
upstream publisher, it processes those values or completion event, and then it may send
values or events downstream to another publisher or a subscriber.

Common Operators

Use map operators to transform values

The map category of operators provide several ways that you can transform values sent
by an upstream publisher, to then send downstream.

Use filter operators to limit which values get through

The filter family of operators provide several ways that you can prevent or limit
values received from an upstream publisher that are sent downstream.

How to Share a Publisher

To understand why you would want to share a subscription, review this example where
two subscribers subscribe to the same publisher.

let subject = PassthroughSubject<Int, Never>()

let publisher = subject

.handleEvents(receiveOutput: { print("Handling", $0) })

_ = publisher

.sink(receiveValue: { print("1st subscriber", $0) })

_ = publisher

.sink(receiveValue: { print("2nd subscriber", $0) })

subject.send(0)

subject.send(1)

This will print:

7/11
Handling 0

1st subscriber 0

Handling 0

2nd subscriber 0

Handling 1

1st subscriber 1

Handling 1

2nd subscriber 1

The handleEvents operator includes closures to handle each event in the publisher’s
lifecycle:

receiveSubscription
receiveRequest
receiveOutput
receiveCompletion
receiveCancel

Each subscriber independently subscribes and handles the values sent by the publisher.
In order to be more efficient, you can use the share() operator to share the publisher to
multiple subscribers.

This will now print:

Handling 0

1st subscriber 0

2nd subscriber 0

Handling 1

1st subscriber 1

2nd subscriber 1

There is one caveat with share() : it will only share values to existing subscribers.

If you add the following code to the end of the previous example:

_ = publisher

.sink(receiveValue: { print("3rd subscriber", $0) })

subject.send(2)

The complete example will now print:

Handling 0

1st subscriber 0

2nd subscriber 0

Handling 1

1st subscriber 1

2nd subscriber 1

Handling 2

1st subscriber 2

2nd subscriber 2

3rd subscriber 2

8/11
The 3rd subscriber does not get the 1 and 2 because it was not yet subscribed.

How to See Every Event

One very useful operator to use when debugging Combine code is the print() operator.
You can insert it anywhere in a publisher or subscription chain of operators.

This will print:

Subscriber: receive subscription: (Multicast)

Subscriber: request unlimited

Publisher: receive subscription: (PassthroughSubject)

Publisher: request unlimited

Publisher: receive value: (0)

Subscriber: receive value: (0)

Publisher: receive value: (1)

Subscriber: receive value: (1)

Subscriber: receive cancel

Publisher: receive cancel

Other Kinds of Operators


Apple groups Combine operators into these categories:

Mapping, including Encoding and Decoding


Filtering, including Matching, Selecting, and Mathematical
Combining, including Reducing and Sequencing
Debugging
Error Handling
Scheduling
Type-Erasing
Adapting
Sharing, including Multicasting
Buffering
Timing
Connecting

These categories are roughly ordered from highest to lowest in terms of typical frequency
of usage, and lowest to highest in terms of complexity. That makes this list a great todo
list if you would like to go beyond the basics and become an expert with Combine.

Create Complex Subscriptions with Multiple Operators


Here is an example of a subscription that involves several operators:

9/11
// 1

let formatter = NumberFormatter()

formatter.numberStyle = .spellOut

// 2

let publisher = (0..<100).publisher

// 3

let subscription = publisher

.dropFirst()

.filter { $0 % 2 == 0 }

.prefix(4)

.map { NSNumber(integerLiteral: $0)}

.compactMap { formatter.string(from: $0) }

.append("Done!")

.sink(receiveValue: { print($0) }) // 4

1. Create a number formatter that will return a string with each number spelled out.
2. Create a publisher from a range of integers from 0 to 100 .
3. Create a subscription to the publisher, using the following operators:
dropFirst() to skip the first value sent.
filter(_:) to only allow even integer values to get through.
prefix(_:) to only take the first four values.
map(_:) to initialize and send downstream an NSNumber instance for each
integer received.
compactMap(_:) to send the return from calling
formatter.string(from:) , filtering out nil s.
append(_:) to append a string onto the received value and send the result
downstream.
4. Subscribe and print received values in the handler.

This will print:

two

four

six

eight

Done!

Conclusion
Combine is a powerful framework for streamlining asynchronous programming, and it is
integrated into many existing frameworks in iOS, macOS, watchOS, and tvOS.

Combine is also tightly integrated with SwiftUI. Together they can be used to create
reactive apps that require a lot less code and complexity than their predecessor
frameworks, that are also much more robust and less prone to common bugs and
unexpected behaviors.

If you’d like to learn more about Combine, check out this book I co-authored:

10/11
Combine: Asynchronous Programming with Swift

It’s packed with coverage of Combine concepts, hands-on


exercises in playgrounds, and complete iOS app projects.
Everything you need to transform yourself from novice to
expert with Combine — and have fun while doing it!

Back

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

© 2020 Scott Gardner

11/11

You might also like