There are three things you generally want to do with Combine: publish values, manipulate those values, and receive those values elsewhere. Let’s see how we can do each of those.
Publishing Values
Values are published with, predictably, Publisher
s. The most common way I see this happen is through @Published
fields. So, for instance:
class App { @Published var name: String? }
This annotation allows you to refer to a Publisher
of that variable:
let app = App() app.name // the variable itself app.$name // the publisher of the variable
Whenever the variable’s value is changed, the publisher will let all its subscribers know. But be careful! When you subscribe to one of these types of Publisher
s, it will immediately hand you the current value – so if you were expecting to be updated at a later point, you might be in trouble. Luckily, you can manipulate Publisher
s to get the behavior you want.
Manipulating Values
Here are some of the most common ways publishers get manipulated/transformed.
dropFirst
Calling dropFirst()
on a Publisher returns a new Publisher that ignores the first value it receives.
eraseToAnyPublisher
If you’re passing Publisher
s around, you may need to satisfy the compiler by calling eraseToAnyPublisher()
on it. This method returns a new Publisher
with all the subtype information erased (just a simple AnyPublisher
).
merge(with:)
This method accepts another Publisher
and returns a new publisher that publishes the values of both Publisher
s as they publish them. So for instance:
let clockSecondsPublisher: AnyPublisher<Int, Never> = ... let namePublisher = app.$name.eraseToAnyPublisher() let merged = namePublisher.merge(with: clockSecondsPublisher) .eraseToAnyPublisher()
the merged
would emit the Int
representing the seconds as they ticked by AND the name whenever it was changed.
combineLatest
This method accepts another Publisher
and returns a new publisher that publishes the values of both Publisher
s as a tuple, after it has received one value from each of them. So for instance:
let clockSecondsPublisher: AnyPublisher<Int, Never> = ... let namePublisher = app.$name.eraseToAnyPublisher() let combined = namePublisher.combineLatest(with: clockSecondsPublisher) .eraseToAnyPublisher()
the combined
would emit the (String?, Int)
once at least one second had passed (recall that app.$name
will immediately provide a value).
map
This method accepts a function from the Publisher
‘s value type to some other type and it returns a new Publisher
that emits values of the other type. Here’s an example:
let clockSecondsPublisher: AnyPublisher<Int, Never> = ... let evenOdd: (Int) -> String = { $0 % 2 == 0 ? "even" : "odd" } let evenOddPub = clockSecondsPublisher.map(evenOdd)
So, evenOddPub
will emit either “even” or “odd” depending on the second.
compactMap
This method is the same as map
except that it ignores nil
s when they are returned by the input function.
filter
This method allows you to choose what values get published, based on a function you provide which accepts a value and returns a Bool
(true
will pass it through for publishing, false
will keep it from publishing). For instance, if we wanted a publisher that only published even clock seconds, it might look like:
let clockSecondsPublisher: AnyPublisher<Int, Never> = ... let evenPublisher = clockSecondsPublisher.filter { $0 % 2 == 0 }
Receiving Values
To receive a value, call sink
on a Publisher
and pass it a function that accepts the Publisher
‘s value type (see example below). The sink
method returns something called a Cancellable
which you can use to cancel your subscription (just call cancel()
on it). Be warned: the subscription will only last as long as your cancellable is in scope! For this reason, I like to create a Set
of AnyCancellable
s that will stick around as long as I need the subscription for.
Putting it all together:
... let evenOddPub = clockSecondsPublisher.map(evenOdd) let cancelSet = Set<AnyCancellable>() let cancellable = evenOddPub.sink { print($0) } cancelSet.insert(cancellable)
Also note that you can call store
on a Cancellable
and pass it a reference to a Set
in order to save it. So the above code could equally look like:
... let evenOddPub = clockSecondsPublisher.map(evenOdd) let cancelSet = Set<AnyCancellable>() evenOddPub.sink { print($0) }.store(in: &cancelSet)
which is a bit more succinct.