Introduction to Fisticuffs

6 min read
iOSMVVM

Fisticuffs is a compact Swift framework for view-model bindings on iOS, inspired by KnockoutJS. It lets developers quickly set up responsive applications without needing to add intermediate view-updating logic.

Background

The Model-view-viewmodel (MVVM) pattern has gained a lot of traction in the iOS community over the past couple years, and for good reason. Separating out the display logic ("view model") from the actual display ("view") has great benefits for code organization and testability.

Fisticuffs simplifies the cycle of updating views based on view-model changes by extending UIKit components for data binding. By encapsulating presentation logic in view-models we can create testable view code. Adding a binding layer like Fisticuffs lets us quickly tie this presentation logic to a view without having to write a lot of wiring.

Overview

At the core of Fisticuffs are 2 classes:

  • Observable - Wraps a value, notifying any subscribers when its value changes. Think of Observable as a read-write property.

    let name: Observable<String> = Observable("Johnny Appleseed")
    name.value = "John Smith"
    
  • Computed - Read-only value computed from one or more Observables. Additionally, since Computed has knowledge of which Observables it depends on, it automatically notifies its subscribers when any of its dependencies are changed. Think of Computed as a computed property.

    let greeting: Computed<String> = Computed {
        return "Hello, \(name.value)"
    }
    print(greeting.value)  // "Hello, John Smith"
    

UI Binding

Fisticuffs extends many UIKit classes to support binding Observable & Computed's to their properties. Building on the previous snippets, we could do something like this:

let nameTextField: UITextField = ...
let greetingLabel: UILabel = ... 

nameTextField.b_text.bind(name)
greetingLabel.b_text.bind(greeting)

In this case, the value of greetingLabel.text will automatically update as the user enters text in the nameTextField.

Example

Traditionally, without a bindings framework, one might implement a login page like so:

class LoginViewController: UIViewController {
    @IBOutlet var usernameField: UITextField!
    @IBOutlet var passwordField: UITextField!
    @IBOutlet var submitButton: UIButton!

    private var isLoading = false

    override func viewDidLoad() {
        super.viewDidLoad()
        updateSubmitButtonState()
    }

    func updateSubmitButtonState() {
        submitButton.enabled = isLoading == false &&
            usernameField.text.isEmpty == false && 
            password.text.isEmpty == false
    }

    @IBAction usernameFieldEditingChanged(sender: AnyObject?) {
        updateSubmitButtonState()
    }

    @IBAction passwordFieldEditingChanged(sender: AnyObject?) {
        updateSubmitButtonState()
    }

    @IBAction submitButtonTouchUpInside(sender: AnyObject?) {
        let username = usernameField.text
        let password = passwordField.text
        // ... do login with `username` / `password` ...
        isLoading = true
        updateSubmitButtonState()
    }
}

While this approach works, there are a few pain points:

  • It's somewhat fragile. Any maintainer has to know when to call updateSubmitButtonState(), and it may be challenging to figure out where we are missing an updateSubmitButtonState() call if the submitButton.enabled property is not being updated.

  • The display and business logic are deeply intertwined, making it difficult to test.

Using an MVVM approach with Fisticuffs, we can solve both those problems:

class LoginViewModel {
    let username: Observable<String> = Observable("")
    let password: Observable<String> = Observable("")

    let loading: Observable<Bool> = Observable(false)

    lazy var canSubmit: Computed<Bool> = Computed { [username = self.username, password = self.password, loading = self.loading] in
        return username.value.isEmpty == false && 
            password.value.isEmpty == false && 
            loading.value == false
    }

    func doLogin() {
        loading.value = true
        // ... do login here with `self.username` & `self.password`
    }
}

class LoginViewController: UIViewController {
    @IBOutlet var usernameField: UITextField!
    @IBOutlet var passwordField: UITextField!
    @IBOutlet var submitButton: UIButton!

    let viewModel = LoginViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Bind our text fields to the corresponding properties on our view model
        usernameField.b_text.bind(viewModel.username)
        passwordField.b_text.bind(viewModel.password)

        // Bind the `enabled` state of the title button to the `canSubmit` field
        submitButton.b_enabled.bind(viewModel.canSubmit)

        // Call the `doLogin` function when the `submitButton` is tapped
        submitButton.b_onTap.subscribe(viewModel.doLogin)
    }
}

We set up a view model (LoginViewModel) that encapsulates all our business logic. Since it has no dependencies on UIKit, it makes it trivial to write tests.

Additionally, because Fisticuffs automatically handles tracking dependencies, it'll automatically update the submit button's enabled property any time any of the dependencies changes. This frees the initial developer and any maintainers from tracking those updates manually.

In action

Summary

Fisticuffs is a great little tool for quickly binding view-models to views. We hope you find it useful, and please check out the project on GitHub!