Stoian Dan
Posted on January 18, 2024
Apple's Combine.framework
provides support for async and reactive programming, and while Swift's latest async/await keywords make it easy to write code that reads linear, but is asynchronous, that's not a 100% overlap with Combine
, hence, it's still relevant.
Combine
is not all about async programming, but about reactive programming, about streaming data and reacting to change in data.
Basically, reactive programming is about reacting to changes in data, or to events. This is done via streams; channels that unite our data during all time and update us with any possible changes as they come.
Therefore, talking the above concepts into consideration, let's see how the map to the Combine
API.
Combine defines:
-
Publisher
- aprotocol
to which one ca subscribe to receive values over time -
Subscriber
- a client of aPublisher
, that receives the values -
Subscription
the binding between a publisher and subscriber - Finally, there is the concept of an operator. Operators are just Publishers that act as intermediary between a upstream publisher and a downstream publishers or a terminal subscriber. They often transform/prepare data for the end subscriber. This allows for greater modularity as these operators can be reused, inserted or removed at will, thus creating an effective pipeline between the initial, upstream publisher and the end subscriber.
Let's say we want to create a Publisher
that publishes all values from 0
to n
, like a range, starting always from 0
and going until n
. We'll call it Until
.
A publisher doesn't necessarily directly create its own values. Publishers are often structs, i.e. value types, they can be created in place and aren't pass around. There is notable exception that is the Subject
publisher.
The Publisher
protocol is generic over two types, an Output
, the type of data which it will yield, and a Failure
, the type of error which it yields, should something go wrong:
protocol Publisher<Output, Failure>
Until
will always yield Int
values and there's no real good reason to encounter and error, so our Failure
type will be Never
.
Other than that the only required method a Publisher
needs implement is called receive
that receives a Subscriber
, acknowledging it by providing back a subscription.
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
This might look complicated, all it's really saying is that the method is generic every a subscriber type and that subscribes's expected input value type needs match our expect output type, so with the failure type, makes sense?
Let's implement this:
import Foundation
import Combine
struct Until: Publisher {
// we output integers
public typealias Output = Int
// we never throw errors
public typealias Failure = Never
// the end value at which stop
let endValue: Int
public init(_ endValue: Int) {
self.endValue = endValue
}
public func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Int == S.Input {
// TODO: implement it
}
}
Great! Now how do we create a subscription to give it back to our subscriber? I'll leave that for a next episode.
For now out publisher will be a bit violent. Normally, the subscriber receives values only after he initially demands them. After each value received, he can demand even more, or less, as he wishes.
For now, our publisher will generate his own values and send them to the subscriber, regardless if he wants or not, but in the next episode we will do this right:
public func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Int == S.Input {
// a dummy subscription that ignores demands
let subscription = Subscriptions.empty
subscriber.receive(subscription: subscription)
// send the subscriber the value immediately
// ignoring any of his possible demands
for val in 0...endValue {
let _ = subscriber.receive(val)
}
// tell our subscriber we are done
subscriber.receive(completion: .finished)
}
How do we use this? We will need a subscriber, lucky Apple made one called Sink
, it automatically demands an unlimited amount of data and has a callback where we can use the data provided by the publisher:
// store the subscription provided by sync,
// normally if we would not do this, it will cancel our request
var cancellables = Set<AnyCancellable>()
Until(10).sink(receiveValue: { val in
print("We received \(val) from publisher!")
}).store(in: &cancellables)
Posted on January 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.