Enumerating Tuples In SwiftUI

swift, swiftui, tuples, list – Feb 13 2021

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

• • •

Overview

ForEach is expecting that your data has some unique id. In this example that is the Integer. If it’s not unique you will run into issues. This example is probably too simple but it will help illustrate a point.

This example it works fine because id:\.0 is an Int and is unique

struct Example: View {
    
    var tupleListExample: [(Int, String)] = [
        (1, "A"),
        (2, "B"),
        (3, "C"),
    ]
    
    var body: some View {
        VStack {
            // NOTE: \.0 is referencing (1, "A") 
            // and if it was \.1 it would be referencing "A" from the tuple
            ForEach(tupleListExample, id:\.0) { item in
                HStack {
                    Text("\(item.0)")
                    Spacer()
                    Text(item.1)
                }
            }
        }
    }
    
}

Example of Problems

You will run into problems when the id is not unique.

I ran into this problem assuming I could use Date() as a unique idenitifer. In this example it sometimes work and soemtimes doesn’t because these dates get init at the same time. Meaning they could all have the same Date() or not.

From this example I came to the conclusion that a Date() is not reliable identifier because you could have items in the collection that could have the same Date().

struct Example: View {
    
    var tupleListExample: [(Date, String)] = [
        (Date(), "A"),
        (Date(), "B"),
        (Date(), "C"),
    ]
    
    var body: some View {
        VStack {
            // This may or may not work as the Date() could all be the same
            ForEach(tupleListExample, id:\.0) { item in
                HStack {
                    Text("\(item.0)")
                    Spacer()
                    Text(item.1)
                }
            }
        }
    }   
}

Conclusion

If you find yourself looping through an array of tuples, maybe consider converting them into some struct that conforms to the Identifiable protocol and make the id property a UUID. As this would guarantee that the id is truly unique.

Here is an example of how to do so.

import SwiftUI
import Combine

// 01 Create a custom struct that conforms to Identifiable
struct Foo: Identifiable {
    var id: UUID = UUID()
    var date: Date
    var letter: String
}


// 02 Create view model for this view
class ExampleViewModel: ObservableObject {

    @Published var results: [Foo] = []
    
    func updateResults(using tuples: [(Date, String)]) {
        let newResults = tuples.map({Foo.init(date: $0.0, letter: $0.1)})
        results = newResults
    }

}

// 03 Your View
struct Example: View {
    
    // 4 Create the model as a `StateObject`
    @StateObject private var model: ExampleViewModel = ExampleViewModel()
    
    var tupleListExample: [(Date, String)] = [
         (Date(), "A"),
         (Date(), "B"),
         (Date(), "C"),
     ]
    
     var body: some View {
         VStack {
            // 5 You can now reference the results from the model and use the `id`
            // by default the results will be empty but in the `onAppear` you will
            // see how this will return the results we want
            ForEach(model.results, id:\.id) { item in
                 HStack {
                     Text("\(item.date)")
                     Spacer()
                     Text(item.letter)
                 }
             }
         }.onAppear(perform: {
            // 6 We use onAppear to pass the tuples to our view model that will then do the
            // the work of converting it into our Struct that our view would perfer.
            model.updateResults(using: tupleListExample)
         })
     }
    
}

struct Example_Previews: PreviewProvider {
    static var previews: some View {
        Example()
    }
}
Follow on Twitter
Follow on Github