Reactive <~ Closure
This article is about how to adapt a closure based api to have a reactive layer on top of it, without modifying any underlying logic!
--
Motivation
In the last one year, I have played a lot with ReactiveSwift and since then I haven’t looked back to non-reactive style of programming. Reactive programming completely re-wires your brain and gradually it changes the approach of solving a problem. Since I like OSS a lot, these days I’m trying to adapt my existing swift libraries to have a reactive layer as well. Though one can argue that why not re-write them completely using reactive approach. Well adding just a top layer give user the freedom to choose between reactive & non-reactive versions of the same library. This is advantageous because we don’t want someone to not use our libraries just because they don’t want ReactiveCocoa since it’s a relatively bigger dependency (you can checkout Receiver if you are looking for a reactive µ-framework)
Closure based api
Let’s say we are using a framework, DataProvider
, which takes care of populating UITableView
& UICollectionView
. After removing the low level details of the class, it looks like something below,
class DataProvider {
...
var sections: ([Section] -> Void)
...
}
As you can see, we have a closure named sections
which we want to invoke or observe to make relevant updates in a table/collection. The objective here is to add a reactive layer along with this closure api. Since the framework, let’s say, is not maintained by us and we don't want to modify their underlying logic so let's try to figure out how to go about it.
ReactiveSwift
1. ReactiveExtensionsProvider
If you are familiar with ReactiveSwift, there’s a protocol ReactiveExtensionsProvider
which marks the foundation of adding a separate layer of reactive apis. This separate layer is provided by a property .reactive
. Below you can checkout the extension in more detail,
public struct Reactive<Base> { ... }extension ReactiveExtensionsProvider { public var reactive: Reactive<Self>
...
}
Since ReactiveCocoa (UIKit + ReactiveSwift) makes NSObject
conform to ReactiveExtensionsProvider
thus every subclass of NSObject
(which is practically the entire UIKit) gets a property –– .reactive
. Let's see some UIKit reactive & non-reactive counterparts,
...
view.alpha // non-reactive
view.reactive.alpha //reactiveview.isHidden
view.reactive.isHidden
...
Similarly if we want to add a reactive layer on top of the DataProvider
, we can simply conform DataProvider
to ReactiveExtensionsProvider
,
extension DataProvider: ReactiveExtensionsProvider {}
Since ReactiveExtensionsProvider
is an empty protocol so we don't need to provide anything. But what we get in return is .reactive
property on an instance of DataProvider
. Now that we have a separate layer for reactive apis, so let's see how to make a couple of such apis. Since they are reactive apis so it's not as straight forward as adding properties directly to DataProvider
. There are few steps involved which we will understand below.
2. BindingTarget
UIView
has properties like –– view.isHidden, view.alpha
etc. We already know that isHidden
is of type Bool
and alpha
is of type CGFloat
. Now if we checkout the types of their reative counterparts like view.reactive.isHidden
, we can find out that it's BindingTarget<Bool>
rather than simply Bool
. Similarly view.reactive.alpha
is of type BindingTarget<CGFloat>
. So if we want a property which we want to bind, we need an instance of BindingTarget
of that corresponding property. Let's try to add a corresponding reactive property for sections
in DataProvider
so that we can do something like dataprovider.reactive.sections
which will have type BindingTarget<[Section]>
,
extension Reactive where Base: DataProvider {
var sections: BindingTarget<[Section]> {
return makeBindingTarget { (dataProvider, newSections) in
dataProvider.sections(newSections)
}
}
}
Now let’s breakdown the extension so we can understand the inner bits,
extension Reactive where Base: DataProvider
––Base
is the custom class in which we want to add a reactive layer (DataProvider
in our case).var sections: BindingTarget<[Section]>
–– We have already discussed it above i.e. if we have a closure api dependent on[Section]
then we would want it’s reactive counterpart of typeBindingTarget<[Section]>
.makeBindingTarget { (dataProvider, newSections) in }
–– It’s an in-built function inside ReactiveSwift framework which returns an instance ofBindingTarget
. It has a closure based initialiser which takes two arguments as part of a tuple i.e. an instance of the Base class (DataProvider
) and the input ([Section]
).dataProvider.sections(newSections)
–– This is the internal underlying logic which we want to use when we get new values ofsections
so that we can trigger the table/collection to update.
Thus after making the above changes, the new reactive api looks something like below,
let dataProvider = DataProvider()// reactive
dataProvider.reactive.sections <~ ... // acts like a target// non reactive
dataProvider.sections(...)
3. BindingSource (Signal)
Just now we saw how we can reactively get the sections which allows us to bind it with other functions. Now what if we want to observe the sections
reactively as well. To achieve it, there's another protocol –– BindingSource
which simply means it can act like source of values which we can listen or bind forward. We already have a class Signal
comforming to BindingSource
, as part of ReactiveSwift framework, to make our job easier. Let's see some UIKit examples to understand the flow,
textField.reactive.continuousTextValues // Signal<String?>
button.reactive.controlEvents // Signal<UIControlEvents>
We can see a similar pattern here as well i.e. how to migrate a type to it’s reactive counterpart — we simply need a Signal
of that type. In our case it's array of section i.e. Signal<[Section]>
. Let's again extend Reactive
to add a new property called sectionsSignal
extension Reactive where Base: DataProvider {
var sectionsSignal: Signal<[Section]> {
return Signal { observer, _ in
base.sections = { sections in
observer.send(value: sections)
}
}
}
}
Let’s again deconstruct the extension without repeating the parts we have already discussed,
return Signal { observer, _ in }
––Signal
intializer gives us two arguments,observer
&lifetime
whereobserver
is the instance of the class which is listening to the values emitted by thisSignal
(for simplicity let's not talk aboutlifetime
).base.onNext
––base
is the instance of the custom class which we want to extend to have a reactive layer. In this case it'sDataProvider
, that's why we can accesssections
property which is nothing but a closure.observer.send(value: sections)
–– whenever someone callsbase.sections()
,sectionsSignal
will emit a new value of sections by calling the methodobserver.send()
.
Thus after making the above changes, the new reactive api looks something like below,
// acts like a source of values
... <~ dataProvider.reactive.sectionsSignal
Conclusion
Thus without modifying any underlying logic of DataProvider
or creating a new class, we are able to extend it to make it plugeable with other reactive apis. The protocols or classes used above are –– ReactiveExtensionsProvider
, BindingTarget
, Reactive
& Signal (BindingSource)
Let's the compare the final api,
extension DataProvider: ReactiveExtensionsProvider {}extension Reactive where Base: DataProvider {
var sections: BindingTarget<[Section]> {
return makeBindingTarget { (dataProvider, newSections) in
dataProvider.sections(newSections)
}
}
var sectionsSignal: Signal<[Section]> {
return Signal { observer, _ in
base.sections = { sections in
observer.send(value: sections)
}
}
}
}let dataProvider = DataProvider()// reactive
dataProvider.reactive.sections <~ ... // used as a target
... <~ dataProvider.reactive.sectionsSignal // used as a source// non-reactive
dataProvider.sections(...)
dataProvider.sections = { sections in ... }
There are few terms/concepts which I have deliberately left out so that it doesn’t mix with the objective of the article. Feel free to ask about them in the comments or add your thoughts on how to make it better! Thanks again for reading 🚀