Summary of WWDC 2019: Combine APIs for iOS
Combine was revealed in WWDC 2019 and is a “Unified, declarative API for processing values over time”.
- Focus on using Generics in order to reduce amount of boilerplate code.
- Write common generic algorithms about asynchronous behaviors only once but they will apply to many types of asynchronous operations.
- Core properties of Combine: Generic, Type safe, Composition first, Request driven.
- Consists of: Publishers, Subscribers and Operators.
“Instead of a few operators that do a lot, we provide a lot of operators that do a little, making them easier to understand” — Tony Parker, Foundation Team
The WWDC2019 keynote: https://developer.apple.com/videos/play/wwdc2019/722/
Combine requires targeting for at-least iOS13 and isn’t available on 12 runtime and below.
Publishers are a “declarative type of combine’s API” which describes how values and errors are produced. They don’t produce the values/errors themselves. They allow for a registration of a subscriber.
A publisher should be a value type (struct), with the following protocol:
- If an error is never produced, then a type of Never can be used for the Error associatedtype.
Receive values and then mutate state based on values received. They are reference types (class).
- A subscription is how a subscriber controls the flow of data from a publisher to a subscriber.
- Completion is only used if our publisher is “finite” and returns a completion or a failure.
Example subscriber: “Assign”:
- This example receives input, and writes that input out to a property on an object using a type safe keypath.
- Never is used for the error type because there’s no way in swift to handle an error when writing a property value.
Interaction between Publisher and Subscriber
- Your controller object holds your Subscriber and wires-up (attaches) that subscriber to the publisher.
- Publisher sends a subscription to the subscriber,
- Subscription is used by the subscriber to make a request to the publisher (for a certain number of values, or unlimited values).
- The publisher is now free to send that number of values (or less) to the subscriber.
- Then, if the publisher is finite, it will send a completion or an error.
Here’s an example of what publisher-subscriber relationship could look like when we are trying to notify that a student’s grade updated. This example, however won’t compile because of the type mismatch between Publisher.Output and Int. An operator object is necessary to make the type mapping.
The operator in this case will convert from a NotificationCenter notification type to an Integer:
Operators are publishers
Operators are also declarative (value types — structs).
They describe a behavior for changing values, adding values, removing values, or any other type of value operation behavior.
Operators subscribe to another publisher called “Upstream” and send their result to the subscriber (the “Downstream).
Map is a very common type of Operator that converts from a provided upstream type to its own type. It uses the same failure type of its upstream object. (does not generate its own failures)
Here’s the full syntax with .Map() plugged in as a converter object that takes the value from the upstream notification and converts it to an Int.
All this wiring syntax is verbose, and instead we can add an extension on Publisher with a generic method .map() instead of creating a converter object.
Usage of this generic .map<T> method:
Declarative Operator API
This .map<T> operator we just covered is part of this new API of functional operators. Some others include:
- list operations,
- take first, take second, take Nth element of a Publisher,
- error handling (turning an error into a default or placeholder value),
- thread or queue movement (ex: moving heavy work to the background, or moving UI work to the foreground),
- scheduling/time (integration with run loop, dispatch queue, timers, timeouts, etc).
A lot of these operator names are the same as collection api names.
The core design principle in Combine is Composition.
A Future — represents a single value at a later point in time.
A Publisher — represents an array of values at a later point in time.
If you already use a functional programming method on synchronous values, such as .map, .reduce, and .filter, try using the same on a Publisher as well, and most likely the name of the operation will be the same.
For example, you can use .compactMap instead of the .map example above to prevent nil values from progressing downstream to the subscriber.
If we wanted, for example to make it so that only students of grade 5 or higher are allowed in the school, and can not graduate more than 3 times, we can apply .filter(≥5) and .prefix(3) operators, like this:
After receiving 3 graduations, cancellations will be sent to the upstream, and completions will be sent to the downstream. So we can build conditional business logic as individual criteria on future changed values.
Zip operator (when/and)
The Zip operator is used to wait for any number of simultaneous async operations to finish and produce a result for the Subscriber only when all tasks are done.
Here, Zip3 method is used, which takes 3 upstreams and then 3 boolean results are converted a single boolean using .map:
Combine Latest operator (when/or)
- Converts several upstreams to a single value
- Can proceed when ANY of it’s inputs provide a value
- Stores last value of each upstream.
- If any upstream changes, you get new events posted to your subscriber.
Ideas for using Combine in your apps
- If you currently respond to NotificationCenter events to check whether to perform some task based on new state in the notification, try using .filter() instead.
- If you use dispatch group to wait for several async operations to finish, you can use .zip instead.
- You can use the .decode() operator that can be used to convert a JSON response from URLResponse.