1

I've got pickers working with a static list of items.

I am stuck on how to populate them using dynamic data (from an @state variable or an environment variable.)

This dirt-simple SwiftUI app creates a segmented control, but it doesn't show a current selection and won't allow the user to select one:

import SwiftUI

struct ContentView: View {
    @State private var colors = ["Red", "Green", "Blue"]
    @State private var selectedColor: Int = 0
    
    var body: some View {
        VStack {
            HStack {
                Spacer().frame(width: 30)
                Picker("What is your favorite color?", selection: $selectedColor) {
                    ForEach(colors, id: \.self) { aValidColor in
                        Text(aValidColor)
                    }
                }
                Spacer().frame(width: 30)
            }
            .pickerStyle(.segmented)
            Spacer().frame(height: 30)
            Text("Value: \(colors[selectedColor])")
        }
    }
}

(I'm using Xcode 15.0 beta 3, if that matters.)

I gather it has something to do with the ids of the items in my ForEach and the tag values that get assigned to the Text items in the Picker, but I can't figure out how to make it work.

Edit:

I got it to work by changing from an Array of String objects to an array of Identifiable Structs, and having the id of each struct be its index in the array:

import SwiftUI

struct AColor: Identifiable {
    let name: String
    let id: Int
}

struct ContentView: View {
    @State private var colors = [
        AColor(name: "Red", id: 0),
        AColor(name:"Green", id: 1),
        AColor(name:"Blue", id: 2)
    ]
    @State private var selectedColor: Int = 0
    
    var body: some View {
        VStack {
            HStack {
                Spacer().frame(width: 30)
                Picker("What is your favorite color?", selection: $selectedColor) {
                    ForEach(colors) { aValidColor in
                        Text(aValidColor.name)
                    }
                }
                Spacer().frame(width: 30)
            }
            .pickerStyle(.segmented)
            Spacer().frame(height: 30)
            Text("Value: \(colors[selectedColor].name)")
        }
    }
}

That is less than ideal though, since now I have to maintain an array of structs where each item in the array has a value that contains its index in the array. Ugh.

It seems like there should be some way to use enumerated() to get an array of objects and their indexes, but ForEach wants a Binding, not an enumerated sequence. (I'm still trying to wrap my head around bindings.)

2 Answers 2

5

The array contains strings but the Binding is to an Int.

So this would be another way to fix it:

ForEach(Array(colors.enumerated()), id: \.element) { index, color in
    Text(color).tag(index)
}

How it works

enumerated() returns a sequence of pairs (tuples). Each tuple consists of:

  • offset: a counter of type Int
  • element: an element from the original array, type String in your case.

This sequence is wrapped as a new Array, which is then used in the ForEach iteration. Since the elements of this array are not Identifiable, it is necessary to tell the ForEach how to form the id of each item:

  • In the snippet here, the source element (a String) is being used as the id. This is valid, because a String implements Hashable. However, for this to work, there may not be any duplicate strings in the source array.

  • Alternatively, you could use the offset. Doing it this way, you should make sure the contents of the source array do not change (at least, the item at any particular position should not change).

The nice thing is that both the offset and the element are supplied as parameters to the closure, so they are both available for use inside the closure. You can also choose your own labels for the parameters (index and color have been used in the example here).

Sign up to request clarification or add additional context in comments.

5 Comments

That's what I was looking for. That lets me use the array index as the item id, so the elements don't have to be unique. What magic does id: \.element do in that ForEach?
Answer updated with a proper explanation of enumerated(), sorry it took a while :)
I'm well familiar with the enumerated() function. One thing I was not aware of, though, is that it returned a named tuple with the names .element and .offset. Where is that nuance documented?
@DuncanC Yes, good luck finding it documented, I looked for it too but didn't see it anywhere. But code completion in Xcode gives you the names.
It's a little confusing to use the default tuple names in the keypath and named parameters inside the closure.
2

The type of the selection parameter and the tag of the ForEach must match. Because you're using id: \.self, the items are tagged with a String.

So:

struct ContentView: View {
    @State private var colors = ["Red", "Green", "Blue"]
    @State private var selectedColor: String = "Red"
    
    var body: some View {
        VStack {
            HStack {
                Spacer().frame(width: 30)
                Picker("What is your favorite color?", selection: $selectedColor) {
                    ForEach(colors, id: \.self) { aValidColor in
                        Text(aValidColor)
                    }
                }
                Spacer().frame(width: 30)
            }
            .pickerStyle(.segmented)
            Spacer().frame(height: 30)
            Text("Value: \(selectedColor)")
        }
    }
}

If you really want to use indices, include the tag:

struct ContentView: View {
    @State private var colors = ["Red", "Green", "Blue"]
    @State private var selectedColor: Int = 0
    
    var body: some View {
        VStack {
            HStack {
                Spacer().frame(width: 30)
                Picker("What is your favorite color?", selection: $selectedColor) {
                    ForEach(Array(colors.enumerated()), id: \.element) { (index, item) in
                        Text(item).tag(index)
                    }
                }
                Spacer().frame(width: 30)
            }
            .pickerStyle(.segmented)
            Spacer().frame(height: 30)
            Text("Value: \(colors[selectedColor])")
        }
    }
}

2 Comments

That works, but it requires that the array of strings be unique. @BenzyNeez solution lets me use an Integer array index as the selection index, which is safer given that I'm ultimately going to be building an array of Structs that the user can edit, and can't guarantee that the items in the array will be unique.
Okay, but keep in mind that if the items can be changed, then using the index as an ID is not safe. See Demystify SwiftUI

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.