BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

ForEach and Picker and the values it's passing

Forums > 100 Days of SwiftUI

If I understand ForEach correctly when used in a Picker, the value ForEach passes to the Picker is actually the row value and not the number in the range. So if like below, I have a range of 10..<21, when the ForEach is on the value of 12, it's only passing the value of 2 to the Picker in the selection binding, because it's on row 2.

struct ContentView: View {
    @State private var selection = 1

    var body: some View {
        Picker("Pick a value", selection: $selection) {
            ForEach(10..<21) { rowNumber in
                Text("Row number: \(rowNumber) selection:\(selection)")
            }
        }

    }
}

3      

Elsewhere I may have written:

The next time you see a ForEach. Ask yourself three questions.
What is the collection of things is this processing?
What makes each thing unique?
What is the ForEach factory making?

    var body: some View {
        Picker("Pick a value", selection: $selection) {
            ForEach(10..<21) { rowNumber in
                Text("Row number: \(rowNumber) selection:\(selection)")
            }
        }
    }
  1. What is the collection of things you are processing?
    A: You are processing ten integer objects, numbered 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, and 20
  2. What makes each thing unique.
    A: Each integer in this collection is unique. (Though you do not specify this in your ForEach declaration.)
  3. What is the ForEach factory making?
    A: The factory is making ten Text objects. Each object holds the provided integer, and the struct's selection variable.

You may have confused yourself by naming the incoming variable: rowNumber. It's not a row number. It's really just one object in your collection. Try calling it collectionObject, or integerObject. That rowNumber is not used by the Picker. It is used by the factory to create a Text object!

The ForEach must be able to process objects conforming to the RandomAccessCollection protocol. It seems that ranges conform to this protocol without modification. Conseqently, the index of the item in the range becomes the object's id by default. Please correct me if this is not the correct interpretation.

When those ten Text objects are created, they are fed into the Picker structure. Each object in the picker is identified with an id. The object id is the value used as the $selction binding.

Whenever you DO click on one of the Text objects, the Picker does its thing and updates the $selection binding using the object's id.

3      

To be clear, you asked:

If I understand ForEach correctly when used in a Picker, the value ForEach passes to the Picker is actually the row value and not the number in the range.

The answer is "No".

In the internal guts of a Picker struct, the picker keeps an array of Views that it displays. You interact with the Picker and you click on one of the ten Views your ForEach created. Each of these views is unique identified using its id.

The Picker decides which element in its internal array you clicked, and it uses the object's id as the value for the binding.

This contrived example may help. Copy this into Playgrounds.

import SwiftUI
import PlaygroundSupport  // to display a picker in Playgrounds
struct ContentView: View {
    @State private var selection = ""
    let months = ["January", "February", "March", "April", "May"]

    var body: some View {
        Picker("Pick a value", selection: $selection) {
        // Notice how the id is a capitalized version of the month.
            ForEach(months, id: \.self.localizedUppercase) { item in
                Text("\(item) \(selection)") 
                // the selection seems to be the value identified as the item's id. 
                // it's not the value in the array.
            }
        }
        .frame(width: 300, height: 300)
        .pickerStyle(.wheel)
    }
}
PlaygroundPage.current.setLiveView(ContentView())

As a test, add the month "MARCH" (all caps) to the end of the array. I think you'll notice that the values in the array are different you'll have "March" and "MARCH" in the picker. But both of these items will have an id of "MARCH". This is confusing, to say the least. This is probably why it's best that the id in ForEach is unique, like a UUID().

3      

Thanks for the clarification. 👍

So with a text range this seems to be true:

Whenever you DO click on one of the Text objects, the Picker does its thing and updates the $selection binding. The selection variable holds the index of the Text object you clicked.

but with array of text items the selection variable holds the actual value at the index. Which seems a little confusing.

struct ContentView: View {
    @State private var selection = ""
    let months = ["January", "February", "March", "April", "May"]

    var body: some View {
        Picker("Pick a value", selection: $selection) {
            ForEach(months, id: \.self) { item in
                Text("\(item) \(selection)")
            }
        }

    }
}

3      

Thanks for testing my answer!

Please see my revised answers for an update. It's not using the value at the array's index, it seems to be using the value supplied as the object's id within the ForEach structure.

3      

After reading your update, I tried something different with a struct that has an id field and it works just like you said. It's using the object's id. So now, I understand why when using a range or an array things appear the way they do.

struct Fellowship: Identifiable {
    let id: Int
    let name: String
    let race: String
    var hasRing: Bool = false
}

struct ContentView: View {
    @State private var selection = 0

    var fellowship: [Fellowship] = [
        Fellowship(id: 1, name: "Frodo", race: "Hobbit", hasRing: true),
        Fellowship(id: 2, name: "Sam", race: "Hobbit"),
        Fellowship(id: 3, name: "Pippin", race: "Hobbit"),
        Fellowship(id: 4, name: "Merry", race: "Hobbit"),
        Fellowship(id: 5, name: "Gandalf", race: "Istari"),
        Fellowship(id: 6, name: "Aragorn", race: "Human"),
        Fellowship(id: 7, name: "Legolas", race: "Elf"),
        Fellowship(id: 8, name: "Gimli", race: "Dwarf"),
        Fellowship(id: 9, name: "Boromir", race: "Human")
    ]

    var body: some View {
        Picker("Pick a value", selection: $selection) {
            ForEach(fellowship) { item in
                Text("\(item.name)")
            }
        }
        Text("\(selection)")

    }
}

Also, I didn't know you could display Pickers in the playgrounds so that's cool. Though, I had to remove this line to get your example to work in mine: .pickerStyle(.wheel) It didn't like the .wheel modifier. I got wheel is unavailable in macOS.

3      

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.