DRIVE II: COMBINE
"We ain't driving tractors here."
In our last episode, we implemented some practical iOS UI programming techniques using RxSwift's Driver trait and UIKit. This time we are going to explore some of the same patterns using Apple's reactive programming framework Combine. Implemented with discipline, Rx based architectures can help you avoid smelly anti-patterns like half-cooked spagehettiNot enough seperation of concerns. or over-cooked lasgnaToo much seperation of concerns., but as with all things it comes with trade-offs.
One of the biggest problems with Rx architectures is the steep learning curve of not only the reactive language extensions themselves but also learning to reason about the system as a declarative stream of events and data as opposed to imperative statefulness. Lucky for us then that Combine adds some much needed developer ergonomics to reactive extensions that makes things a lot easier when operating on publishers across API boundaries.
THE RETURN OF THE DRIVER
Find the source code for a project with working examples here.
If you recall, RxSwift's Driver Trait is a guarantee that the publisher is shared with the last element replayed upon subscription, the subscription is always received on the main thread, and the publisher can never error out. It is instructive to think about Drivers as UI or system events "driving the application".
One of the main driversI am so sorry. of the RxSwift Driver trait was to help make dealing with type inference across API boundaries less problematic. Combine's convenient eraseToAnyPublisher API makes this aspect of the driver trait redundant however it is instructive to walk through the implementation with Combine and the result leaves us with some easily re-usable Rx patterns.
One of the nice things about Combine compared to RxSwift is that the API is much simpler. The primary drawback of this simplicity is that we will need to import CombineExt to provide some operators we need for this architecture pattern to be successful. Nonetheless our code is going to look a lot cleaner thanks to Combine's integration with Foundation. Apple does not provide default publishers for UIKit control events so we will need to add CombineCocoa to our project as well.
Below are two extensions to Publisher to fulfill our requirements for a Driver. The first demonstrates how to use an Empty publisher to convert a Publisher error type to Never.
Lines 3-4: Some typealias conveniences.
Lines 8-9: Catch and return an Empty publisher to convert the error type to Never.
Line 11: Share and replay the last element.
Line 12: Receive on the main thread.
If it makes more sense to provide a default value to the driver function wrap the default value parameter in a Just publisher instead of returning an Empty publisher.
One more extension for our UITextfield publisher so we don't have to worry about unwrapping optionals anywhere along our publisher chain.
MVVM + VC
We can't accurately call this pattern MVVM becuase it's more like Model-View-ViewModel-ViewController. The ViewController is responsible for connecting the ViewModel to the Views as well as managing the navigation stack which is exactly what UIViewControllers are supposed to do!
Take a look at the ViewModel implementation below as now is a good time to review some rulesThe ViewModel cannot import UIKit and will never subscribe to any publishers. to help enforce seperation of concerns and prevent side effects.
Initialize the ViewModel with the IBOutlet drivers.
The ViewModel isn't too much different than last time except now it is significantly simpler. Define the outputs, connect the inputs and subscribe in the ViewController. Bish bash bosh, next thing you know, Robert's your father's brother.
One thing we wont be implementing here is the drive function itself which serves as a type erased replacement for subscribe in RxSwift. Combine's built-in type erasure has already made our API boundaries tidy and easy to work with.
Back in our ViewController we can start to connect the ViewModel outputs to the Views.
Since the ViewModel outputs are all Drivers we can rest assured that our subscriptions will be received on the main thread.
Now we can connect the login button enabled state and the request result outlets.
As always, don't forget to use weak self, put the subscriptions in the bag, and call your bind function in viewDidLoad.
IN THE BAG
Next time we'll explore async/await ;)
Acknowledgements
Styles by Tufte-CSS.
Syntax by PrismJS