SwiftUI Picker onChange or equivalent?

I want to change another unrelated @State variable when a Picker gets changed, but there is no onChanged and it's not possible to put a didSet on the pickers @State. Is there another way to solve this?


Solution 1:

Here is what I just decided to use... (deployment target iOS 13)

struct MyPicker: View {
    @State private var favoriteColor = 0

    var body: some View {
        Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
            Text("Red").tag(0)
            Text("Green").tag(1)
            Text("Blue").tag(2)
        }
    }

    func colorChange(_ tag: Int) {
        print("Color tag: \(tag)")
    }
}

Using this helper

extension Binding {
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
        return Binding(
            get: { self.wrappedValue },
            set: { selection in
                self.wrappedValue = selection
                handler(selection)
        })
    }
}

EDIT:

If your deployment target is set to iOS 14 or higher -- Apple has provided a built in onChange extension to View, which can be used like this instead (Thanks @Jeremy):

Picker(selection: $favoriteColor, label: Text("Color")) {
    // ..
}
.onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }

Solution 2:

First of all, full credit to ccwasden for the best answer. I had to modify it slightly to make it work for me, so I'm answering this question hoping someone else will find it useful as well.

Here's what I ended up with (tested on iOS 14 GM with Xcode 12 GM)

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

    var body: some View {
        Picker(selection: $selection, label: Text("Some Label")) {
            ForEach(0 ..< 5) {
                Text("Number \($0)") }
        }.onChange(of: selection) { _ in
            print(selection)
        }
        
    }
}

The inclusion of the "_ in" was what I needed. Without it, I got the error "Cannot convert value of type 'Int' to expected argument type '()'"

Solution 3:

I think this is simpler solution:

@State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]

// USE this if needed to notify parent
@Binding var notifyParentOnChangeIndex: Int    

var body: some View {

   let pi = Binding<Int>(get: {

            return self.pickerIndex

        }, set: {

            self.pickerIndex = $0

            // TODO: DO YOUR STUFF HERE
            // TODO: DO YOUR STUFF HERE
            // TODO: DO YOUR STUFF HERE

            // USE this if needed to notify parent
            self.notifyParentOnChangeIndex = $0

        })

   return VStack{

            Picker(selection: pi, label: Text("Yolo")) {
                ForEach(self.yourData.indices) {
                    Text(self.yourData[$0])
                }
            }
            .pickerStyle(WheelPickerStyle())
            .padding()

   }

}