SwiftUI Picker not trigger `onChange` first time when inside Menu in MacOS

code

import SwiftUI

@main
struct swiftuiTestApp: App {
  @State private var value = "a";
  
  var body: some Scene {
    WindowGroup {
      Menu("menu") {
        Picker("", selection: $value) {
          Text("A").tag("a")
          Text("B").tag("b")
          Text("C").tag("c")
        }
        .pickerStyle(.inline)
        .onChange(of: value) { oldValue, newValue in
          print("onChange", newValue)
        }
      }
    }
  }
}
  1. on MacOS, onChange is not triggered when an option is selected for the first time, but subsequent option selections are triggered。
  2. on MacOS, when Picker is not inside Menu, it works fine
  3. on iOS, both works fine with inside or not inside menu
Answered by DTS Engineer in 802669022

@chijun89 I tested this out and it looks like a bug on macOS! Can you please file a bug report at https://feedbackassistant.apple.com and add the code snippet. Please paste the FB number here.

Thanks

I displayed the value on Picker title, it turns out that first time select another option , the value did changed, but the onChange is not trigger

Picker("value \(value)", selection: $value) {
     //...
}

if I move onChange to Menu, the problem disappeared

Menu("menu") {
        Picker("value \(value)", selection: $value) {
          Text("A").tag("a")
          Text("B").tag("b")
          Text("C").tag("c")
        }
        .pickerStyle(.inline)
      }
      .onChange(of: value) { oldValue, newValue in
        print("onChange", newValue)
      }

but my goal is to encapsulate Picker into a controlled component similar to React, so I have to put state inside menu

import SwiftUI

struct TBSelect: View {
  let title: String;
  let value: String;
  let options: Array<TBSelectOption>;
  let tip: String;
  let onChange: (_ value: String) -> Void;
  
  @State private var innerValue: String;
  
  struct TBSelectOption {
    let title: String;
    let value: String;
    let systemImage: String?;
  }
  
  init(title: String, value: String, options: Array<TBSelectOption>, tip: String = "", onChange: @escaping (_ value: String) -> Void) {
    _innerValue = State(initialValue: value);
    
    self.title = title
    self.value = value
    self.options = options
    self.tip = tip
    self.onChange = onChange;
  }
  
  var body: some View {
    Picker(title, selection: $innerValue) {
      ForEach(options, id: \.value) { option in
        if let systemImage = option.systemImage {
          Label(option.title, systemImage: systemImage)
            .tag(option.value)
        } else {
          Text(option.title)
            .tag(option.value)
        }
      }
    }
    .help(tip)
    .onChange(of: innerValue) { oldValue, newValue in
      onChange(newValue)
    }
  }
}

There is effectively something weird here. At first change, Picker does not react.

That seems to be an old bug (or undocumented behaviour): https://forums.developer.apple.com/forums/thread/688685

In addition of the solution you found, I implemented the Spacer().onChange trick and it works:

struct swiftuiTestApp: App {
    @State private var value = "a"

    var body: some Scene {
        WindowGroup {
            Menu("menu \(value)") {
                Picker("", selection: $value) {
                    Text("A").tag("a")
                    Text("B").tag("b")
                    Text("C").tag("c")
                }
                .pickerStyle(.inline)
                .onChange(of: value) { oldValue, newValue in
                    print("onChange", oldValue, newValue)
                }
            }
            Spacer().onChange(of: value) { oldValue, newValue in
                print("Spacer onChange", oldValue, newValue)
            }
        }
    }
}

Here is the log:

Spacer onChange a b      // First change does not trigger picker onChange, but triggers Spacer()
Spacer onChange b c      // next change does trigger picker onChange, and Spacer()
onChange b c

You should file a bug report.

I think there is something wrong with Menu in MacOS, toogle insdie Menu has the same problem

struct swiftuiTestApp: App {
  @State private var val = false;
  
  var body: some Scene {
    WindowGroup {
      Menu("menu") {
        Toggle(isOn: $val) {
          Text("open")
        }.onChange(of: val) { oldValue, newValue in
          print("onChange", newValue)
        }
      }
    }
  }
}
Accepted Answer

@chijun89 I tested this out and it looks like a bug on macOS! Can you please file a bug report at https://feedbackassistant.apple.com and add the code snippet. Please paste the FB number here.

Thanks

A week has passed and I haven't received any feedback from Apple.

https://feedbackassistant.apple.com/feedback/15061288

SwiftUI Picker not trigger `onChange` first time when inside Menu in MacOS
 
 
Q