This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

Lisa from Kickstarter shows us their test-driven development process to reactive programming.

00:06 We're joined by Lisa from Kickstarter today. Together we're going to use test-driven reactive programming to implement the logic of a signup form. A view model will hold all of the logic.

Setting Up

00:31 We'll first take a look at the plain view controller we've prepared. It has four UI components: three text fields and a submit button. We've added a target to each component inside viewDidLoad. Later in this episode, we'll implement the methods called by the components:

class MyViewController: UIViewController {
    let emailTextField = UITextField()
    let nameTextField = UITextField()
    let passwordTextField = UITextField()
    let submitButton = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.emailTextField.addTarget(self,
                                      action: #selector(emailChanged),
                                      for: .editingChanged)
        self.nameTextField.addTarget(self,
                                     action: #selector(nameChanged),
                                     for: .editingChanged)
        self.passwordTextField.addTarget(self,
                                         action: #selector(passwordChanged),
                                         for: .editingChanged)
        self.submitButton.addTarget(self,
                                    action: #selector(submitButtonPressed),
                                    for: .touchUpInside)
    }
    
    func submitButtonPressed() {
    }
    
    func emailChanged() {
    }
    
    func nameChanged() {
    }
    
    func passwordChanged() {
    }
    
    // ...
}

01:03 The view model we use conforms to three protocols. Kickstarter organizes its code with view models that have protocols for their inputs and their outputs and a ViewModelType that provides the interface to the inputs and outputs:

protocol MyViewModelInputs {
}

protocol MyViewModelOutputs {
}

protocol MyViewModelType {
    var inputs: MyViewModelInputs { get }
    var outputs: MyViewModelOutputs { get }
}

class MyViewModel: MyViewModelType, MyViewModelInputs, MyViewModelOutputs {
    
    init() {
    }
    
    var inputs: MyViewModelInputs { return self }
    var outputs: MyViewModelOutputs { return self }
}

01:30 When we first looked at Kickstarter's actual codebase, this pattern seemed overly complicated, but soon we saw the same architecture can be used everywhere. Once you're familiar with this separation, the code becomes very easy to read and everything starts to make sense.

Defining Inputs

01:48 Before we start implementing the logic, we need to define the rules the submit form should follow:

  1. The submit button is only enabled when all of the form fields are present — i.e. they contain text.

  2. The signup is successful if the email address is valid.

  3. As a security measure, the submit button is disabled forever after three unsuccessful signup attempts.

02:56 We have four UI components that accept input, so it makes sense to base the view model's inputs on these components. Text fields can be empty, so we use optional strings for the input values of the three text field components. There's also a method for when the submit button is pressed. We define a fifth input that describes the initial state of the view, which is called from viewDidLoad — that way, the view model ties in with the view controller life cycle:

protocol MyViewModelInputs {
    func nameChanged(name: String?)
    func emailChanged(email: String?)
    func passwordChanged(password: String?)
    func submitButtonPressed()
    func viewDidLoad()
}

04:13 Now we have to implement these methods in the view model class.

04:27 The Kickstarter team uses MutableProperty to bridge between the imperative and the functional world. This allows the members to set a property's value through a method from the view controller and to use the value reactively as a signal. MutableProperty is generic over its underlying type and the initializer takes an initial value. We follow Kickstarter's naming convention:

let nameChangedProperty = MutableProperty<String?>(nil)

In the view model's nameChanged method, we pass the new value to the above property. The other three text fields use the same pattern:

class MyViewModel: MyViewModelType, MyViewModelInputs, MyViewModelOutputs {
    
    // ...
    
    let nameChangedProperty = MutableProperty<String?>(nil)
    func nameChanged(name: String?) {
        self.nameChangedProperty.value = name
    }
    
    let emailChangedProperty = MutableProperty<String?>(nil)
    func emailChanged(email: String?) {
        self.emailChangedProperty.value = email
    }
    
    let passwordChangedProperty = MutableProperty<String?>(nil)
    func passwordChanged(password: String?) {
        self.passwordChangedProperty.value = password
    }
}

05:34 The other two inputs — submitButtonPressed and viewDidLoad — don't have a value to store. We still "ping" the property by passing in (), a void value:

class MyViewModel: MyViewModelType, MyViewModelInputs, MyViewModelOutputs {
    // ...
    
    let submitButtonPressedProperty = MutableProperty()
    func submitButtonPressed() {
        self.submitButtonPressedProperty.value = ()
    }
    
    let viewDidLoadProperty = MutableProperty()
    func viewDidLoad() {
        self.viewDidLoadProperty.value = ()
    }
}

Defining Outputs

05:58 The next step is to think of our app's outputs, such as displaying an alert message to the user upon submitting the form. Another output is whether or not the submit button is enabled.

06:48 In the outputs protocol, we specify two signals, which are generic over both their value's type and an error type, NoError:

protocol MyViewModelOutputs {
    var alertMessage: Signal<String, NoError> { get }
    var submitButtonEnabled: Signal<Bool, NoError> { get }
}

07:38 We then implement these signals in the view model, declared with let, because they can be immutable. We'll properly define the signals later, but we can start by instantiating them as empty signals:

class MyViewModel: MyViewModelType, MyViewModelInputs, MyViewModelOutputs {
    
    init() {
        self.alertMessage = .empty
        self.submitButtonEnabled = .empty
    }
    
    // ...
    
    let alertMessage: Signal<String, NoError>
    let submitButtonEnabled: Signal<Bool, NoError>
}

08:45 We've defined the inputs and outputs. But before connecting the two with functionality, we'll write the tests.

Writing Tests

09:06 These tests will model the rules we defined for the interface's behavior. We'll cover all edge cases with our tests and then work on our implementation until all tests succeed.

09:36 In our test suite, we create a TestObserver for each output — this is a class, written by Kickstarter, that gives access to the history of a property's values:

class ViewModelTests: XCTestCase {
    let vm: MyViewModelType = MyViewModel()
    let alertMessage = TestObserver<String, NoError>()
    let submitButtonEnabled = TestObserver<Bool, NoError>()
}

10:53 We hook up the view model outputs to the corresponding observers:

class ViewModelTests: XCTestCase {
    let vm: MyViewModelType = MyViewModel()
    let alertMessage = TestObserver<String, NoError>()
    let submitButtonEnabled = TestObserver<Bool, NoError>()
    
    override func setUp() {
        super.setUp()
        self.vm.outputs.alertMessage.observe(self.alertMessage.observer)
        self.vm.outputs.submitButtonEnabled.observe(self.submitButtonEnabled.observer)
    }
}

11:13 We're now ready to write some tests. We walk through possible scenarios and check the history of an output's values to see whether our rules are being followed. We start with testing the submitButtonEnabled state. In this test, we first call viewDidLoad, which sets up the view. Then we check that the submit button is disabled, following the rule that the form is only valid if all fields contain a value:

func testSubmitButtonEnabled() {
    self.vm.inputs.viewDidLoad()
    self.submitButtonEnabled.assertValues([false])
}

12:19 We assert the property's values as an array instead of a single value because we're checking the entire history of the property. As we go through the scenario, the history is collected by the observer.

13:15 Next, we simulate the user entering their name in the name text field. The submit button should still be disabled, because not all fields are valid yet:

func testSubmitButtonEnabled() {
    self.vm.inputs.viewDidLoad()
    self.submitButtonEnabled.assertValues([false])
    
    self.vm.inputs.nameChanged(name: "Chris")
    self.submitButtonEnabled.assertValues([false])
}

13:35 We then enter an email address and a password. After entering the password, submitButtonEnabled should go from false to true. We check the property's history, which should now have a second boolean, true:

func testSubmitButtonEnabled() {
    // ...
    
    self.vm.inputs.emailChanged(email: "chris@gmail.com")
    self.submitButtonEnabled.assertValues([false])
    
    self.vm.inputs.passwordChanged(password: "secret123")
    self.submitButtonEnabled.assertValues([false, true])
}

14:45 By testing the history of a property, we not only check that the value changed, but we also check all the changes the property went through. When we clear the name field, the submit button should go back to being disabled:

func testSubmitButtonEnabled() {
    // ...
    
    self.vm.inputs.nameChanged(name: "")
    self.submitButtonEnabled.assertValues([false, true, false])
}

15:42 We write a second test for a successful signup. After filling out the form and pressing the submit button, we check the alert message property:

func testSuccessfulSignup() {
    self.vm.inputs.viewDidLoad()
    self.vm.inputs.nameChanged(name: "Lisa")
    self.vm.inputs.emailChanged(email: "lisa@rules.com")
    self.vm.inputs.passwordChanged(password: "password123")
    self.vm.inputs.submitButtonPressed()
    
    self.alertMessage.assertValues(["Successful"])
}

17:02 We can copy this test and adjust it slightly in order to test how an invalid email address is handled:

func testUnsuccessfulSignup() {
    self.vm.inputs.viewDidLoad()
    self.vm.inputs.nameChanged(name: "Lisa")
    self.vm.inputs.emailChanged(email: "lisa@rules")
    self.vm.inputs.passwordChanged(password: "password123")
    self.vm.inputs.submitButtonPressed()
    
    self.alertMessage.assertValues(["Unsuccessful"])
}

17:24 We also test the last rule defined for our form: after three unsuccessful attempts, the form should disable itself. We enter an invalid email address and hit the submit button three times, after which we check the alert message's history:

func testTooManyAttempts() {
    self.vm.inputs.viewDidLoad()
    self.vm.inputs.nameChanged(name: "Lisa")
    self.vm.inputs.emailChanged(email: "lisa@rules")
    self.vm.inputs.passwordChanged(password: "password123")
    
    self.vm.inputs.submitButtonPressed()
    self.vm.inputs.submitButtonPressed()
    self.vm.inputs.submitButtonPressed()
    
    self.alertMessage.assertValues(["Unsuccessful", "Unsuccessful", "Too Many Attempts"])
    
}

18:27 We also check the submit button's enabled state in this scenario. The button should start out disabled when the view loads, become enabled when all the fields are filled out, and finally become disabled after the third submit attempt:

func testTooManyAttempts() {
    // ...
    self.submitButtonEnabled.assertValues([false, true, false])

}

18:43 To strongly test our security rule, we make sure the button stays disabled, even after we fix the invalid email address:

func testTooManyAttempts() {
    // ...
    
    self.vm.inputs.emailChanged(email: "lisa@rules.com")
    self.submitButtonEnabled.assertValues([false, true, false])
}

19:13 We're ready to write the logic and make our tests pass.

Implementing Logic with Signals

19:28 Back in the view model's initializer, we can make our outputs stay expressive and concise by creating some helper signals. For instance, we know our form data consists of three fields — name, email, and password — so we can combine these three signals into one signal:

class MyViewModel: MyViewModelType, MyViewModelInputs, MyViewModelOutputs {
    
    init() {
        let formData = Signal.combineLatest(
            self.emailChangedProperty.signal,
            self.nameChangedProperty.signal,
            self.passwordChangedProperty.signal
        )
        
        // ...
    }
    
    // ...
}

20:17 This formData is a new signal that combines the signals from our three properties. Therefore, the type of this signal is a tuple of three String?s, along with the NoError.

20:33 Using formData, we can create a signal for the successful signup message. We can sample the formData signal when the signal of submitButtonPressedProperty fires — in other words, each time the submit button is pressed, this new signal emits the latest value of the form data:

let successfulSignupMessage = formData
    .sample(on: self.submitButtonPressedProperty.signal)

21:14 Since the signal is meant for the successful signup message, we filter using a helper that checks if all form data is valid:

let successfulSignupMessage = formData
    .sample(on: self.submitButtonPressedProperty.signal)
    .filter(isValid(email:name:password:))

21:35 The filter line uses syntax called point-free style, which comes from functional programming. This allows us to compose functions without having to explicitly pass in the values — or points.

22:06 Finally, we have to map the boolean we receive to a message:

let successfulSignupMessage = formData
    .sample(on: self.submitButtonPressedProperty.signal)
    .filter(isValid(email:name:password:))
    .map { _ in "Successful" }

22:24 If we assign this signal to alertMessage, we can check that a first test, testSuccessfulSignup, passes.

22:46 It's easy to make testUnsuccessfulSignup pass as well. We create a signal for the unsuccessful signup by again sampling the formData helper signal, this time filtering invalid form data.

23:12 We take another in-between step and create a signal for when the submit button is pressed with invalid form data. From this we can derive both an unsuccessful message signal and a signal for too many signup attempts:

let submittedFormDataInvalid = formData
    .sample(on: self.submitButtonPressedProperty.signal)
    .filter { !isValid(email: $0, name: $1, password: $2) }

23:55 Because we're performing a negation of isValid in a closure, we unfortunately can't use the same point-free style, so we manually pass in the values from the form data tuple.

24:13 This new signal pings when the user enters invalid form data and submits it. We then map over the signal to create a signal for the unsuccessful message:

let unsuccessfulSignupMessage = submittedFormDataInvalid
    .map { _ in "Unsuccessful" }

24:47 To create the alertMessage signal, we merge two sources: the successful and unsuccessful message signals. We can use a helper method to create a signal that takes other signals of the same type and emits when any of those signals ping:

self.alertMessage = Signal.merge(
    successfulSignupMessage,
    unsuccessfulSignupMessage
)

25:27 The third alert message we have to write is the case where the user has attempted to submit the form three times with invalid data. Again, we take the submittedFormDataInvalid signal, but we ignore it the first two times:

let tooManyAttemptsMessage = submittedFormDataInvalid
    .skip(first: 2)
    .map { _ in "Too Many Attempts" }

26:25 This allows invalid form data to be submitted twice, but only on the third attempt does this signal emit.

26:35 We add this signal to the alert message:

self.alertMessage = Signal.merge(
    successfulSignupMessage,
    unsuccessfulSignupMessage,
    tooManyAttemptsMessage
)

26:43 It looks like we forgot something because our tests aren't passing yet. When inspecting the alert message history, we see that we receive an "Unsuccessful" value one time too many. This comes from the unsuccessful signup message signal, which should stop emitting after two times. We fix it by only taking the first two pings of the invalid form data signal:

let unsuccessfulSignupMessage = submittedFormDataInvalid
    .take(first: 2)
    .map { _ in "Unsuccessful" }

27:29 Now the test passes.

27:44 Just as we merged a few signals to create the alert message, we'll also use a few sources and combine them to define the output for the submit button's enabled state. We know that when the view loads, the button should start out disabled:

self.submitButtonEnabled = Signal.merge(
    self.viewDidLoadProperty.signal.map { _ in false }
    
}

28:40 We want to enable the button once the form fields are present (i.e. they have data). We use the isPresent helper, which checks the character count of the given inputs:

self.submitButtonEnabled = Signal.merge(
    self.viewDidLoadProperty.signal.map { _ in false },
    formData.map(isPresent(email:name:password:))

}

29:12 Finally, we want the submit button's enabled state to be false after too many attempts:

self.submitButtonEnabled = Signal.merge(
    self.viewDidLoadProperty.signal.map { _ in false },
    formData.map(isPresent(email:name:password:),
    tooManyAttemptsMessage.map { _ in false }
}

29:43 Only one test still isn't passing. After too many signup attempts, the submit button doesn't stay disabled forever. When we fix the invalid email address, the formData signal emits, causing submitButtonEnabled to emit again as well. We have to make submitButtonEnabled stop emitting after too many attempts:

self.submitButtonEnabled = Signal.merge(
    self.viewDidLoadProperty.signal.map { _ in false },
    formData.map(isPresent(email:name:password:)),
    tooManyAttemptsMessage.map { _ in false }
)
    .take(until: tooManyAttemptsMessage.map { _ in () } )

31:12 Once too many attempts occur, the submit button signal will stop emitting forever. All our tests are now passing!

Using the View Model in the View Controller

31:38 Judging from the tests we wrote, we already have a working app. But we're missing one step: we have to implement the view model in the view controller. To do this, we create an instance of our view model:

class MyViewController: UIViewController {
    let vm: MyViewModelType = MyViewModel()

    // ...
}

32:22 Now we can use the view model's inputs and outputs in the view controller's methods. The view model's interface is so concise that it's immediately obvious how we should use it. We start with the inputs:

override func viewDidLoad() {
    super.viewDidLoad()
    
    // ...
    
    self.vm.inputs.viewDidLoad()
}

func submitButtonPressed() {
    self.vm.inputs.submitButtonPressed()
}

func emailChanged() {
    self.vm.inputs.emailChanged(email: self.emailTextField.text)
}

func nameChanged() {
    self.vm.inputs.nameChanged(name: self.nameTextField.text)
}

func passwordChanged() {
    self.vm.inputs.passwordChanged(password: self.passwordTextField.text)
}

34:14 And we finish by using the view model's two outputs. We observe the output signals on a UIScheduler, which makes sure all work is performed on the main queue. With observeValues, we specify a closure that receives new values from the signal and uses them to update the interface:

override func viewDidLoad() {
    super.viewDidLoad()
    
    // ...
    
    self.vm.outputs.alertMessage
        .observe(on: UIScheduler())
        .observeValues { [weak self] message in
            let alert = UIAlertController(title: nil,
                                          message: message,
                                          preferredStyle: .alert)
            alert.addAction(.init(title: "OK", style: .default, handler: nil))
            self?.present(alert, animated: true, completion: nil)
    }

    self.vm.outputs.submitButtonEnabled
        .observe(on: UIScheduler())
        .observeValues { [weak self] enabled in self?.submitButton.isEnabled = enabled }
    
    // ...
}

Running the App

36:24 We can finally run our app and see how it works in an interactive Playground tab. We enter form data and get a successful signup. If we enter an invalid email address and submit, we get the unsuccessful message, and when repeatedly pressing the submit button, the form gets disabled and stays that way. Great!

37:50 It's impressive how we could write the whole test suite before writing the logic or even looking at or touching the interface.

38:17 Throughout Kickstarter's codebase, we've seen the same pattern of separating inputs and outputs, writing clear tests, and combining signals. Once you have a basic knowledge of how this works, the code becomes very easy to understand.

Resources

  • Playground

    Written in Swift 3.1

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes