Bindable values in Swift
Arguably one of the most challenging aspects of building apps for most platforms is making sure that the UI we present to the user always remains in sync with our underlying data models and their associated logic. It’s so common to encounter bugs that causes stale data to be rendered, or errors that happen because of conflicts between the UI state and the rest of the app’s logic.
It’s therefore not surprising that so many different patterns and techniques have been invented in order to make it easier to ensure that a UI stays up to date whenever its underlying model changes — everything from notifications, to delegates, to observables. This week, let’s take a look at one such technique — that involves binding our model values to our UI.
Constant updates
One common way to ensure that our UI is always rendering the latest available data is to simply reload the underlying model whenever the UI is about to be presented (or re-presented) on the screen. For example, if we’re building a profile screen for some form of social networking app, we might reload the User
that the profile is for every time viewWillAppear
is called on our ProfileViewController
:
class ProfileViewController: UIViewController {
private let userLoader: UserLoader
private lazy var nameLabel = UILabel()
private lazy var headerView = HeaderView()
private lazy var followersLabel = UILabel()
init(userLoader: UserLoader) {
self.userLoader = userLoader
super.init(nibName: nil, bundle: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Here we always reload the logged in user every time
// our view controller is about to appear on the screen.
userLoader.load { [weak self] user in
self?.nameLabel.text = user.name
self?.headerView.backgroundColor = user.colors.primary
self?.followersLabel.text = String(user.followersCount)
}
}
}
There’s nothing really wrong with the above approach, but there’s a few things that could potentially be improved:
- We always have to keep references to our various views as properties on our view controller, since we’re not able to assign our UI properties until we’ve loaded the view controller’s model.
- When using a closure-based API to get access to the loaded model, we have to weakly reference
self
(or explicitly capture each view) in order to avoid retain cycles. - Each time our view controller is presented on screen, we’ll reload the model, even if only seconds have passed since we last did so, and even if another view controller also reloads the same model at the same time — potentially resulting in wasted, or at least unnecessary, network calls.
One way to address some of the above points is to use a different kind of abstraction to give our view controller access to its model. Like we took a look at in “Handling mutable models in Swift”, instead of having the view controller itself load its model, we could use something like a UserHolder
to pass in an observable wrapper around our core User
model.
By doing that we could encapsulate our reloading logic, and do all required updates in a single place, away from our view controllers — resulting in a simplified ProfileViewController
implementation:
class ProfileViewController: UIViewController {
private let userHolder: UserHolder
private lazy var nameLabel = UILabel()
private lazy var headerView = HeaderView()
private lazy var followersLabel = UILabel()
init(userHolder: UserHolder) {
self.userHolder = userHolder
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
// Our view controller now only has to define how it'll
// *react* to a model change, rather than initiating it.
userHolder.addObserver(self) { vc, user in
vc.nameLabel.text = user.name
vc.headerView.backgroundColor = user.colors.primary
vc.followersLabel.text = String(user.followersCount)
}
}
}
To learn more about the above version of the observer pattern — check out “Handling mutable models in Swift”, or the two-part article “Observers in Swift”.
While the above is a nice improvement over our original implementation, let’s see if we can take things further — especially when it comes to the API that we expose to our view controllers — by instead directly binding our model values to our UI.
From observable to bindable
Instead of requiring each view controller to observe its model and to define explicit rules as to how each update should be handled, the idea behind value binding is to enable us to write auto-updating UI code by simply associating each piece of model data with a UI property, in a much more declarative fashion.
To make that happen, we’re first going to replace our UserHolder
type from before with a generic Bindable
type. This new type will enable any value to be bound to any UI property, without requiring specific abstractions to be built for each model. Let’s start by declaring Bindable
and defining properties to keep track of all of its observations, and to enable it to cache the latest value that passed through it, like this:
class Bindable<Value> {
private var observations = [(Value) -> Bool]()
private var lastValue: Value?
init(_ value: Value? = nil) {
lastValue = value
}
}
Next, let’s enable Bindable
to be observed, just like UserHolder
before it — but with the key difference that we’ll keep the observation method private:
private extension Bindable {
func addObservation<O: AnyObject>(
for object: O,
handler: @escaping (O, Value) -> Void
) {
// If we already have a value available, we'll give the
// handler access to it directly.
lastValue.map { handler(object, $0) }
// Each observation closure returns a Bool that indicates
// whether the observation should still be kept alive,
// based on whether the observing object is still retained.
observations.append { [weak object] value in
guard let object = object else {
return false
}
handler(object, value)
return true
}
}
}
Note that we’re not making our observation handling code thread-safe at this point — since it’ll mainly be used within the UI layer — but for tips on how to do that, check out “Avoiding race conditions in Swift”.
Finally, we need a way to update a Bindable
instance whenever a new model became available. For that we’ll add an update
method that updates the bindable’s lastValue
and calls each observation through filter
, in order to remove all observations that have become outdated:
extension Bindable {
func update(with value: Value) {
lastValue = value
observations = observations.filter { $0(value) }
}
}
It may be argued that using filter
to apply side-effects (like we do above) isn’t theoretically correct, at least not from a strict functional programming perspective, but in our case it does exactly what we’re looking for — and since we’re not reliant on the order of operations, using filter
is quite a good match, and saves us from essentially writing the exact same code ourselves.
With the above in place, we can now start using our new Bindable
type. We’ll start by injecting a Bindable<User>
instance into our ProfileViewController
, and rather than setting up each of our views using properties on our view controller, we’ll instead do all of their individual setup in dedicated methods that we call within viewDidLoad
:
class ProfileViewController: UIViewController {
private let user: Bindable<User>
init(user: Bindable<User>) {
self.user = user
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
addNameLabel()
addHeaderView()
addFollowersLabel()
}
}
Our view controller is already starting to look much simpler, and we’re now free to structure our view setup code however we please — since, through value binding, our UI updates no longer have to be defined within the same method.
Binding values
So far we’ve defined all of the underlying infrastructure that we’ll need in order to actually start binding values to our UI — but to do that, we need an API to call. The reason we kept addObservation
private before, is that we’ll instead expose a KeyPath
-based API that we’ll be able to use to directly associate each model property with its corresponding UI property.
Like we took a look at in “The power of key paths in Swift”, key paths can enable us to construct some really nice APIs that give us dynamic access to an object’s properties, without having to use closures. Let’s start by extending Bindable
with an API that’ll let us bind a key path from a model to a key path of a view:
extension Bindable {
func bind<O: AnyObject, T>(
_ sourceKeyPath: KeyPath<Value, T>,
to object: O,
_ objectKeyPath: ReferenceWritableKeyPath<O, T>
) {
addObservation(for: object) { object, observed in
let value = observed[keyPath: sourceKeyPath]
object[keyPath: objectKeyPath] = value
}
}
}
Since we’ll sometimes want to bind values to an optional property (such as text
on UILabel
), we’ll also need an additional bind
overload that accepts an objectKeyPath
for an optional of T
:
extension Bindable {
func bind<O: AnyObject, T>(
_ sourceKeyPath: KeyPath<Value, T>,
to object: O,
// This line is the only change compared to the previous
// code sample, since the key path we're binding *to*
// might contain an optional.
_ objectKeyPath: ReferenceWritableKeyPath<O, T?>
) {
addObservation(for: object) { object, observed in
let value = observed[keyPath: sourceKeyPath]
object[keyPath: objectKeyPath] = value
}
}
}
With the above in place, we can now start binding model values to our UI, such as directly associating our user’s name
with the text
property of the UILabel
that’ll render it:
private extension ProfileViewController {
func addNameLabel() {
let label = UILabel()
user.bind(\.name, to: label, \.text)
view.addSubview(label)
}
}
Pretty cool! And perhaps even cooler is that, since we based our binding API on key paths, we get support for nested properties completely for free. For example, we can now easily bind the nested colors.primary
property to our header view’s backgroundColor
:
private extension ProfileViewController {
func addHeaderView() {
let header = HeaderView()
user.bind(\.colors.primary, to: header, \.backgroundColor)
view.addSubview(header)
}
}
The beauty of the above approach is that we’ll be able to get a much stronger guarantee that our UI will always render an up-to-date version of our model, without requiring our view controllers to really do any additional work. By replacing closures with key paths, we’ve also achieved both a more declarative API, and also removed the risk of introducing retain cycles if we’d ever forget to capture a view controller as a weak reference when setting up model observations.
Transforms
So far, all of our model properties have been of the same type as their UI counterparts, but that’s not always the case. For example, in our earlier implementation we had to convert the user’s followersCount
property to a string, in order to be able to render it using a UILabel
— so how can we achieve the same thing with our new value binding approach?
One way to do just that would be to introduce yet another bind
overload that adds a transform
parameter, containing a function that converts a value of T
into the required result type R
— and to then use that function within our observation to perform the conversion, like this:
extension Bindable {
func bind<O: AnyObject, T, R>(
_ sourceKeyPath: KeyPath<Value, T>,
to object: O,
_ objectKeyPath: ReferenceWritableKeyPath<O, R?>,
transform: @escaping (T) -> R?
) {
addObservation(for: object) { object, observed in
let value = observed[keyPath: sourceKeyPath]
let transformed = transform(value)
object[keyPath: objectKeyPath] = transformed
}
}
}
Using the above transformation API, we can now easily bind our followersCount
property to a UILabel
, by passing String.init
as the transform:
private extension ProfileViewController {
func addFollowersLabel() {
let label = UILabel()
user.bind(\.followersCount, to: label, \.text, transform: String.init)
view.addSubview(label)
}
}
Another approach would’ve been to introduce a more specialized version of bind
that directly converts between Int
and String
properties, or to base it on the CustomStringConvertible
protocol (which Int
and many other types conform to) — but with the above approach we have the flexibility to transform any value in any way we see fit.
Automatic updates
While our new Bindable
type enables quite elegant UI code using key paths, the main purpose of introducing it was to make sure that our UI stays up-to-date automatically whenever an underlying model was changed, so let’s also take a look at the other side — how a model update will actually be triggered.
Here our core User
model is managed by a model controller, which syncs the model with our server every time the app becomes active — and then calls update
on its Bindable<User>
to propagate any model changes throughout the app’s UI:
class UserModelController {
let user: Bindable<User>
private let syncService: SyncService<User>
init(user: User, syncService: SyncService<User>) {
self.user = Bindable(user)
self.syncService = syncService
}
func applicationDidBecomeActive() {
syncService.sync(then: user.update)
}
}
What’s really nice about the above is that our UserModelController
can be completely unaware of the consumers of its user data, and vice versa — since our Bindable
acts as a layer of abstraction for both sides, which enables both a higher degree of testability, and also makes for a more decoupled system overall.
Conclusion
By binding our model values directly to our UI, we can both end up with simpler UI configuration code that eliminates common mistakes (such as accidentally strongly capturing view objects in observation closures), and also ensures that all UI values will be kept up-to-date as their underlying model changes. By introducing an abstraction such as Bindable
, we can also more clearly separate our UI code from our core model logic.
The ideas presented in this article are strongly influenced by Functional Reactive Programming, and while more complete FRP implementations (such as RxSwift) take the idea of value binding much further (for example by introducing two-way binding, and enabling the construction of observable value streams) — if all we need is simple unidirectional binding, then something like a Bindable
type may do everything that we need, using a much thinner abstraction.
We’ll definitely return to both the topic of Functional Reactive Programming, and declarative UI coding styles, in future articles. Until then, what do you think? Have you implemented something similar to Bindable
before, or is it something you’ll try out? Let me know — along with your questions, comments and feedback — on Twitter or by contacting me.
Thanks for reading! 🚀