Search code examples
macosswiftuimenu

SwiftUI: can you have a menu item with a checkmark and shortcut?


I'm writing a macOS app using SwiftUI, specifically, I'm adding a menu to the menu bar. The menu should have a selectable item -- item can have an optional checkmark in front of it -- and a shortcut. I cannot get it to work. It seems like you can either have a shortcut assigned -- you place a Button in the menu and attach the shortcut to the Button.

Button(String("Test")) {}
  .keyboardShortcut("1")

Or, you can have the item to be selectable -- you place a Button() into a Picker and set up the selection binding and the Button tag correctly.

Picker(selection: $selection) {
  Button(String("Test 1")) {}
    .tag(1)
}.pickerStyle(.inline)

But, not both. As soon as you put a Button with the shortcut into a Picker, the shortcut disappears.

Picker(selection: $selection) {
  Button(String("Test")) {}
    .keyboardShortcut("1") // << ignored
    .tag(1)
}.pickerStyle(.inline)

Am I missing something here or is this a known bug in SwiftUI? macOS 13.4.1, deployment target 12.0


Solution

  • I don't think keyboard shortcuts on a Picker is currently supported. See also these related posts for people with similar situations: 1, 2.

    As the first post says, you can use a bunch of Toggles instead, though you would have to handle updating the states of other toggles yourself, so that no two toggles are on at the same time.

    Example:

    struct ToggleState: Identifiable, Hashable {
        var isOn = false
        let id: Int
        let shortcut: KeyboardShortcut
    }
    
    @State var toggles = [
        ToggleState(isOn: true, id: 0, shortcut: .init("A")),
        ToggleState(id: 1, shortcut: .init("B")),
        ToggleState(id: 2, shortcut: .init("C")),
    ]
    
    var body: some View {
        ForEach(Array(toggles.indices), id: \.self) { i in
            Toggle("\(toggles[i].id)", isOn: Binding(get: {
                toggles[i].isOn
            }, set: { _ in
                for j in toggles.indices {
                    toggles[j].isOn = false
                }
                toggles[i].isOn = true
            }))
                .keyboardShortcut(toggles[i].shortcut)
        }
    }