Summary of WWDC2019: Combine in Practice keynote
Combine is a Unified, Declarative API for processing values over time.
Value Publishers conform to the Publisher protocol:
Here’s an example of how the response of a network request is parsed and then decoded to a model. If decoding fails, an Error is returned downstream.
Every publisher describes how it can fail.
Operators are used to recover from errors.
Error handling in Combine is not convention based, but is more flexible in how you can handle errors at each step.
An error type of Never indicates that error handling needs to be done elsewhere in the stream and the current operation will not return an error.
.assertNoFailure() assumes that a failure will never occur with a given operation:
If .assertNoFailure() encounters any kind of error, the app will crash:
Some of the other error handling operators are:
.retry, .catch, mapError, setFailureType.
The .catch operator, upon encountering an error, will terminate the original publisher, and create a new connection to another publisher provided in the recovery block. It will then continue receiving values from this recovery publisher instead.
In the below example, .catch is used to handle exceptions and provide a default value using the Just() method which is built into Combine. This chain can no longer fail:
In the above example, once an error is encountered, the publisher will be terminated and will stop giving further signals to the subscriber. If we want to maintain the original publisher upon failure, the Just() publisher should be created and used to decode the data, making sure that either the placeholder value or successfully parsed value is returned back to the original chain.
To create a resulting publisher that outputs just the Magic Trick name, for example, use the .publisher() command on the end of the chain.
They describe when and where a particular event is delivered.
Some built-in examples are:
.delay — delay the delivery of an event to a future time.
.debounce — wait for a submitted value to settle for a specified period before passing down the chain.
.throttle — guarantees that events are delivered no faster than a specified rate.
.receive(on:) — guarantees that downstream events are received on a particular thread or queue.
.subscribe(on:) — sets the scheduler to receive values on a specific queue which also propagates downstream.
Summary of Publishers
- They are a recipe for an event stream
- Operators describe new publishers from existing ones
- Strongly typed values/errors over time.
- Can produce values synchronously or asynchronously (Just or Notification center for example)
- Can attach compatible subscribers.
The 3 methods are called according to these rules:
- In response to a subscribe call, a publisher will call receive(subscription:) exactly once.
- A publisher can then provide 0 or more values to a subscriber, after the subscriber requests them.
- A publisher can send at most a single completion indicating that the publisher has finished or a failure has arisen. Once that completion has arisen, no further values may be emitted.
In other words:
“A subscriber will receive a single subscription followed by zero or more values, possibly terminated by a single completion, indicating that a publish finished or failed”
Here are some built in subscriber types in Combine:
- Key path assignment
Here’s an example of the key path assignment subscriber:
The resulting value from the publisher we discussed above, is taken and assigned to any key path property of any object.
cancel() can be called to terminate the subscription at any point.
The AnyCancellable protocol automatically will call cancel on deinit, which reduces our burden of having to call .cancel() explicitly on our subscribers.
We can however call .cancel() ourselves to free up resources, as result of user action, or for any other reason to terminate a subscription.
A .sink operator will fire the completion block on every value change, and provides a cancellable handle. You can do anything you want in the value changed block to decorate behavior based on changed values.
Subjects are a hybrid between a Publisher and a Subscriber and they let you send values Imperatively (helpful with existing codebases).
A subject receives a single value, and broadcasts it to multiple subscribers.
There are two types of Subjects: Passthrough and Current Value
The Passthrough subject does not store a value inside, simply forwarding it.
The Current Value subject stores the last sent value, allowing new Subscribers to catch up based on that previous value.
In this example, a Subject subscribes to a publisher, also Subscribes to itself using .sink(), and also sends values imperatively, like the value “Please”. (It’s both a Publisher and a Subscriber)
You can inject a passthrough subject into your existing stream using .share():
SwiftUI Integration Subscriber
One of SwiftUI’s main characteristics is: you only need to define dependencies in your application, and the framework takes care of the rest. (Provide a publisher that describes when and how your data has changed). To do this your custom types should conform to the BindableObject protocol.
The type system of the language will enforce that you handle errors before you get to the publisher in your custom type.
Here’s an example of a model in which both data members use properties to signal .send() on the subject whenever they change. The Swift UI view then gets its Text from the BindableObject it holds.
A new body will be automatically generated, whenever you signal that your model has changed.
WWDC 2019 Apple talk: Combine in Practice: https://developer.apple.com/videos/play/wwdc2019/721/