SwiftUI FocusedValue, macOS Menus, and the Responder Chain

On macOS, you can use the SwiftUI FocusedValue API to achieve a behavior similar to that of the Responder Chain, including autoenabling menu items.

SwiftUI FocusedValue, macOS Menus, and the Responder Chain

Anyone coming into SwiftUI development with an AppKit background will undoubtedly be disappointed by its current set of capabilities on macOS. As any cross-platform framework, SwiftUI makes compromises, and, arguably being iOS-first, it makes the most significant sacrifices on the Mac by attempting to reduce the set of the available functionality to the subset supported by iOS and UIKit.

Luckily, in a lot of cases you can interoperate with AppKit with little overhead, accessing the application lifecycle, windows, and representing your NSViews in SwiftUI. However, in a few cases, the mapping is less than straightforward.

Imagine the following use case in a SwiftUI application. We have an app with multiple scenes, implemented using a WindowGroup. Each window has a sidebar and some selection state. We want to add the following menu commands:

  • A menu item to set the sidebar selection in the key window.
  • Menu items to edit and delete selected elements in the key window, automatically disabled when no selection is available.

Supporting this use case requires us to have access to 2 concepts: the mapping of the key window to the SwiftUI view hierarchy, and the Responder Chain to autoenable items.

Automatic Menu Enabling

NSMenu makes it straightforward to set up menu items that are automatically enabled using the Responder Chain and responded to by some view potentially deep inside the hierarchy – without having to maintain the reference to the responder. Simply specify the selector with a nil target, set autoenablesItems, and the framework will automatically perform validation when each user event is processed.

The same behavior is still achievable using SwiftUI – using the relatively new FocusedValue abstraction. The mechanism is most similar to the Environment, and while it is similar to the Responder Chain, there are a few notable differences.

Potential Solutions

We could consider the following solution, especially if we’re coming from a single-window model of thinking that is prevalent on iOS:

  • Store the view model – or an equivalent state container – in the App;
  • Propagate the view model to subviews using the Environment;
  • Use the commands modifier on the scene to add menu items;
  • In their actions, set the state of the view model.

This works at first glance, but quickly falls apart in more complex use cases. If you order a new window from the window group, you’ll notice that all state is synchronized between them, including the sidebar selection or any element selection.

Great Mac apps support multiple windows.

What the user expects to happen (and what will also work well in the newer windowing models like iPadOS and visionOS) is letting each window own its view model / state, and use the Environment to propagate it down the view hierarchy.

But how do we enable the menu items?

FocusedValue as a Responder Chain

FocusedValue is a SwiftUI reinvention of the Responder Chain. In the best of SwiftUI traditions, this system handles a lot of things implicitly, making it easy to get started but harder to achieve more customized behavior.

Let’s first handle the use case of changing sidebar selection.

1. Define a key

Let’s say our sidebar selection state is managed by a view model class SidebarModel that contains a nested type called Item to represent individual items.

First, we define a key that will be used to propagate the action.

struct SidebarSelectionCommandKey: FocusedValueKey {
    typealias Value = (SidebarModel.Item) -> Void
}

extension FocusedValues {
    var sidebarSelectionCommand: SidebarSelectionCommandKey.Value? {
        get { self[SidebarSelectionCommandKey.self] }
        set { self[SidebarSelectionCommandKey.self] = newValue }
    }
}

2. Use FocusedValue property wrapper

To be able to invoke the command, use the FocusedValue property wrapper at the App level.

We can then invoke its Value closure directly in our MenuCommand callback.

@main
struct ResponderChainApp: SwiftUI.App {
    // ...
    
    @FocusedValue(\.sidebarSelectionCommand) private var sidebarSelectionCommand
    
    var body: some Scene {
        WindowGroup(id: "browser") { /* ... */ }
    }
    .commands {
        CommandMenu("New Menu") {
            Button("All") {
                sidebarSelectionCommand?(.all)
            }
            .keyboardShortcut("0")
        }
    }
}

3. Handle the command

We can now use the focusedSceneValue modifier to respond to the command somewhere down the view hierarchy. In this example case, it makes sense to do so inside the View that owns the SidebarModel instance.

.focusedSceneValue(\.sidebarSelectionCommand) { item in
    sidebarModel.selection = item
}

Try out the result, and note that even if you have multiple windows, only the key window will receive the callback. This makes the focus values a suitable abstraction over the Responder Chain.

Autoenable Items

We can now work towards our other goal of being able to dynamically enable and disable our menu items depending on whether a selection is present.

If we can figure out how to propagate the state from a view deep in the hierarchy of the key window to our scene definition, we can apply the disabled modifier to the corresponding command.

Similar to how we’ve been able to define a closure that was defined on a view and invoked by a menu command, we can define a more complex data structure that will encapsulate the item’s enablement state at a given time.

struct SelfEnablingCommand<T> {
    var isDisabled: () -> Bool
    var invocation: (T) -> Void
    
    init(isDisabled: @escaping @autoclosure () -> Bool, invocation: (T) -> Void) {
        self.isDisabled = isDisabled
        self.invocation = invocation
    }
}

It’d also be great to reuse the same menu command to both delete and edit the selected elements, so let’s define an enumeration as well:

enum SelectionOperation {
    case edit
    case delete
}

Now to the familiar piece: define another FocusedValueKey to use the new SelfEnablingCommand.

struct SelectionOperationCommandKey: FocusedValueKey {
    typealias Value = SelfEnablingCommand<SelectionOperation>
}

extension FocusedValues {
    var selectionOperationCommand: SelectionOperationCommandKey.Value? {
        get { self[SelectionOperationCommandKey.self] }
        set { self[SelectionOperationCommandKey.self] = newValue }
    }
}

We can now use the isDisabled closure to compute the enablement state of the menu items. The command group implementation would look like this:

CommandGroup(after: .undoRedo) {
    if let selectionOperationCommand {
        Group {
            Divider()
            
            Button("Edit…") {
                selectionOperationCommand.invocation(.edit)
            }
            .keyboardShortcut("e")
            
            Button("Delete", role: .destructive) {
                selectionOperationCommand.invocation(.delete)
            }
            .keyboardShortcut(.delete)
        }
        .disabled(selectionOperationCommand.isDisabled())
    }
}

Now, some view in the hierarchy should be able to define a response by applying the focusedSceneValue modifier:

.focusedSceneValue(\.selectionOperationCommand, .init(isDisabled: viewModel.selectedElements.isEmpty) { operation in
    // Do something with the operation...
}

Further Reading

To learn more about these topics, I highly recommend you explore the documentation: