Custom binding operator in Combine
For the last couple of years I have been using ReactiveSwift
to use reactive paradigms in my apps. And I couldn't be happier when Apple announced Combine
, the new shiny reactive framework. Though when I jumped into it, it didn't feel like home right away. The two frameworks felt a bit different even though they both follow the same principles of reactive stream.
One of the missing pieces is the binding operator <~
which I think everyone in the ReactiveSwift community is fond of. So, I thought let's try to recreate it using Combine as it shouldn't be too hard. You can read more about the operator here though in nutshell (the simplified version) it allows a subscriber (which recieves values) to subscribe to a publisher (which emits values). One can picture the following relationship between a publisher and a subscriber using the custom operator as below,
subscriber <~ publisher
Cool, so looking at the above, we need three things i.e. a publisher, a subscriber and a way to bind them.
Below we create our custom operator which takes two parameters i.e. a publisher & a subscriber but we don't bind them yet,
Step 1
func <~ (publisher: Publisher, subscriber: Subscriber) {
// some way to bind `publisher` & `subscriber`
}
Now go ahead and run it (assuming you declared the operator and added precedencegroup), it won't compile 😓
Looks like we can't use Publisher
& Subscriber
as regular parameters, so let's use them as generic constraints instead as suggested by the compiler,
Step 2
func <~ <S: Subscriber, P: Publisher>
(subscriber: S, publisher: P)
{
// some way to bind `publisher` & `subscriber`
}
And voila it compiles now, feeling pretty good right? 😄
Cool so now let's implement the next part i.e. find a way to bind a publisher and subscriber. Looking at the apis which are part of Publisher
protocol, there's a function func subscribe<S: Subscriber>(_ subscriber: S)
which, as per the documentation, attaches the specified subscriber to a publisher which is exactly what we need. Let's go ahead and use it,
Step 3
func <~ <S: Subscriber, P: Publisher>
(subscriber: S, publisher: P)
{
publisher.subscribe(subscriber)
}
Though again it won't compile 😓
This time it says it can't figure out the generic requirement. Not to worry, let's checkout the declaration of the func subscribe(:)
.
extension Publisher {
/// Attaches the specified subscriber to this publisher.
public func subscribe<S>(_ subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
So looking at it, we need to fulfill a requirement i.e. Self.Failure == S.Failure, Self.Output == S.Input
. Let's break it down one by one and understand what it means.
The first constraint says Self.Failure == S.Failure
which means that we need to make sure that the failure type of the publisher should match the failure type of its subscriber. Similarly Self.Output == S.Input
means that we need to match the publisher's output type to its subscriber's input type which makes sense as well because we can only assign String
to a label's text property (.text
) and not a color. Cool so let's add the extra requirement we need,
Step 4
func <~ <S: Subscriber, P: Publisher>
(subscriber: S, publisher: P)
where S.Input == P.Output, S.Failure == P.Failure
{
publisher.subscribe(subscriber)
}