Weekly Swift articles, podcasts and tips by John Sundell.

Maintaining model consistency in Swift

Published on 15 Dec 2019

When designing the model layer of any app or system, establishing a ”single source of truth” for each state and piece of data that we’re dealing with is often essential in order to make our logic behave predictably.

However, ensuring that each state is stored in just a single place can often be easier said than done — and it’s very common to end up with bugs and errors resulting from inconsistent model data, especially when such models are passed around and mutated in several different places.

While some of those errors are bound to happen outside of the models themselves, this week, let’s take a look at how we can improve the internal consistency within each of our models — and how doing so can let us establish a much stronger foundation for our codebase as a whole.

Paw

This ad keeps all of Swift by Sundell free for everyone. If you can, please check this sponsor out, as that directly helps support this site:

Paw

Paw: A GraphQL and REST API client that lets you test and describe the APIs that you call from your app. Just enter the URL of the API endpoint that you’re looking to call, add any headers, parameters, authentication, or body data. Hit return — and everything is automatically checked for you, from the standard OAuth 2 login to very custom API flows.

Deriving dependent states

The overall model layer of any given system can most often be described as a hierarchy, in which more high-level pieces of data depend on some form of underlying state. As a simple example, let’s say that we’re working on a contact management app, and that we have a Contact model which contains each person’s contact information — such as their name and email address:

struct Contact {
    let id: ID
    var firstName: String
    var lastName: String
    var fullName: String
    var emailAddress: String
    ...
}

At first glance, the above might look like any standard data model, but there is actually a substantial risk for it to become inconsistent. Since we have three separate properties for firstName, lastName and fullName, we’re going to always have to remember to update a contact’s fullName whenever we make any changes to either the firstName or lastName property — otherwise, we’ll end up ambiguous information.

Since fullName is, at the end of the day, a convenience — a higher-level state that makes building our UI simpler — let’s make it derivative. Rather than implementing it as a separate, stored property, let’s turn it into a computed one instead:

struct Contact {
    let id: ID
    var firstName: String
    var lastName: String
    var fullName: String { "\(firstName) \(lastName)" }
    var emailAddress: String
    ...
}

That way, we no longer have to worry about our model becoming inconsistent, since a contact’s fullName will now be re-computed each time that it’s accessed according to the current firstName and lastName.

However, always re-computing a dependent state each time that it gets accessed is not always practical — especially if that state depends on a potentially large collection of elements, or if the required computation involves a bit more than simply combining a few underlying values.

Like we took a look at in “Utilizing value semantics in Swift”, in those situations, maintaining a separate stored property might again be the best approach — but if we prevent that property from being externally mutated, and if we make it auto-update whenever its underlying state changes, then we can still ensure that its containing model remains consistent.

Here’s how we might use a property observer to do that for a Leaderboard model, which contains high score entries for the top players of a game, as well as the current average score among those players:

struct Leaderboard {
    typealias Entry = (name: String, score: Int)

    var entries: [Entry] {
        // Each time that our array of entries gets modified, we
        // re-compute the current average score:
        didSet { updateAverageScore() }
    }
    
    // By marking our property as 'private(set)', we prevent it
    // from being mutated outside of this type:
    private(set) var averageScore = 0

    init(entries: [Entry]) {
        self.entries = entries
        // Property observers don't get triggered as part of
        // initializers, so we have to call our update method
        // manually here:
        updateAverageScore()
    }

    private mutating func updateAverageScore() {
        guard !entries.isEmpty else {
            averageScore = 0
            return
        }

        let totalScore = entries.reduce(into: 0) { score, entry in
            score += entry.score
        }

        averageScore = totalScore / entries.count
    }
}

The patterns used above don’t only improve the consistency of our models, they also make those models easier to use and understand — since we no longer have to be aware of rules like “always remember to update X when you modify Y” — as that sort of logic is now baked into the models themselves.

Consistent collections

While maintaining a 1:1 relationship between two pieces of state can be challenging enough, things get even trickier when we have to ensure that multiple collections remain consistent with each other. Going back to the example of a contact management app from before, let’s say that we’re now building a ContactList class — which will store a set of contacts, while also enabling those contacts to be organized into groups, and to be marked as favorites:

class ContactList {
    var name: String
    var contacts = [Contact.ID : Contact]()
    var favoriteIDs = Set<Contact.ID>()
    var groups = [Contact.Group.Name : Contact.Group]()

    init(name: String) {
        self.name = name
    }
}

Above we’re making use of nested types in order to make types like Group and Name more contextual and “self-documenting”.

Similar to the earlier example in which we were required to manually keep a contact’s names in sync, the above model also makes each of its call sites responsible for keeping it consistent. For example, when removing a contact, we also have to remember to remove its ID from our set of favoriteIDs — and when renaming a group, we always have to update its key within the groups dictionary as well.

Both of the two functions below fail to do that, and even though they might look perfectly valid, they’re both causing the ContactList they’re mutating to become inconsistent:

func removeContact(_ contact: Contact) {
    // If the removed contact was also added as a favorite, its
    // ID will still remain in that list, even after it was removed.
    contactList.contacts[contact.id] = nil
}

func renameGroup(named currentName: Contact.Group.Name,
                 to newName: Contact.Group.Name) {
    // The renamed group's key will now be incorrect, since
    // it's still referring to the group's previous name.
    contactList.groups[currentName]?.name = newName
}

An initial idea on how to avoid the above kind of inconsistencies might be to take the same private(set) approach that we used on our Leaderboard model from before, and prevent our collections from being mutated outside of the ContactList type:

class ContactList {
    var name: String
    private(set) var contacts = [Contact.ID : Contact]()
    private(set) var favoriteIDs = Set<Contact.ID>()
    private(set) var groups = [Contact.Group.Name : Contact.Group]()
    ...
}

However, in this case we actually need to be able to mutate our collections somehow — so the above approach would require us to replicate several of our underlying collections’ APIs, in order to make mutations like adding and removing contacts possible:

extension ContactList {
    func add(_ contact: Contact) {
        contacts[contact.id] = contact
    }

    func remove(_ contact: Contact) {
        contacts[contact.id] = nil
        favoriteIDs.remove(contact.id)
    }

    func renameGroup(named currentName: Contact.Group.Name,
                     to newName: Contact.Group.Name) {
        guard var group = groups.removeValue(forKey: currentName) else {
            return
        }

        group.name = newName
        groups[newName] = group
    }
}

The above might work as long as we only have to mutate our collections in very simple ways, and as long as we won’t add any new pieces of data to our model, but it’s not a very flexible solution overall. Requiring a brand new API to be created for each mutation is, in general, not a great design — so let’s see if we can find a more dynamic and future-proof approach.

If we think about it, keeping our ContactList data in sync really just requires us to be able to react to any changes to the property that’s also used as an element’s key (id in the case of Contact and name in the case of Contact.Group), and to be able to perform an update whenever an element was removed (so that we can make sure that no removed contacts still remain in the favoriteIDs set).

Let’s add both of those two capabilities by implementing a very lightweight wrapper around Dictionary. Our wrapper, let’s call it Storage, will use Swift’s key paths mechanism in order to keep our keys in sync — and will also enable us to attach a keyRemovalHandler closure to get notified whenever a key was removed:

extension ContactList {
    struct Storage<Key: Hashable, Value> {
        fileprivate var keyRemovalHandler: ((Key) -> Void)?

        private let keyPath: KeyPath<Value, Key>
        private var values = [Key : Value]()

        fileprivate init(keyPath: KeyPath<Value, Key>) {
            self.keyPath = keyPath
        }
    }
}

Our initializer and keyRemovalHandler are marked as fileprivate in order to prevent instances of our new Storage type to be created outside of the file that ContactList is defined in, further strengthening the consistency of our model code.

To make Storage behave like a real Swift collection, we have two options. We could either make it conform to the full Collection protocol or, if we only need to be able to iterate over it, we could simply make it conform to Sequence — by forwarding the call to makeIterator() to its underlying dictionary:

extension ContactList.Storage: Sequence {
    func makeIterator() -> Dictionary<Key, Value>.Iterator {
        values.makeIterator()
    }
}

With the above in place, we will still be able to write for loops over our collections, and to use APIs like forEach, map and filter on them — just like when using Dictionary directly.

Next, to enable Storage to be mutated, we’re going to add a subscript implementation that both ensures that an element’s key gets updated in case the property its key is based on was changed, and that also calls our keyRemovalHandler when a key was removed:

extension ContactList.Storage {
    subscript(key: Key) -> Value? {
        get { values[key] }
        set {
            guard let newValue = newValue else {
                return remove(key)
            }

            let newKey = newValue[keyPath: keyPath]
            values[newKey] = newValue

            if key != newKey {
                remove(key)
            }
        }
    }

    private mutating func remove(_ key: Key) {
        values[key] = nil
        keyRemovalHandler?(key)
    }
}

Just like that, our collection wrapper is done, and we’re ready to update ContactList to use it — by storing our contacts and groups using our new type, and by registering a keyremovalHandler that ensures that our favoriteIDs set stays in sync with our collection of contacts:

class ContactList {
    var name: String
    var contacts = Storage(keyPath: \Contact.id)
    var favoriteIDs = Set<Contact.ID>()
    var groups = Storage(keyPath: \Contact.Group.name)

    init(name: String) {
        self.name = name

        contacts.keyRemovalHandler = { [weak self] key in
            self?.favoriteIDs.remove(key)
        }
    }
}

Note how Swift is able to infer the generic Key and Value types of our Storage instances based on the key paths that are passed into them — beautiful!

With our new implementation, we’ll still be able mutate our collections by adding and removing values, just like when using Dictionary directly — only now we’re ensuring that our data remains consistent, completely automatically.

As an added bonus, since we now have a custom collection type in place, we could go one step further and make it even nicer to use — by adding convenience APIs for adding and removing values without having to worry about what key to use:

extension ContactList.Storage {
    mutating func add(_ value: Value) {
        let key = value[keyPath: keyPath]
        values[key] = value
    }

    mutating func remove(_ value: Value) {
        let key = value[keyPath: keyPath]
        remove(key)
    }
}

Using the above APIs and our subscript implementation from before, we’re now free to decide how we want to add and remove values within each situation, without impacting the consistency of our model in any way:

// Adding values:
contactList.contacts[contact.id] = contact
contactList.contacts.add(contact)

// Removing values:
contactList.contacts[contact.id] = nil
contactList.contacts.remove(contact)

While writing a custom collection isn’t always appropriate, whenever we want to add new behaviors to one of the data structures that the standard library offers, creating lightweight wrappers that are each tailored to a very specific domain can be a great approach.

Support Swift by Sundell by checking out this sponsor:

Paw
Paw

Paw: A GraphQL and REST API client that lets you test and describe the APIs that you call from your app. Just enter the URL of the API endpoint that you’re looking to call, add any headers, parameters, authentication, or body data. Hit return — and everything is automatically checked for you, from the standard OAuth 2 login to very custom API flows.

Conclusion

In many ways, in order to make a code base really robust and capable, we have to start by making its core data models as predictable and consistent as we possibly can — since those models often act as the foundation on top of which the rest of our code base is built.

By not requiring various states to be synced manually, and by preventing inconsistent states from being stored in the first place, we not only end up with a much stronger foundation — but one that’s often easier to work with as well, as our model architecture ends up doing much of that synchronization work for us.

What do you think? How do you currently ensure that your models remain as consistent as possible, and will any of the techniques covered in this article help you get closer to that goal? Let me know — along with your questions, comments and feedback — either on Twitter or via email.

Thanks for reading! 🚀