Uncategorized

Quick Swift: Combine (Just the Basics)

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, Publishers. 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 Publishers, 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 Publishers 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 Publishers 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 Publishers 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 Publishers 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 nils 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 AnyCancellables 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s