In this blog post I will explain Identifiable
and ObjectIdentifier
based 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: