Enumerating Tuples In SwiftUI

ForEach with an Array of Tuples can cause some unexpected results, if your id that your using is not unique. Here is a story.

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
                HStack {
                    Text("\(item.0)")
                    Spacer()
                    Text(item.1)
                }
            }
        }
    }
}

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

Common Pitfalls: Non-unique Identifiers

Problematic Example with Dates

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. And if it contains the same date and even the same second it will behave in strange ways.

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 {
    var id: UUID = UUID()
    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.