SwiftUI - Picker Binding not Working :-(

I am trying to bind a Picker, but it's not working. I create a BindableObject, an instance of Settings, add it to the environment using environmentObject() in SceneDelegate, and address it in the View using @EnvironmentObject


struct ContentView : View {
    var favoriteFoods = ["Tofu", "Seitan", "Nilla Wafers", "Avocado Toast"]
    
    @EnvironmentObject var settings : Settings

     // meanwhile, inside var body: some View ...
     Picker(selection: $settings.favoriteFoodChoice, label: Text("Favorite Food")){
           ForEach(self.favoriteFoods.identified(by: \.self)){ food in
                 Text(food)
           }
     }


Here's what my Settings looks like:


class Settings : BindableObject {
    
    var didChange = PassthroughSubject<void,never>()
    
    var favoriteFoodChoice:Int {
        willSet {
            print("Favorite Food Choice will be \(newValue)")
            didChange.send()
            print("Favorite Food Choice: \(favoriteFoodChoice)")// never changes when I select a different food.
        }
        didSet {
            print("Favorite Food Choice was \(oldValue)")
            didChange.send()
            print("Favorite Food Choice: \(favoriteFoodChoice)")// never changes when I select a different food.
        }
    }
    
    init(favoriteFoodChoice:Int) {
        self.favoriteFoodChoice = favoriteFoodChoice
    }
    
    convenience init(){
        self.init(favoriteFoodChoice:0)
    }
    
}


When I change the selected food, I see output from print(), but favoriteFoodChoice stays the same. Isn't it supposed to change?? Or am I misunderstanding how binding works with Pickers?


Any help would be appreciated!

Accepted Reply

I should be embarrassed to post from memory, since I am unable to test code (posting from my phone), but I think if you remove your event handler (didChange) for willSet, and use only didSet, you'll get your expected result. didChange, as I understand it, is the notification to swiftUI to update the environment...there may be something wonky in sending didChange before and after a data change, but I'm unsure, as it's hard to peek under the hood of some of the less documented stuff.


edit:

Here is your code... modified to function as I think you expect:

struct ContentView: View {
    var favoriteFoods = ["Tofu", "Seitan", "Nilla Wafers", "Avocado Toast"]

    @EnvironmentObject var settings: Settings
    var body: some View {
        HStack {
            Text("Picker: ")
            Picker(selection: $settings.favoriteFoodChoice, label: Text("Favorite Food")){
                ForEach(0 ..< favoriteFoods.count) {
                    Text(self.favoriteFoods[$0]).tag($0)
                }
            }
        }
    }
}

class Settings: BindableObject {
    
    var didChange = PassthroughSubject<void, never="">()

    var favoriteFoodChoice: Int = 0 {
        willSet {
            print("Favorite Food Choice will be \(newValue)")
            //didChange.send()
            print("Favorite Food Choice: \(favoriteFoodChoice)") // never changes when I select a different food.
        }
        didSet {
            print("Favorite Food Choice was \(oldValue)")
            didChange.send()
            print("Favorite Food Choice: \(favoriteFoodChoice)") // never changes when I select a different food.
        }
    }

}

I killed the extra didChange notification, and corrected the syntax of your Picker control to show labels in the picker control (and shortend the syntax of the ForEach loop...just to see if I could remember how, I'm novice with Swift). I also moved the variable init into its declaration and removed the two init() calls, in essence setting the control to the first available selection.

Replies

I should be embarrassed to post from memory, since I am unable to test code (posting from my phone), but I think if you remove your event handler (didChange) for willSet, and use only didSet, you'll get your expected result. didChange, as I understand it, is the notification to swiftUI to update the environment...there may be something wonky in sending didChange before and after a data change, but I'm unsure, as it's hard to peek under the hood of some of the less documented stuff.


edit:

Here is your code... modified to function as I think you expect:

struct ContentView: View {
    var favoriteFoods = ["Tofu", "Seitan", "Nilla Wafers", "Avocado Toast"]

    @EnvironmentObject var settings: Settings
    var body: some View {
        HStack {
            Text("Picker: ")
            Picker(selection: $settings.favoriteFoodChoice, label: Text("Favorite Food")){
                ForEach(0 ..< favoriteFoods.count) {
                    Text(self.favoriteFoods[$0]).tag($0)
                }
            }
        }
    }
}

class Settings: BindableObject {
    
    var didChange = PassthroughSubject<void, never="">()

    var favoriteFoodChoice: Int = 0 {
        willSet {
            print("Favorite Food Choice will be \(newValue)")
            //didChange.send()
            print("Favorite Food Choice: \(favoriteFoodChoice)") // never changes when I select a different food.
        }
        didSet {
            print("Favorite Food Choice was \(oldValue)")
            didChange.send()
            print("Favorite Food Choice: \(favoriteFoodChoice)") // never changes when I select a different food.
        }
    }

}

I killed the extra didChange notification, and corrected the syntax of your Picker control to show labels in the picker control (and shortend the syntax of the ForEach loop...just to see if I could remember how, I'm novice with Swift). I also moved the variable init into its declaration and removed the two init() calls, in essence setting the control to the first available selection.

Thanks so much, m_bedwell. I copied your code, and it worked. I then uncommented out the redundant didChange.send(), cleaned the project, and it still worked. However, when I restored my old Picker code:


ForEach(self.favoriteFoods.identified(by: \.self)){ food in  
                 Text(food)  
    } 

it crashed with EXC_BAD_INSTRUCTION (which it did not do previously). So, I'm not quite sure what's going on with ForEach() but I'm sticking with your (and hackingwithswift.com's) way of doing things. 🙂


Thanks again for you assistance, it is much appreciated!

A single dimensional string array shouldn’t need the ‘identified by’ clause I don’t think, but I’m not able to test or research that statement currently. I’m also not sure it would solve your error to remove it. ForEach seems to want to behave like a standard for loop, but doesn’t always comply. I’m looking forward to detailed docs when they become available, rather than just the api stubs we have for most of this right now.

This is not working anymore. Any other solution?

Sorry for late opening closed discussion. I had same problem and notice that non functioning code do not have .tag(index) after Picker element, but answer had it. I tested it and with .tag(index) my code worked. I think that can be solution

Thanks @int0xCC that worked for me. See the documentation on tag(_:) for more information.

https://developer.apple.com/documentation/swiftui/form/tag(_:)

This might not be your problem. But I spend 3 hours on a problem exactly like this last night. And re-built an example line by line until I learned:

Foreach in a picker does not work with the old code I had copy/pasted using @Environment(\.presentationMode) var presentationMode Let me say it again because it's so crazy to me. Simply Declaring that variable, and not using it, breaks picker+forEach's. Which means it has nothing to do with any index you as a developer explicitly used. Ref: https://developer.apple.com/documentation/swiftui/presentationmode.

I was able to easily reproduce the following crash when navigating between two views: Swift/ContiguousArrayBuffer.swift:600: Fatal error: Index out of range

Instead replacing it with "https://developer.apple.com/documentation/swiftui/environmentvalues/ispresented". Or in my case I was able to do the navigation ( a submit/cancel form ) in a different/standard way

Dear Apple, Tried in xcode 5.2 and 5.3, please add a massive deprecation warning when people attempt to use that code.