Introduction to Fisticuffs
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 ofObservable
as a read-write property.let name: Observable<String> = Observable("Johnny Appleseed") name.value = "John Smith"
Computed
- Read-only value computed from one or moreObservable
s. Additionally, sinceComputed
has knowledge of whichObservable
s it depends on, it automatically notifies its subscribers when any of its dependencies are changed. Think ofComputed
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 anupdateSubmitButtonState()
call if thesubmitButton.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.
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!