Combine Framework Overview

Some time ago Apple has introduced a brand new framework called Combine (WWDC 2019). This framework is aimed to reduce developers’ need to implement multiple delegate callbacks and completion handlers and simplify events handling in general. Also, Combine has a declarative API which dramatically improves the readability of the code.
Just imaging that you have a search field and would like to filter a list of items according to the entered text. You can use a UITextView delegate for that, of course, you’ll have to use a timer in addition to throttle user’s input and you’ll definitely have to process the entered text somehow to make it suitable for your filtering logic.
Or you would like to make a network request to your server API, then process this request and transform received data into the model, after that you’d like to filter that data and change your UI accordingly.
Looks pretty complex, isn’t it? So here comes Combine which can drastically simplify all this stuff and allow you to create a single processing chain for the process.
Combine Components
Generally, Combine is simple and consists of 3 concepts:
- Publishers
- Subscribers
- Operators
Let’s discuss each one to understand their purpose.
Publishers
The publisher is a type that defines how values and errors are produced over time. The publisher must be structure and follow the Publisher protocol. Also, the publisher allows one or more subscribers to subscribe to the values it produces.
The publisher has two associated types, one for Output and another one for Failure, and method receive. Here is how Publisher protocol looks like:
public protocol Publisher {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
Receive method allows us to connect the publisher with one or more subscribers.
Subscribers
Subscriber is a class that is responsible for requesting end receiving data from the publisher, it can also receive an error. We can look at the subscriber as a final destination point of our data flow chain. Subscriber implements the Subscriber protocol which as Input and Failure associated types and 3 receive methods for different purposes. Here is how Subscriber protocol looks like:
public protocol Subscriber : CustomCombineIdentifierConvertible {
associatedtype Input
associatedtype Failure : Error
func receive(subscription: Subscription)
func receive(_ input: Self.Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Self.Failure>)
}
receive(subscription: Subscription) method will be invoked by the publisher right after a connection between the publisher and subscriber is established (it happens exactly after calling receive<S>(subscriber: S) method).
After that instance of Subscription can be used to request data from the publisher, also we can use this instance to cancel the subscription and exit the flow.
Next, when new values are ready the publisher calls the second method of subscriber receive(_ input: Self.Input) with new values.
When the publisher stops publishing it calls the last method receive(completion: Subscribers.Completion<Self.Failure>) with one of the two enum values represents either success .finished or error .failure(Failure).
We should keep in mind one important thing that Publisher and Subscriber must obey to be able to connect to each other:
The Output of the publisher must be of the same type as Input of the subscriber. Also, Failure of both should be the same type or Never if there can’t be any error in the flow.
Operators
The last important piece of the chain is Operators. Just imagine that publisher can emit numbers in form of a string, but the subscriber can accept only numbers in form of integers. So we have to convert the value somewhere in between. Here come operators in play.
Technically operators are convenience functions that allow receiving the data from the upstream publisher (or operator), transform it, and republish it down the stream to the next step in the chain.
The Data Flow in Combine
Every chain of data flow in Combine can be represented with the following scheme:
Publisher -> [Operator] -> [Operator] -> ... -> Subscriber(s)
This means that the value the publisher emits can be processed via zero or more operators and after that can be consumed by one or more subscribers.
Example
Here is the simple Counter class that uses PassthroughSubject and publisher to update its clients about count changes
import Combine
// simple counter class that publish it's value
final class Counter {
// create a PassthroughSubject to publish change of the counter
// there will be no error so we pass Never as Failure associated type
private var subject = PassthroughSubject<Int, Never>()
// it is important to make a separate computed property for PassthroughSubject
// to hide subject and expose only it's publisher
var publisher: AnyPublisher<Int, Never> {
subject.eraseToAnyPublisher()
}
private(set) var count: Int = 0 {
didSet {
// after changing count value send it via our subject
subject.send(count)
}
}
func increment() {
count += 1
}
func decrement() {
count -= 1
}
func setCount(with value: Int) {
count = value
}
}
And Counter usage will be the following
// create an instance of our Counter
var counter = Counter()
// subscribe to the publisher and store the subscription in cancellable var
var cancellable = counter.publisher.sink {
print($0)
}
counter.increment()
counter.increment()
counter.setCount(with: 10)
counter.decrement()
Try to figure out the result of this code! Actually, it’s the following:
1
2
10
9
It would be nice to understand one important thing about the invocation of subject.eraseToAnyPublisher() inside the computed property. Each time you invoke eraseToAnyPublisher() it creates a new publisher, but the subject itself still remembers all previous publishers that are created earlier.
Conclusion
Well, looks like Apple has created a really interesting thing called Combine. Reactive programming is very popular now and it definitely would be a good idea to adopt some of its concepts. Using Combine is one of them.