Identifying objects in Swift

We often encounter situations when we need to find a way to store objects based on some concept of identity. Whether it's in a cache, storing representations of objects on disk, or simply using a dictionary - we often need to find ways to uniquely identify objects that we deal with.

This week, let's take a look at some common concepts of identity that we have at our disposal in Swift, and how we can use them in different ways for values and objects.

Equatable

A core protocol that is often used to compare objects & values is Equatable. This is a protocol that many of you are probably already familiar with, since anytime you want to enable the == operator to be used with a type, you need to conform to it. Here's an example:

struct Book {
    let title: String
    let author: String
}

extension Book: Equatable {
    static func ==(lhs: Book, rhs: Book) -> Bool {
        guard lhs.title == rhs.title else {
            return false
        }

        guard lhs.author == rhs.author else {
            return false
        }

        return true
    }
}

Instance equatable

While Equatable is perfect for when dealing with values (like structs or enums), for objects/classes it might not be what you're looking for. Sometimes you want to check if two objects are the same instance. To do this, we use =='s lesser known sibling; ===, which lets you compare two objects based on their instance, rather than their value.

Let's take a look at an example where we want to reload an InventoryManager every time it gets assigned a new dataSource:

// Protocols with the 'class' constraint can only be conformed to
// by classes, enabling us to assume that an object will be used.
protocol InventoryDataSource: class {
    var numberOfItems: Int { get }

    func item(at index: Int) -> Item
}

class InventoryManager {
    var dataSource: InventoryDataSource? {
        // 'oldValue' is a magic variable that always contains the
        // previous value of the property when a new one is set
        didSet { dataSourceDidChange(from: oldValue) }
    }

    private func dataSourceDidChange(from previousDataSource: InventoryDataSource?) {
        // We don't want to reload if the same data source is re-assigned
        guard previousDataSource !== dataSource else {
            return
        }

        reload()
    }
}

As you can see above, using === can be pretty neat, since it enables you to perform checks without requiring a type conforming to a certain protocol to also implement Equatable (which adds boilerplate, and also self-constraints the protocol - more on the latter in an upcoming post).

Hashable

Just like Equatable, another protocol that is super common to conform to when working with value types is Hashable. This is a requirement whenever you are using a type in some form of hash-based collection, like a Set, or as a key in a Dictionary.

Let's extend our Book type from before to also conform to Hashable:

extension Book: Hashable {
    var hashValue: Int {
        return title.appending(author).hashValue
    }
}

We are now able to, for instance, build a collection of only unique books:

class BookStore {
    var inventory = [Book]()
    var uniqueBooks = Set()

    func add(_ book: Book) {
        // Inventory will contain all books, including duplicates
        inventory.append(book)

        // By using a set (which we can, now that 'Book' conforms to 'Hashable')
        // we can guarantee that 'uniqueBooks' only contains unique values
        uniqueBooks.insert(book)
    }
}

Hashable for protocol types

While Hashable is easy to use (although it can become a bit boilerplate-ish to implement) when dealing with concrete types (such as in the BookStore example above), it can become a bit tricky when dealing with protocols.

One thing to keep in mind when using hash values, is that you can only rely on them when you know that all of your objects or values are of the exact same type. Since this is not the case with protocols, we have to rely on some other method.

Let's say we're building a rendering API, where different objects can request to be rendered the next time the screen draws a frame. To use this API, objects conform to a Renderable protocol, and uses a Renderer to enqueue themselves for rendering when needed (similar to how UIView has a setNeedsLayout method), like this:

class Circle {
    var radius: CGFloat {
        didSet { renderer?.enqueue(self) }
    }
    var strokeColor: UIColor {
        didSet { renderer?.enqueue(self) }
    }
    weak var renderer: Renderer?
}

extension Circle: Renderable {
    func render(in context: CGContext) {
       context.drawCircle(withRadius: radius, strokeColor: strokeColor)
    }
}

As an optimization, we want to make sure that we're only rendering each object once per frame, even though it might request rendering multiple times before a frame is drawn. To make this happen we'll need to keep track of unique instances conforming to Renderable that have been enqueued, but since those instances are probably going to be of completely different types, we can't just add Equatable or Hashable as a requirement.

ObjectIdentifier

One solution to the above problem, is to use Swift's ObjectIdentifier type to identify instances, and to make sure that our rendering queue won't contain duplicates.

In Swift, every instance of a class has a unique identifier that you can obtain by creating an ObjectIdentifier for it, like this:

func enqueue(_ renderable: Renderable) {
    let identifier = ObjectIdentifier(renderable)
}

ObjectIdentifier already conforms to both Equatable and Hashable, which enables us to write a thin wrapper type around Renderable that uses each instance's identifier to provide it with identity:

class RenderableWrapper {
    fileprivate let renderable: Renderable

    init(renderable: Renderable) {
        self.renderable = renderable
    }
}

extension RenderableWrapper: Renderable {
    func render(in context: CGContext) {
        // Forward the call to the underlying instance
        renderable.render(in: context)
    }
}

extension RenderableWrapper: Equatable {
    static func ==(lhs: RenderableWrapper, rhs: RenderableWrapper) -> Bool {
        // Compare the underlying instances
        return lhs.renderable === rhs.renderable
    }
}

extension RenderableWrapper: Hashable {
    var hashValue: Int {
        // Use the instance's unique identifier for hashing
        return ObjectIdentifier(renderable).hashValue
    }
}

We can now simply use a Set to keep track of unique instances that need rendering in our Renderer:

class Renderer {
    private var objectsNeedingRendering = Set()

    func enqueue(_ renderable: Renderable) {
        // Since we use a 'Set', duplicates will automatically be
        // discarded, we don't have to write any code for it
        let wrapper = RenderableWrapper(renderable: renderable)
        objectsNeedingRendering.insert(wrapper)
    }

    func screenDidDraw() {
        // Start by emptying the queue
        let objects = objectsNeedingRendering
        objectsNeedingRendering = []

        // Render each object
        for object in objects {
            let context = makeContext()
            object.render(in: context)
        }
    }
}

Conclusion

When using values, Equatable and Hashable is probably the way to go in terms of identity - since you are more comparing a normalized representation of a value, rather than a unique instance. However, when dealing with objects, using some of the techniques from this post can make your APIs easier to use, and in result decrease complexity and increase stability.

Instead of requiring implementors to conform to Equatable, or exposing some form of unique identifier (like a UUID), you can use techniques like the === operator and the ObjectIdentifier type to quickly and uniquely identify objects without much extra code.

What do you think? Do you have any other favorite techniques when it comes to dealing with object identity in Swift? Let me know, along with any questions or feedback that you might have - either in the comments section below or on Twitter @johnsundell.

Thanks for reading 🚀

Building a declarative animation framework in Swift - Part 1

Writing end-to-end JSON mapping tests in Swift