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. But when I left off, there was a major bug in that code: all changes were treated as adding or removing Players, not changing existing players.

The basic problem is simple: When we ask ArrayDiff to compare two Players to see if they're the same, we tell it to use ==, the equality operator:

self.scoreboard.players.diff(from: oldValue.players, match: ==)

But two Player instances aren't equal unless all of their properties are equal. The result: any change to a Player is registered as removing the old Player and inserting a new one.

What we really want is to compare identity, so changing the "same" Player is treated as an update, not a removal and addition. In other words, we want to use ===, the identity comparison operator:

self.scoreboard.players.diff(from: oldValue.players, match: ===)

But Swift won't compile that. What gives?

It turns out we've made a mistake. In the move to value types, we've accidentally lost the ability to talk coherently about "which instances changed" at all.

The Problem

Objects have a concept of an identity. Different objects are considered to be different, even if their contents are equal:

let string1 = NSString()
let string2 = NSString()
string1 == string2      // true
string1 === string2     // false

Value types, on the other hand, don't have the concept of an identity:

let string1 = ""
let string2 = ""
string1 == string2      // true
string1 === string2     // syntax error, no === operator

In an app like Scoreboard, this leads to trouble. We want to distinguish between this:

var player = scoreboard.players[0]
player.name = "Mike"
player.score = 0
scoreboard.players[0] = player

And this:

scoreboard.players[0] = Player(name: "Mike", score: 0)

In the first case, we want to update the cell for that player; in the second case, we want to remove the old cell and insert a new one. But for a value type, there's no concept of the first snippet "modifying" a Player while the second one "replaces" a Player. Players don't have an identity; you can tell if their contents are identical, but you can't tell if one is "the same one but modified" as another.

So does that mean we'll have to abandon this approach entirely, and go back to using classes and scattering update code around?

No. It's true that structs don't have identities. But we can give them identities ourselves. And it turns out we can do this in a wonderfully orthogonal way.

The Identifiable Protocol

What we're going to do is add a property to each struct which represents its identity. Two instances share an identity if their identity properties are equal. We can express this as a protocol:

protocol Identifiable {
    typealias Identity: Equatable
    var identity: Identity { get }
}

And then we can overload the === operator to support this notion:

func === <T: Identifiable> (lhs: T, rhs: T) -> Bool {
    return lhs.identity == rhs.identity
}

This protocol allows each conforming type to choose any type of identity it wants, as long as that type is Equatable. They can be Ints, Strings, or all sorts of other things. Scoreboard chooses to use a type specifically designed as an unique identifier: an NSUUID.

So we'll need to modify the simple definitions of Scoreboard and Player we gave earlier:

struct Scoreboard: Identifiable, Equatable {
    var identity = NSUUID()
    var name: String
    var players: [Player]
}

func == (lhs: Scoreboard, rhs: Scoreboard) -> Bool {
    return lhs === rhs && lhs.name == rhs.name && lhs.players == rhs.players
}

struct Player: Identifiable, Equatable {
    var identity = NSUUID()
    var name: String
    var score: Int
}

func == (lhs: Player, rhs: Player) -> Bool {
    return lhs === rhs && lhs.name == rhs.name && lhs.score == rhs.score
}

(These types' identity properties are mutable because the current implementation sometimes changes an identity during initialization. Otherwise, they would be let constants.)

A Little Bonus

You can make NSObject join the Identifiable party by extending it to treat ObjectIdentifier, the Swift standard library type that represents an object's identity, as its identity property:

extension NSObject: Identifiable {
    var identity: ObjectIdentifier {
        return ObjectIdentifier(self)
    }
}

Scoreboard doesn't do this because it has no need to treat objects as Identifiable, but it's a pretty neat trick!

(Why NSObject instead of AnyObject? It turns out Swift doesn't let you extend AnyObject. That's too bad, because it'd make this trick even cooler.)

Two Instances, One Identity

It's important to note a small difference between objects with identities and value-typed Identifiable instances. With objects, there is only ever one instance with a given identity. If two object variables are ===, then changes made through one variable will be seen through the other as well:

// PlayerObject is a hypothetical class-based version of Player
var playerObject = PlayerObject(name: "Joe", score: 0)
var playerObject2 = playerObject
playerObject2.score = 1

playerObject === playerObject2              // true
playerObject.score == playerObject2.score       // true

But even if they compare ===, Identifiable value types are still separate instances with separate copies of their data, so two === instances can have different values:

var player = Player(name: "Joe", score: 0)
var player2 = player
player2.score = 1

player === player2                  // true
player.score == player2.score               // false!

You can think of player and player2 as being two different versions of the same abstract Player. In the case of our diffing code, one of these versions is the Player as it once was, and the other is the Player as it is now.

Does this difference mean that overloading === is the wrong move, and I should be inventing some new kind of equality operator to test identities, or maybe using ~= instead? Maybe. It's a matter of opinion, and my opinion is that === is clear enough. But reasonable programmers can disagree.

Diffing By Identity

So with Identifiable in place, and its === overload set up, now we can make the change I proposed earlier:

self.scoreboard.players.diff(from: oldValue.players, match: ===)

And, et voilà!, our collection view reloads modified cells in place instead of removing and re-adding them. Much better!

This is working great for ScoreboardViewController, but there's one little problem: Scoreboard also has a list of Scoreboards, and we'd like to do the same thing there. But the list of Scoreboards is a table view, not a collection view. How can we share this diffing logic between two unrelated controllers with different view classes? I'll discuss that in Part IV.