Is there a way to call a function when a SwiftUI Picker selection changes?
Asked Answered
I

2

24

I would like to call a function when selectedOption's value changes. Is there a way to do this in SwiftUI similar to when editing a TextField?

Specifically, I would like to save the selected option when the user changes the selectedOption.

Here is my picker:

struct BuilderPicker: View {
    let name: String
    let options: Array<String>
    @State var selectedOption = 0
    var body: some View {
        HStack {
            Text(name)
                .font(.body)
                .padding(.leading, 10)
            Picker(selection: $selectedOption, label: Text(name)) {
                ForEach(0 ..< options.count) {
                    Text(self.options[$0]).tag($0)
                }
            }.pickerStyle(SegmentedPickerStyle())
                .padding(.trailing, 25)
        }.onTapGesture {
            self.selectedOption = self.selectedOption == 0 ? 1 : 0
        }
            .padding(.init(top: 10, leading: 10, bottom: 10, trailing: 0))
            .border(Color.secondary, width: 3)
            .padding(.init(top: 0, leading: 15, bottom: 0, trailing: 15))
            .font(.body)
    }

}

I’m still new to SwiftUI and would love some help. Thanks!

Indigotin answered 3/11, 2019 at 0:33 Comment(0)
I
24

If the @State value will be used in a View, you don't need extra variable name

  struct BuilderPicker: View {
// let name: String = ""
 let options: Array<String> = ["1", "2","3","4","5"]
 @State var selectedOption = 0
 var body: some View {
    HStack {
        Text(options[selectedOption])
            .font(.body)
            .padding(.leading, 10)
        Picker(selection: $selectedOption, label:    Text(options[selectedOption])) {
            ForEach(0 ..< options.count) {
                Text(self.options[$0]).tag($0)
            }
        }.pickerStyle(SegmentedPickerStyle())
            .padding(.trailing, 25)}
//        }.onTapGesture {
//            self.selectedOption = self.selectedOption == 0 ? 1 : 0
//        }
        .padding(.init(top: 10, leading: 10, bottom: 10, trailing: 0))
        .border(Color.secondary, width: 3)
        .padding(.init(top: 0, leading: 15, bottom: 0, trailing: 15))
        .font(.body)
    }


 }

If you need separated operation on the @State, the simplest way is adding one line : onReceive() to the view.

  HStack {
        Text("")
            .font(.body)
            .padding(.leading, 10)
        Picker(selection: $selectedOption, label: Text("")) {
            ForEach(0 ..< options.count) {
                Text(self.options[$0]).tag($0)
            }
        }.pickerStyle(SegmentedPickerStyle())
            .padding(.trailing, 25)}
  //        }.onTapGesture {
 //            self.selectedOption = self.selectedOption == 0 ? 1 : 0
 //        }
        .padding(.init(top: 10, leading: 10, bottom: 10, trailing: 0))
        .border(Color.secondary, width: 3)
        .padding(.init(top: 0, leading: 15, bottom: 0, trailing: 15))
        .font(.body)
        .onReceive([self.selectedOption].publisher.first()) { (value) in
            print(value)
    }
Idea answered 3/11, 2019 at 2:18 Comment(12)
the reason didSet isn't working for you is because you commented out the onTapGesture. I'm trying your idea now.Indigotin
Don't use extra gesture if everything on @State var is built in.Idea
This works, but it feels like there should be a better way to do this. Thanks!Indigotin
This works, but "[self.selectedOption].publisher.first()" w00t ?:)Beisel
Is [self.selectedOption].publisher.first() Public API? It doesn't feel like Public API, but gosh darnit it works.Dynast
You can use ‘Just(self.selectedOption)’Idea
What is purpose of brackets in '[self.selectedOption]'?Garter
The purpose of the brackets is to put selectedOption into an Array. Once in an array, then you can call publisher on it as it conforms to Sequence protocol.Isochroous
Wow, thanks so much @Idea -- using .onReceive(Just(...)) {value in } is fantastically useful! I'm finally able to run code in a child view when a variable passed from its parent changes.Member
It's possible to enter an infinite loop with this if you update an ObservedObject in the callback. You'll know really quick as it will lock the ui and your memory will spike.Harday
Can confirm that this is a dangerous approach if you update an ObservedObject in the callback.Haematocele
Best answer. onReceive() is the key.Laurustinus
H
6

The previous solution will end up in an infinite loop if you update an ObservedObject in the callback since .onReceive is also called when the View got rendered.

→ A better approach is to use a .onChange method on the Binding itself:

Picker(selection: $selectedOption.onChange(doSomething), label: Text("Hello world")) {
        // ...
    }

To do so you need to write an extension for Binding like described here.

Haematocele answered 4/8, 2020 at 20:59 Comment(1)
You're definitely correct about the loop. However, rather than use an extension on Binding, you can just use the .onChange(of: perform:) modifier on the Picker itself, where of: is your @State variable and perform: is the action you want to perform.Fontenot

© 2022 - 2024 — McMap. All rights reserved.