Apple Combine Framework

Prenez
5 min readJan 26, 2021

Just my notes …

(These are my personal Combine notes. It’s not meant to be a tutorial, but a refresher for someone who is learning Combine).

Combine is the recent framework from Apple that gives you a pub-sub model for data handling in a Swift (and SwiftUI app).

There are publishers, and there are subscribers.

Publishers have two outputs: a value of some Type, and an Error of some Error Type.

Subscribers have two inputs: a value of some Type, and an Error of some Error Type.

Publishers provide access to a pile of data and make it serially available via the value output; subscribers receive the data from the publishers via the value input.

The types of output for Publisher and input for Subscribers have to match. For example, if your publisher outputs a String, then your subscriber must input a String.

The Error Types of output for publishers and input for subscribers must match. For example, if your publisher outputs an Error of type MyCustomError, then your subscribers must input an error of type MyCustomerror.

==== PUBLISHERS

A publisher can output
*a value
*a completion
*an error

and that’s it.

==== COMPLETIONS AND ERRORS FINISH THE PUBLISHER FOR GOOD

A completion is sent when a publisher has no more values to send. Once a completion or error is sent, that publisher will never send a value again. It is dead.

==== UTILITY

There are three kinds of utility publishers:

Just — send one value and one completion
Empty — send one completion
Fail — send one Error

==== FOUNDATION

Combine also supports Foundation structures like NotificationCenter, Timers and Key Value Objects.

You can get

NOTIFICATIONCENTER
let pub = NotificationCenter.default.publisher (for:
UserDefaults.didSomeNotification
)
_output is Notification, error is Never_

now this publisher will publisher notifications to a subscriber.

TIMER
let pub = Timer(every: x seconds)
_output is Date, error is Never_

now this publisher will send timing events to a subscriber.

KEY VALUE OBSERVING
class ShoppingCart: NSObject {
var total: Double = 0
}

let cart = ShoppingCart()
let pub = cart.publisher(for: \.total)
_output is Double, error is Never_

=== SUBSCRIBERS

There are two kinds of subscribers
sink
assign

==== SINK AND ASSIGN

sink subscribers receive values from a stream, and you handle them however you like in the closure.

assign subscribers get an object and assign values from a stream to the property of an object

==== LONGFORM SINK

let pub = [1,2,3].publisher
let sub = Subscribers.Sink<Int,Never> (
receiveCompletion: { completion in
switch completion …
},
receiveValue: { value in
print(value)
}
}

pub.subscribe(sub)

==== SHORTHAND SINK

implicit subscriber

pub.sink (
receiveCompletion: { completion in
switch completion …
},
receiveValue: { value in
print(value)
}
}

===== LONGFORM ASSIGN

assign a value from the stream to the latestMessage property on the forum object

let messages = [“hello”,”world”].publisher
/* string, never */

class Forum {
var latestMessage: String = “” {
didSet {
print(“\(latestMessage)”)
}
}
}
let forum = Forum()
let subscribe = Subscribers.assign<Forum, String> ( object: forum, keyPath: \.latestMessage)
messages.subscribe(sub)

==== SHORTHAND ASSIGN

messages.assign(to: \.latestMessage, on: forum)

===== SUBJECTS

==== SUBJECTS ARE PUBLISHERS

====SUBJECTS ARE PUBLISHERS

====SUBJECTS ARE PUBLISHERS

So you remember that now, right?

Subject/Publishers are a way to transforming data from mechanisms outside the Combine system, into a Combine stream. That means there’s a send
message that goes with subjects, and it means you have to handle cancelable.

==== PASSTHROUGH SUBJECT PUBLISHER

For passthrough subject, you’ll set up a publisher with a subscriber, and then send data into the publisher.

let publisherSubject = PassThroughSubject<Int, Never>
publisherSubject.sink {
handle the data
}

elsewhere…
send data into the publisher

publisherSubject.send(input: aValue)
publisherSubject.send(completion: .finished)
publisherSubject.send(completion: .failure(MyCustomError()))

=== CURRENTVALUE SUBJECT PUBLISHER

a current value subject is essentially a publisher that includes an init variable.

let word = CurrentValueSubject<String, Never(“hello”)
“hello” is sent immediately to any new subscribers

then you can send new values
word.send(“world”)

you can look at the current value
let x = word.value

Note, all subscribers receive whatever values you send to a CurrentValueSubject.

==== VOID

You can make the output of a PassthroughSubject Void, if you simply want to send the event:

let pub = PassThroughSubject<Void,Never>
pub.send()
// just send the event, such as a button tap

==== CANCELLABLES

once a publisher has no more values to send a subscription, the subscription is cancelled unless it’s retained.

subscription.cancel() is the way to do it manually

for example

let x = ints.sink { value in
print (value)
}

x is an AnyCancellable — the subscription, and it retrains the subscription for as long as its needed.

====

CANCELLABLE EXAMPLE WITH TIMER

class TickTock {
var timerCancellable: AnyCancellable? // retains the subscription
init() {
var timerCancellable = Timer.publish(every 5 second on main thread and common) {
.autoconnect (restarts the Timer, or you can use .connect)
.sink { value in
}

}

let example = TickTock()
DispatchQueue.main.asyncAfter (5 seconds)
example = nil // this calls cancel on the subscription
}

==== CANCELLABLE WITH SET<ANYCANCELLABLE>

You usually want to place your cancellables in a Set, to support multiple subscribers.
You use .store to place the cancellable in a set on the object.
This way when you add a new subscriber you don’t wipe out the retain from the prior subscriber.

class TickTock {
var cancellables: Set<AnyCancellable> = [] // retains the subscription
init() {
Timer.publish(every 5 second on main thread and common) {
.autoconnect (restarts the Timer, or you can use .connect)
.sink { value in
}
.store(&cancellables)
}

==== SELF IN SINK

if you have a method on the object:

class TickTock {
var cancellables: Set<AnyCancellable> = [] // retains the subscription

func tick() {
print(“tick”)
}

then if you call the method in sink:

init() {
Timer.publish(every 5 second on main thread and common) {
.autoconnect (restarts the Timer, or you can use .connect)
.sink { value in
self.tick()
}
.store(&cancellables)
}

you have to use weak or unowned in order to make sure you don’t create a retain cycle.

unowned means you are certain that the object will exist at the time you call it’s method in .sink. Otherwise, you weak.

unowned in a pointer, weak is an Optional.

==== REQUEST UNLIMITED

If you look at the printout in the console you’re going to see ‘request unlimited’ when the Timer starts up. The subscription controls how many values it’s going to accept from the publisher, and in the case of a Timer, as many as it wants to omit. This is of more concern when you’re writing your own publisher or subscriber.

This is an area known as back pressure management, how much water is coming out of the hose.

--

--

Prenez

Writes iOS apps in Swift and stories in American English.