12

I'm trying to add a callback to a SwiftUI picker but can't get it to execute. The didSet apparently does not execute when picker value changes. Here's what I've tried so far:

struct ContentView : View {
    @State private var picked: Int = 0 {didSet{print("here")}}
    var someData = ["a", "b", "c"]
    var body: some View {

        VStack {
            Picker(selection: $picked,
                   label: Text("")) {
                    ForEach(0 ..< someData.count)     {Text(self.someData[$0]).tag($0)}
            }
            .pickerStyle(.wheel)
            Text("you picked: \(someData[picked])")
        }
    }
}
Fateh Khan
  • 71
  • 6
slicerdicer
  • 155
  • 1
  • 10

3 Answers3

19

Code updated to beta 6

The @State variable will never execute didSet, because it does not change. What does change is the wrapped value. So the only way I can think to tap it, is by putting a second binding in the middle, to serve as an intermediary, and pass the value along. This way, you can put your callback there.

struct ContentView : View {
    @State private var picked: Int = 0

    var someData = ["a", "b", "c"]
    var body: some View {

        let p = Binding<Int>(get: {
            return self.picked
        }, set: {
            self.picked = $0

            // your callback goes here
            print("setting value \($0)")
        })


        return VStack {
            Picker(selection: p,
                   label: Text("")) {
                    ForEach(0 ..< someData.count) {
                        Text(self.someData[$0]).tag($0)
                    }
            }
            .pickerStyle(WheelPickerStyle())
            Text("you picked: \(someData[picked])")
        }
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • DOESNT COMPILE: Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type – jimijon Aug 27 '19 at 14:40
  • This works only half way. I can seem to make it work coming back. Say you want to convert the letter "b" passed in via binding and make that the selected button. I can't make it work – jimijon Oct 28 '19 at 19:36
  • @jimijon that is a trivial problem, not related to SwiftUI: Simply look for the item index in the array: `self.picked = self.someData.firstIndex(of: "b") ?? 0` – kontiki Oct 29 '19 at 12:09
  • It's not. Try to pass the binding to view after it's been set. How does one set the binding: @Binding alphabetLetter:String. ? You need to then set self.picked to self.someData.first... this causes then the infinite loop – jimijon Oct 29 '19 at 13:40
  • Uau nice solution, works great! I needed this to update Binding variable coming from Parent View(s). – sabiland Jan 23 '20 at 11:13
3

you can use ObservableObject to workaround it


import SwiftUI

class PickerModel:ObservableObject{
    @Published var picked: Int = 0 {didSet{print("here")}}
}

struct Picker_Callback: View {
   @ObservedObject var pickerModal = PickerModel()
    var someData = ["a", "b", "c"]
    var body: some View {

        VStack {
            Picker(selection: self.$pickerModal.picked,
                   label: Text("")) {
                    ForEach(0 ..< someData.count)     {Text(self.someData[$0]).tag($0)}
            }
            //.pickerStyle(.wheel)
            Text("you picked: \(someData[self.pickerModal.picked])")
        }
    }
}

springday
  • 61
  • 1
  • 3
2

You can't use the didSet modifier on a variable with the @State property wrapper.

The State wrapper tells SwiftUI that the wrapped value must be managed by the SwiftUI framework itself. This means that the view doesn't actually contains the value, it only contains a State (which is immutable) that provide some kind of reference to the wrapped value.

Actually, you should forget about the didSet callback inside a SwiftUI View. The View protocol requires a body, which is not marked as mutating (SwiftUI views are struct). So a View is basically immutable.

rraphael
  • 10,041
  • 2
  • 25
  • 33