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 //reactive
view.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 type BindingTarget<[Section]>.
  • makeBindingTarget { (dataProvider, newSections) in } –– It’s an in-built function inside ReactiveSwift framework which returns an instance of BindingTarget. 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 of sections 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 where observer is the instance of the class which is listening to the values emitted by this Signal (for simplicity let's not talk about lifetime).
  • base.onNext –– base is the instance of the custom class which we want to extend to have a reactive layer. In this case it's DataProvider, that's why we can access sections property which is nothing but a closure.
  • observer.send(value: sections) –– whenever someone calls base.sections(), sectionsSignal will emit a new value of sections by calling the method observer.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 🚀