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:

  1. 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.
  2. 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.
  3. 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! 🚀

String literals in Swift

Different flavors of type erasure in Swift