In this blog post, I will explore the Swift Identifiable protocol and how it plays a crucial role in identifying data within SwiftUI views. SwiftUI, Apple’s modern declarative framework for building user interfaces, brings a fresh approach to handling data and views. One of its key features is the ability to efficiently manage and update views based on their unique identifiers.
In this tutorial, I will guide you through the process of implementing the Identifiable protocol in your data models, explaining its significance in SwiftUI views. You will explore various scenarios where SwiftUI utilizes identifiable, such as working with SwiftUI List, ForEach, and Picker.
By the end of this tutorial, you will have a solid understanding of how SwiftUI identifies data using the Identifiable protocol and how you can leverage this powerful feature in your own SwiftUI projects. So, let’s dive in and discover how identifiable enhances the way SwiftUI handles data in list views!
What is the Identifiable Protocol
In SwiftUI, the Identifiable protocol plays a crucial role in identifying and managing data in list views. It is a protocol provided by the Swift standard library that allows us to uniquely identify instances of a data type.
When a data type conforms to the Identifiable protocol, it must provide a property called id
that uniquely identifies each instance. This id property can be of any type that conforms to the Hashable protocol, such as UUID, String, or Int.
You can find information about the Identifiable Procol conformance requirements in the documentation:
/// A class of types whose instances hold the value of an entity with stable
/// identity.
/// Conforming to the Identifiable Protocol
/// =======================================
///
/// `Identifiable` provides a default implementation for class types (using
/// `ObjectIdentifier`), which is only guaranteed to remain unique for the
/// lifetime of an object. If an object has a stronger notion of identity, it
/// may be appropriate to provide a custom implementation.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public protocol Identifiable<ID> {
/// A type representing the stable identity of the entity associated with
/// an instance.
associatedtype ID : Hashable
/// The stable identity of the entity associated with this instance.
var id: Self.ID { get }
}
By conforming to the Identifiable protocol, SwiftUI is able to efficiently track and update individual items in a list view. It enables SwiftUI to determine which items have been added, removed, or modified, and apply the necessary updates to the user interface.
Implementing Swift Identifiable in Your Data Model
To implement the identifiable protocol in your data model, follow these steps:
1. Declare conformance to the identifiable protocol in your data model struct or class.
struct Person: Identifiable {
let id: String
var name: String
}
Notice that I made the id a let
constant and the name a var
. The name property can be changed later by the user. But the id should always stay the same, which I enforce by making it a let
. The id needs to stay the same otherwise SwiftUI will not be able to identify your data correctly and you may get funky animations and reordering of your UI.
2. Provide a unique identifier for each instance by adding an id property.
let person1 = Person(id: "1", name: "John")
let person2 = Person(id: "2", name: "Jane")
Make sure that the identifier property is unique for each instance. This uniqueness is crucial for Swift to correctly identify and update items in list views.
What Types Can You Use for ObjectIdentifiers?
The identifier property can be of any type that conforms to the Hashable protocol, such as UUID, String, Int, or even a custom type. Oftentimes, you will see UUIDs because they ensure that all IDs are unique. Every time I create a new UUID with the initializer UUID(), a new unique id will be generated:
struct Person: Identifiable {
let uuid: UUID
var name: String
init(name: String, uuid: UUID = UUID() {
self.name = name
self.uuid = uuid
}
}
If your data is coming from a server or another external source, ensure that the identifier is unique for each item. You can use a computed property to generate a unique identifier based on the data itself.
struct Person: Identifiable {
let id: String
let name: String
// other properties...
var identifier: String {
// Generate a unique identifier based on the data
return "\(name)-\(id)"
}
}
How to Make Enum Types Conform to Swift Identifiable?
To implement Swift Identifiable in an enum, you need to provide a unique identifier for each case. You have a few options for achieving this:
- Use self as the identifier:
enum Animal: Identifiable {
case dog
case cat
case bird
var id: Self { return self }
}
2. Using Raw Values: If your enum has raw values of a type that conforms to Hashable, you can use the raw value as the identifier. For example, if you have an enum representing different categories of animals, you can use the animal’s name as the identifier:
enum Animal: String, Identifiable {
case dog = "Dog"
case cat = "Cat"
case bird = "Bird"
var id: String { return rawValue }
}
3. Using Associated Values: If your enum cases have associated values, you can use them as identifiers. For example, if you have an enum representing different types of tasks, you can use the task name as the identifier:
enum Task: Identifiable {
case work(String)
case study(String)
var id: String {
switch self {
case .work(let name), .study(let name):
return name
}
}
}
If you use another type that is identifiable as the associated value, you can return its id:
enum PeopleConnection: Identifiable {
case friend(Person)
case college(Person)
var id: UUID {
switch self {
case .friend(let person), .college(let person):
return person.id
}
}
}
How to Make a Type Identifiable by Itself?
You can use the type itself to identify it. The identity of the instance will change when any of its properties change.
This will increase the size of the label’s title and icon. If you want to make the image larger or smaller independently from the text, you can use the image scale modifier:
struct Pet: Identifiable {
var name: String
var animal: Animal
var owner: Person
var id: Self { return self }
}
Examples of Identifiable in SwiftUI Views
In this section, I will walk you through a couple of examples to demonstrate how to use the Identifiable protocol in SwiftUI list views.
You can use Identifiable data with SwiftUI Lists. For example, we can show a list of people:
struct ContentView: View {
@State private var people: [Person] = [
Person(name: "John"),
Person(name: "Jane"),
Person(name: "Mike")
]
var body: some View {
NavigationStack {
List(people) { person in
Text(person.name)
}
.navigationTitle("Contacts")
}
}
}
Similarly, you can use identifiable data with ForEach
. For example, if I want to add swipe on delete to the above example:
List {
ForEach(people) { person in
Text(person.name)
}
.onDelete(perform: { indexSet in
people.remove(atOffsets: indexSet)
})
}
Additionally, you can use identifiable data with SwiftUI Pickers
. For example, if you wanted to add a control view where the user selects its favorite animal:
private enum Animal: String, Identifiable, CaseIterable {
case dog = "Dog"
case cat = "Cat"
case bird = "Bird"
var id: Self { return self }
}
I made the Animal enum conform to CaseIterable Protocol to get an array of all cases, which is used to show all options in the picker:
struct ContentView: View {
@State private var selectedAnimal: Animal = Animal.dog
var body: some View {
Picker("Favorite Animal", selection: $selectedAnimal) {
ForEach(Animal.allCases) { animal in
Text(animal.rawValue)
//.tag(animal)
}
}
.pickerStyle(.segmented)
}
}
What Happens When SwiftUI Cannot Correctly Identify Your Data
I want to show you why having a stable identification of your views is important in SwiftUI. There is no better way to show you than by giving a bad example.
In the blow example, when the user presses on the plus button a new food item is attached to the data array. The new items always have the same name and icon. Because the list in this example uses the name property to identify the data, it finds multiple rows with the same name and identifies them the same. Thus, all new food items are treated as the same data, and the same view is used. These updating problems will increase if you move and delete data entries. You can avoid these issues by using stable identification.
struct Food {
var name: String
var icon: String
var isFavorite: Bool
}
struct FoodListView: View {
@State private var foods = Food.preview()
var body: some View {
NavigationView {
List(foods, id: \.name) { food in
HStack {
Text(food.icon)
Text(food.name)
}
}
.toolbar {
Button {
let newFood = Food(name: "new food",
icon: "\(foods.count)",
isFavorite: false)
foods.append(newFood)
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
}
In this example multiple data items with the same identifier are used. Causing the list to not correctly show the data.
Another bad practice would be to use a computed property with a new UUID:
private enum Animal: String, Identifiable {
case dog = "Dog"
case cat = "Cat"
case bird = "Bird"
var id: UUID { return UUID() }
}
Every time the view updates where this data is used, SwiftUI will access the id property and decide if the data changed. Because the ID changed, a new view needs to be recreated. This can cause unnecessary redraws that can significantly decrease the performance of your app.
Make sure to not change the identifier once the data is instanciated.
Conclusion
In conclusion, identifying data in SwiftUI’s list views is crucial for efficient view rendering and seamless user experiences. By conforming to the Swift Identifiable protocol, you enable SwiftUI to uniquely identify each item in your data models. This identification allows SwiftUI to optimize view updates, animations, and transitions, resulting in smoother and more responsive interfaces.
Throughout this blog post, you have explored various scenarios where Swift Identifiable is used, such as in lists, pickers, tables, and more. By leveraging Identifiable, you can ensure that your SwiftUI apps perform optimally and provide a seamless user experience.