Introduction

In this blog post, we'll dive into the nuances of using ForEach in SwiftUI, especially when dealing with tuples. Understanding how ForEach works and the significance of unique identifiers is crucial for any SwiftUI developer.

Overview: The Role of ForEach

ForEach in SwiftUI is a powerful tool used to iterate over a collection. It requires each item in the data set to have a unique id. In this example, we use an integer as the id, but be cautious — if the id is not unique, issues may occur.

Simple Example with Integers

Here's a straightforward example using a tuple list where id:\.0 represents the integer, providing a unique identifier:

struct Example: View {
var tupleListExample: [(Int, String)] = [
(1, "A"),
(2, "B"),
(3, "C"),
]
var body: some View {
VStack {
ForEach(tupleListExample, id:\.0) { item in // [!code highlight]
HStack {
Text("\(item.0)")
Spacer()
Text(item.1)
}
}
}
}
}

In this snippet, .0 and .1 reference the first and second elements of each tuple, respectively.1

Common Pitfalls: Non-unique Identifiers

Using non-unique identifiers like Date() can lead to unpredictable behavior. Below is an example where this approach sometimes works and sometimes doesn't, due to the Date() instances potentially initializing at the same time:

struct Example: View {
var tupleListExample: [(Date, String)] = [
(Date(), "A"),
(Date(), "B"),
(Date(), "C"),
]
var body: some View {
VStack {
ForEach(tupleListExample, id:\.0) { item in
HStack {
Text("\(item.0)")
Spacer()
Text(item.1)
}
}
}
}
}

This example illustrates the unreliability of Date() as a unique identifier. Why is this? ForEach needs the id to be unique within the collection — when two rows report the same identity, SwiftUI can't tell them apart, so it may drop, duplicate, or fail to animate rows. Three Date() values constructed back-to-back can easily land on the same instant,2 which is exactly the collision that breaks it.

Conclusion: Guarantee unique id

When looping through an array of tuples, a better approach is to convert them into a struct that conforms to the Identifiable protocol, using UUID for a truly unique id.

Transforming Tuples into Structs

Here's how you can convert a list of tuples into a struct:

import SwiftUI
import Combine
// Custom struct conforming to Identifiable
struct Foo: Identifiable { // [!code highlight]
var id: UUID = UUID() // [!code highlight]
var date: Date
var letter: String
}
// ViewModel for the view
class ExampleViewModel: ObservableObject {
@Published var results: [Foo] = []
func updateResults(using tuples: [(Date, String)]) {
results = tuples.map { Foo(date: $0.0, letter: $0.1) }
}
}
// The SwiftUI View
struct Example: View {
@StateObject private var model = ExampleViewModel()
var tupleListExample: [(Date, String)] = [
(Date(), "A"),
(Date(), "B"),
(Date(), "C"),
]
var body: some View {
VStack {
ForEach(model.results, id: \.id) { item in
HStack {
Text("\(item.date)")
Spacer()
Text(item.letter)
}
}
}.onAppear {
model.updateResults(using: tupleListExample)
}
}
}
struct Example_Previews: PreviewProvider {
static var previews: some View {
Example()
}
}

In this example, we create a Foo struct conforming to Identifiable, and a view model to handle the transformation of tuple data into this struct.

Summary

In conclusion, while tuples are handy in SwiftUI, ensuring unique identifiers is key to avoiding issues with ForEach. Converting tuples into a struct with a UUID id is a reliable way to ensure uniqueness.

Feel free to explore further in the SwiftUI Documentation for more insights.

  1. id:\.0 is a key-path expression — it tells ForEach to use the tuple's first element as the identity for each row. Whatever you point it at has to be both Hashable and unique across the collection.

  2. Date() reads the system clock, but the three initializers run microseconds apart — fast enough that they can resolve to the same value, especially once SwiftUI compares them. Identity should come from the data, not from when the row happened to be created.