It was really exciting to hear about Apples new framework called Combine at WWDC 2019. Finally, we have a native way to write functional reactive code and to build apps in a declarative way.
The main components of Combine are Publisher
, Subject
, Subscriber
and Operator
. Here is a brief summary of what they do:
CurrentValueSubject
- as the name indicates, this subject type has access to the current valuePassthroughSubject
- as the name indicates, this subject passes the current value through, i.e. it has no access to itIn the following figure, we can see these components in action. We will go through the example step by step and in more detail below.
Imagine, we have an app that presents articles to the user. The articles can receive likes. As a requirement, we want to be able to change the count of likes for every article and also to notify subscribers about this change.
As the first step, we use a combination of Combine's publisher and subject to achieve our goal.
struct Article {
// 3.
var likesCountPublisher: AnyPublisher<Int, Never> {
likesCountSubject.eraseToAnyPublisher()
}
// 1.
private let likesCountSubject: CurrentValueSubject<Int, Never>
// 2.
init(likesCount: Int) {
likesCountSubject = CurrentValueSubject(likesCount)
}
// 4.
func addLike() {
likesCountSubject.send(likesCountSubject.value + 1)
}
}
Let's go through this code step by step.
likesCountSubject
as a private variable of type CurrentValueSubject<Int, Never>
. We will use this subject to send new values to subscribers. Since the subject is a generic type we specify it's Output
and Failure
type. The output type defines what kind of values the subject will send, in our case Int
values. Since in cannot fail in our example, we use Never
as the error type.likesCount
parameter to give likesCountSubject
an initial value.likesCountPublisher
of type AnyPublisher<Int, Never>
. It has the same Output
and Failure
type as our subject. The publisher can be used by subscribers. We don't necessarily need the publisher here, we could simply make our subject public. But since we don't want anybody else outside of the article struct sending new values, we use a publisher to only make the subscribing part available to the outside world.addLike
method for increasing the count of likes. Here, we use our subject to send new values. To be able to add a like we access the subject's value
property to get the current value of likes. This is the reason why we used a CurrentValueSubject
and not a PassthroughSubject
in this case, since only a CurrentValueSubject
has access to it's current value.Now, we can use the Article
struct as following:
// 1.
let article = Article(likesCount: 5)
// 2.
let likesCountSubscriber = article.likesCountPublisher
.sink { value in
print(value)
}
// 3.
article.addLike()
article.addLike()
Let's go through this code step by step.
likesCountSubscriber
which is interested in any update on the likes count. The subscriber uses the publisher and it's sink(receiveValue:)
method to subscribe for updates. Now, every time the likes count changes, the closure with be called that prints the new value.addLike()
twice.Therefore, the code produces the following output:
5
6
7
When we create a new subscriber, the publisher always returns an object that conforms to the Cancellable
protocol. So if we wanted to cancel receiving new values, we could call the cancel()
method on the subscriber.
For example, if we add the cancel call in between increasing likes,
article.addLike()
likesCountSubscriber.cancel()
article.addLike()
the last value would not be printed.
Now, we can use operators to modify the received values before the printing closure is executed. For example, if we want to print something more descriptive, we can use the map
operator to map the Int
values to String
values:
let likesCountSubscriber = article.likesCountPublisher
.map { "\($0) likes" }
.sink { print($0) }
Now, our output looks likes this:
5 likes
6 likes
7 likes
Combine provides a lot of useful operators and we can chain as many operators as we want.
To dive deeper into the operators, let's look at the following example.
let publisher1 = PassthroughSubject<[Int], Never>()
let publisher2 = PassthroughSubject<[Int], Never>()
publisher1
.merge(with: publisher2) // 1.
.flatMap { Publishers.Sequence(sequence: $0) } // 2.
.filter { $0 % 2 == 0 } // 3.
.dropFirst() // 4.
.collect() // 5.
.map { $0.sorted() } // 6.
.sink { print($0) }
publisher1.send([1, 3])
publisher2.send([6, 10])
publisher2.send([4, 19, 8])
publisher1.send(completion: .finished)
publisher2.send(completion: .finished)
The example doesn't do something useful, but it hopefully gives a good understanding for the operators and combinations of them.
Try to figure out by yourself which output every operator produces before looking into the solution below.
[1, 3] [6, 10] [4, 19, 8, 6]
, because we combine elements from publisher1
with those from publisher2
with the merge
operator1, 3, 6, 10, 4, 19, 8
, because we flatten the Int
array into a sequence of Int
values with flatMap
6, 10, 4, 8
, because we filter out all uneven values with the filter
operator10, 4, 8
, because we drop the first element with the dropFirst
operator[10, 4, 8]
, because we collect all received items with the collect
operator which returns them as an array[4, 8, 10]
, because we map the array into a sorted array with the map
operatorOf course, there are a lot more operators to discover. The full list of operators is available at the official Publisher documentation.
The lifecycle of a subscriber is linked to the lifecycle of the retaining object. Whenever this object is released, the cancel method is automatically called on the subscriber property and it will be released as well.
Of course, just like with "traditional" memory management in Swift, you need to be aware of not creating retain cycles, e.g. when using strong selfs in the sink
closure.
Now that you know the Combine basics, my suggestion for the next step would be to look into property wrappers. Especially interesting in this case is the @Publish property wrapper that turns a variable into a Combine publisher.