Cannot select submenus from menubar using SwiftUI on macOS14

112 views Asked by At

I'm trying to add a submenu off one of my menu items using the following code:

import SwiftUI

@main
struct MenuAppApp: App 
{
    var body: some Scene 
    {
        WindowGroup 
        {
            ContentView()
        }
        .commands
        {
            MyMenu()
        }
    }
}

struct MyMenu: Commands
{
    let array = ["Submenu 1", "Submenu 2", "Submenu 3"]
    @State var selected = "Submenu 1"
    
    var body: some Commands
    {
        CommandMenu("My Menu")
        {
            Button("First Thing")
            {
                print ("First Thing")
            }
            
            Divider()
            Picker(selection: $selected, label: Text("Second Thing"))
            {
                ForEach (Array(array.enumerated()), id: \.element)
                { index, item in
                    Text(item).tag(item)
//                    Button(item)
//                    {
//                        print("Button \(item) selected")
//                    }
                }
            }
            .onChange(of: selected)
            {
                print ("OnChange \(selected)")
            }
            
        }
    }
}

struct ContentView: View 
{
    var body: some View 
    {
        VStack 
        {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

This renders the menu correctly but it will not execute the onChange portion of the menu until after the second time of choosing the menu.

Select Second Thing → Submenu 2 Nothing happens Click once anywhere on the menu bar It will then print "On Change Submenu 2"

You have to reselect any menu in the menu bar before the action will be executed.

If I uncomment the button code then button action never gets executed.

I also tried moving the selected State var to the top level and then using a binding. This did exactly the same.

How should I do this properly?

2

There are 2 answers

0
vadian On BEST ANSWER

A possible solution is a custom Binding between selected and the picker

struct MyMenu: Commands
{
    let array = ["Submenu 1", "Submenu 2", "Submenu 3"]
    @State private var selected = "Submenu 1"
    
    var body: some Commands
    {
        let secondThingBinding = Binding {
            selected
        } set: { newValue in
            selected = newValue
            print ("OnChange \(newValue)")
        }

        CommandMenu("My Menu")
        {
            Button("First Thing")
            {
                print ("First Thing")
            }
            
            Divider()
            Picker(selection: secondThingBinding, label: Text("Second Thing"))
            {
                ForEach (Array(array.enumerated()), id: \.element)
                { index, item in
                    Text(item).tag(item)
//                    Button(item)
//                    {
//                        print("Button \(item) selected")
//                    }
                }
            }
        }
    }
}
3
Curious Jorge On

This is curious! It looks like the .onChange call’s location is the culprit.

In the code you've given, onChange is (on the Picker) inside the CommandMenu, part of a hierarchy of View instances that only exist while SwiftUI is keeping those views alive. The details of this are not available to us, but from the behaviour we are seeing (that you are only getting the print messages when you click on the menu again), we can infer that SwiftUI probably discarded the entire MyMenu view hierarchy as soon as the "SubMenu X" has been selected. This includes the .onChange, and therefore the code that would print your messages.

The solution is very simple. Just put the .onChange (and the selected state variable) outside the Commands struct (I would recommend on the WindowGroup), and use a binding in your MyMenu struct to access selected. When the lifetime of the state variable and the .onChange closure are tied to a persistent View, the problem goes away. I have tried this and it works for me.