Using Identifiable in SwiftUI

Using Identifiable in SwiftUI

In this blog post I will explain Identifiable and ObjectIdentifierbased on the example of using SwiftUI's List.

Typically you create lists dynamically from an underlying collection of data.

struct Ocean {
    let name: String
}

struct ContentView: View {
    private var oceans = [
        Ocean(name: "Pacific"),
        Ocean(name: "Atlantic"),
        Ocean(name: "Indian"),
        Ocean(name: "Southern"),
        Ocean(name: "Arctic")
    ]

    var body: some View {
        List(oceans) {
            Text($0.name)
        }
    }
}

This will fail

Initializer 'init(_:rowContent:)' requires that 'Ocean' conform to 'Identifiable'.

SwiftUI needs to know how it can identify each item uniquely otherwise it will struggle to compare view hierarchies to figure out what has changed.

There are several possible solutions.

Using a different initializer

    var body: some View {
        List(oceans, id: \.name) {
            Text($0.name)
        }
    }

Here we use an initializer to pass in the key path of an attribute that helps SwiftUI to identify each item uniquely.

Choose this option if you want to keep your data model unchanged.

Adopting Identifiable

struct Ocean: Identifiable {
    let name: String
    let id = UUID()
}

This is my recommended way of dealing with this situation.

As you can see it is quite simple. Swift 5.1 added the Identifiable protocol to the standard library, declared as follows:

protocol Identifiable {
    associatedtype ID: Hashable
    var id: ID { get }
}

The only requirement is to have a property with the name id which is Hashable.

Use the Identifiable protocol to provide a stable notion of identity to a class or value type. For example, you could define a User type with an id property that is stable across your app and your app’s database storage. You could use the id property to identify a particular user even if other data fields change, such as the user’s name.

An excellent article to dig deeper is:

In Swift, only class instances and metatypes have unique identities. There is no notion of identity for structs, enums, functions, or tuples.

Using a Reference Type For Implicit Adoption

Identifiable provides a default implementation for class types (using ObjectIdentifier), which is only guaranteed to remain unique for the lifetime of an object.

class Ocean: Identifiable {
    let name: String

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

This works because of the default protocol implementation of id for AnyObject types:

extension Identifiable where Self: AnyObject {
  public var id: ObjectIdentifier {
    return ObjectIdentifier(self)
  }
}

If an object has a stronger notion of identity, it may be appropriate to provide a custom implementation.

Note that for a class type an explicit initializer is needed. Otherwise, you will encounter the error:

'Ocean' cannot be constructed because it has no accessible initializers

If you wanna learn more about ObjectIdentifier then I recommend this article:

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!