In Part I of this series, I discussed how my Apple TV app, Scoreboard, uses a model comprised entirely of value types, allowing me to centralize all view updates in a single property observer. In Part II, I discussed using an array diffing algorithm to figure out how to update the view. In Part III, I showed how you can give value types identities so you can tell an addition from an edit. In Part IV, I explained how this diff-based updating logic is shared by unrelated controllers.

But there's still a significant problem with this approach. In a conventional object-based model, you can hand your own objects to other view controllers, and they can mutate them in ways that your code will see later. In a value-based model, other view controllers have their own copies of your models, and their changes don't affect your copies. How can you be sure to update the right part of your model?

The answer comes back to the Identifiable protocol.

The Approach

Let's say you're writing a PlayerEditorViewController. This controller can edit a Player's name and score. It keeps the instance it's editing in a player field. You display it with an EditPlayer segue, and it returns with an unwind segue to your savePlayer(_:) action.

Writing prepareForSegue(_:sender:) is simple enough:

func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    switch segue.identifier {
    case "EditPlayer"?:
        let indexPath = indexPathForCell(sender as! UICollectionViewCell)
        let controller = segue.destinationViewController as! PlayerEditorViewController

        controller.player = scoreboard.players[indexPath.item]

    default:
        fatalError("Unknown segue \(segue.identifier)")
    }
}

But what happens now? If the controller tries to edit the player, that won't change anything in scoreboard.players.

We're going to have to remember something about the Player that we can use to find it later. One strategy would be to store its index in the scoreboard.players array. But array indices can change; we'd prefer something more permanent. What can we do?

Well, we already have a permanent identifier for each model: the model's identity. We can use that to look for the right model to replace. Here's a naïve example:

@IBAction func savePlayer(segue: UIStoryboardSegue) {
    let controller = segue.sourceViewController as! PlayerEditorViewController
    let player = controller.player

    scoreboard.players[scoreboard.players.indexOf({ p in p === player })!] = player
}

That's the logic we need. But it's quite a lot of boilerplate, isn't it? Especially for something we'll probably do in other places in our code. It'd be nicer to say, oh, maybe:

@IBAction func savePlayer(segue: UIStoryboardSegue) {
    let controller = segue.sourceViewController as! PlayerEditorViewController
    let player = controller.player

    scoreboard.players.byIdentity[player.identity] = player
}

Well, you can. And once you know the tricks, it's not even difficult.

Trick One: Extensions with a where Clause

Whatever we do here, we want it to apply to the Arrays of Identifiable types that we use. It shouldn't—really, can't—apply to other Arrays.

This is easy enough to ensure with a where clause:

extension Array where Element: Identifiable {
    ...
}

Trick Two: Computed Variables Setting self

What we want to do is provide something like a dictionary form of the array, with identities as keys and elements as values. We'll end up doing something else later, but we can use Dictionary as a placeholder for now.

What we're going to do is return a dictionary representation of the array:

extension Array where Element: Identifiable {
    var byIdentity: [Element.Identity: Element] {
        get {
            var dict: [Element.Identity: Element] = [:]
            for elem in self {
                dict[elem.identity] = elem
            }
            return dict
        }
    }
}

But we also want to be able to modify this dictionary and update the array from it. No problem—just add a setter:

extension Array where Element: Identifiable {
    var byIdentity: [Element.Identity: Element] {
        get {
            var dict: [Element.Identity: Element] = [:]
            for elem in self {
                dict[elem.identity] = elem
            }
            return dict
        }
        set {
            self = Array(newValue.values)
        }
    }
}

By setting self, we can replace the entire array with a new one derived from the identity-based representation. You might recognize this approach from Swift.String, which has characters, unicodeScalars, utf16, and utf8 views as assignable properties.

Trick Three: An Inside-Out Representation

However, we don't really want a Dictionary. The order of elements in our array is very important, but Dictionary stores its values unordered. Dictionary also contains a lot of things we don't really need—if you want to loop over the elements, you can just use the original array. And there's nothing stopping you from mismatching keys and value identities.

So let's stub out a custom type we'll use instead.

extension Array where Element: Identifiable {
    var byIdentity: IdentityView<Element> {
        get {
            return IdentityView(self)
        }
        set {
            self = newValue.elements
        }
    }
}

struct IdentityView<Element: Identifiable> {
    init(_ elems: [Element]) {
        ...
    }

    var elements: [Element] {
        get { ... }
    }

    subscript(identity: Element.Identity) -> Element? {
        get { ... }
        set { ... }
    }
}

As you can see, this type has the same subscripting semantics as a Dictionary, but none of the ancillary parts, like conformance to CollectionType.

But we have an important decision to make. How are we going to store the elements? Are we going to build some sort of dictionary or tree to help find elements faster by identity?

My decision was, no. We're going to use the array and scan it sequentially.

extension Array where Element: Identifiable {
    var byIdentity: IdentityView<Element> {
        (omitted)
    }

    func indexOfIdentity(identity: Element.Identity) -> Int? {
        return indexOf { elem in elem.identity == identity }
    }
}

struct IdentityView<Element: Identifiable> {
    init(_ elems: [Element]) {
        elements = elems
    }

    var elements: [Element]

    subscript(identity: Element.Identity) -> Element? {
        get {
            guard let index = elements.indexOfIdentity(identity) else {
                return nil
            }
            return elements[index]
        }
        set { ... }
    }
}

Why? Well, I expect that you're not going to hold on to an IdentityView instance very long. You're probably only going to subscript it once or twice before you throw it away. Any attempt to convert the array into a "faster" data structure would require at least one full scan of the array—and probably quite a bit more work than just that—while a single sequential scan would take no more time than that, and even two sequential scans would probably be faster than building a data structure. These are seat-of-the-pants decisions, of course, but if profiling demonstrated that IdentityView instances were problematically slow, it could always be revisited later in more depth.

The second decision to be made is about the setting logic. What do we do if identity doesn't exist in the array? What if identity and newValue.identity don't match? What if newValue is nil? Are we going to ignore some of those cases, or should we try to do the right thing?

My decision was to try to do something sensible in all of those cases, even though that's a little bit complicated. Rather than using lots of nested ifs, we use a switch statement on both newValue and elements.indexOfIdentity to determine exactly what to do.

    subscript(identity: Element.Identity) -> Element? {
        get {
            (omitted)
        }
        set {
            switch (newValue, elements.indexOfIdentity(identity)) {
            case (let newValue?, let index?):
                // Replacing/editing an existing instance
                elements[index] = newValue

            case (let newValue?, nil):
                // Adding something that doesn't exist yet
                elements.append(newValue)

            case (nil, let index?):
                // Setting something to nil to delete it
                elements.removeAtIndex(index)

            case (nil, nil):
                // Trying to delete something that doesn't actually exist
                break
            }
        }
    }
}

Putting It All Together

With that in place, the syntax I proposed earlier now works perfectly:

@IBAction func savePlayer(segue: UIStoryboardSegue) {
    let controller = segue.sourceViewController as! PlayerEditorViewController
    let player = controller.player

    scoreboard.players.byIdentity[player.identity] = player
}

We can use that in other places, too, such as to communicate updates between the master ScoreboardListViewController and the detail ScoreboardViewController:

protocol ScoreboardListViewControllerDetailType {
    var scoreboard: Scoreboard { get set }

    // detail controller must call master.detailDidUpdateSelectedScoreboard() after making changes
    weak var master: ScoreboardListViewController? { get set }
}

class ScoreboardListViewController: UITableView, Transformable {
    var detail: ScoreboardListViewControllerDetailType?

    var scoreboards: [Scoreboard] {
        didSet {
            transform(from: oldValue, to: scoreboards)

            if let detail = detail {
                // If the selected scoreboard has been deleted, pick a default.
                let updatedSelection = scoreboards.byIdentity[currentSelection.identity] ?? scoreboards.first
                let currentScoreboard = detail.scoreboard

                if currentSelection != updatedSelection {
                    detail.scoreboard = updatedSelection
                }
            }
        }
    }

    func detailDidUpdateSelectedScoreboard() {
        let updatedSelection = detail!.scoreboard
        scoreboards[updatedSelection.identity] = updatedSelection
    }

And there you have it: a very handy way to access and manipulate models by identity, rather than by using indexes that might change.

At this point, though, you might be feeling like this is a bit of a shaggy dog story. We've gone to a lot of effort to avoid using objects, but what's the benefit? Not having to write some insert and reload calls? I'll discuss some of the possibilities this technique opens up—and conclude the series—in Part VI.