20

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!

Eli Front
  • 695
  • 1
  • 8
  • 28

2 Answers2

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)
    }
E.Coms
  • 11,065
  • 2
  • 23
  • 35
3

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.

ixany
  • 5,433
  • 9
  • 41
  • 65
  • 5
    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. – Bill Abt Mar 10 '21 at 01:35