DRIVE
Functional Reactive Form Validation in iOS with RxSwift
WHY DRIVE?
SwiftUI has been getting all the love since it was announced but I want to take some time to write about the productivity benefits of functional reactive programming using RxSwift when combined with the stability of UIKit.
Observables are an excellent data-binding mechanism when escaping target-action, delegate based MVC patterns but even after the steep learning curve remembering tedious boilerplate and dodging footguns can be time consuming and error prone. Furthermore type inference across API boundaries can result in frustrating fights with the Swift compiler.
An example from the RxSwift documentation is an effective demonstration of the implementation complexity faced when using Rx with UIKit properly.
Thankfully RxSwift provides us with some wrappers around common UI patterns that can help simplify implementations. RxSwift calls these wrappers traits and today we are going to focus on the Driver trait.
RxSwift Traits are simple structs that implement the builder pattern to return an observable sequence guaranteed to have certain properties. The Driver trait guarantees three properties that happen to be integral to correct UI implementations: events are observed on the main thread, the observable sequence can't error out, and side effects are shared so that each subscription will share the same computational resources.
UITEXTFIELD VALIDATION
Form validation that provides the user with immediate feedback if a text field meets predefined requirements is a common UX pattern and this example will show you how you can use the Driver trait for effective results with a very small amount of clean, testable code.
BRAKING ZONE
For the purpose of narrowing the focus of this article I won't be going over project setup, how to connect outlets in storyboards, the basics of RxSwift/RxCocoa etc. I will also be handwaving the networking, validation and network activity tracking utilities as those implementation details are outside the scope of this article. The working project source code is available here if you would like to take a closer look or test a working example on a simulator or device.
This project will use MVVM but there are a few ground rules to help enforce seperation of concerns:
- The ViewModel can never import UIKit or reference the ViewController.
- The ViewModel will never subscribe to an observable.
- The ViewController will never directly call any methods defined on the ViewModel.
First we define inputs to the ViewModel. The initializer for our new ViewModel class below takes a pair of Driver<String>
for the email and password and a Driver<Void>
for the sign in button tap. We are not going to explicitly store references to these sequences.
In the ViewController we initialize the ViewModel passing in the Observables from the IBOutlets. Note the call to .asDriver()
to build the Drivers from the ControlEvent observables.
Next we define output properties to store the reference to the result of the operator transformations we are going to perform on the input observables.
Above we are using Driver.combineLatest
to combine events from the UITextField Drivers and the sign in button tap. The purpose of this is to exploit a behavior of combine latest that the result observable will not emit an event until both source observables have at least one in order to prevent displaying validation errors before user interaction. Then we .flatMapLatest
the combined text and tap events passing the string into our validation service's appropriate validation method and return a Driver<Validation>
that is stored in the output properties defined above.
Close the loop by calling drive(onNext:)
on the output observables in the ViewController. Don't forget to put the results of the drive(onNext:)
calls in the bag
or to call bind()
in viewDidLoad()
.
THE GETAWAY
Add two more output properties to the ViewModel. The first is signingIn: Driver<Bool>
to manage the state of the current request so we can disable the sign in button when a request is in-flight. The second is the output Driver for the response from the network request to sign in.
The ViewModel snippet below is preparation to handle the sign in implementation. Compose the email and password text drivers (the inputs) with the validation result drivers (the outputs) in addition we create another Driver wrapped observable from a utility class borrowed from RxExample that provides the ability to track the activity of an observable so we can prevent the user from creating a duplicate request if one is already in-flight and the user mashes the sign in button.
Finally we hook everything together, there is a lot going on here so let's break it down line by line. First call withLatestFrom()
on the signIn: Driver<Void>
passing in the combined inputs and outputs composed above (validated
). Then flatMapLatest
the sign in button tap event with the observables composition, guard for successful validation and no requests in flight, then flatMap
the observable from the network sign in request, check the response status (the example code is oversimplified but in a real world application here is where you determine error messsages to display if for instance the response was 401 unauthorized) and return an Observable<LoginResponse>
, add the activity tracking and finally build the signedIn: Driver<LoginResponse>
output Driver.
Now in the ViewController we use drive(onNext:)
to drive the state of the isEnabled
property of the sign in button and drive signedIn
to handle the result of the sign in network call. Again taking caution to use [weak self]
and don't forget to dispose of the result of drive
in the bag.
The success state is where the application would presumably handle navigating elsewhere or dismissing the sign in screen if presented modally. In a real world application the response should wrap a more informative error message that can then be displayed to the user.
TEST DRIVE
What about unit testing the ViewModel you ask? Simple, since all the ViewModel knows is that it needs 3 Drivers we can provide those easily.
IN THE BAG
That's the post, you can find the completed working project here. Feel free to drop some feedback or questions on Twitter or you can go to the source of this blog post itself and create an issue or pull-request. Until next time.