SwiftUI makes it less than straightfowrad to let a View somewhere deep in your hiearachy add menu commands. But using SwiftUI’s focus support, it can be made a litle less painful.

I’ve come up with the following technique. This article assumes you already know how to create Commands and use Focused Values in SwiftUI (there are plenty of good resources for commands and focus online). No doubt this technique can be improved, and I’d love constructive feedback.

The gist of it is to store a reference to a closure you supply in a focused value.

The source code is part of this project.

Usage

Imagine an “Object” menu with commands for “Bring to Front” and “Send to Back.”

Example menu with Bring to Front and Send to Back commands, and a window with simple objects.

We want to be able to handle those commands with code like this:

struct
ContentView: View
{
    @State  var items               =   Item.testItems
    @State  var selection           =   [Item.ID]()
    
    var
    body: some View
    {
        RepositionableItemContainer(self.$items, selection: self.$selection)
        { inItem in
            ItemView(item: inItem)
        }
        .onBringToFront(disabled: self.selection.isEmpty)
        {
            let items = self.items.filter { self.selection.contains($0.id) }
            self.items.removeAll { self.selection.contains($0.id) }
            self.items.append(contentsOf: items)
        }
        
    }
}

Implementation

To enable the usage above, we want to store the closure as a focused value (we’ll also store a boolean indicating if the command should be enabled or not). To do that we have to create a key for the value, which will be a tuple of (Bool, () -> Void):

Note: For simplicity, I’ll only show the implementation for one of the two commands.

struct
BringToFrontCommandKey : FocusedValueKey
{
    typealias Value     =   (Bool, () -> Void)
}

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

A View extension provides the convenience for implemeting the result of the command:

extension
View
{
    func
    onBringToFront(disabled: Bool = false, perform: @escaping () -> ())
        -> some View
    {
        self.focusedSceneValue(\.bringToFrontCommand, (disabled, perform))
    }
}

We also need to create the menu commands themselves. Here it’s done as a separate Commands struct, but that’s not required.

struct
ObjectMenuCommands : Commands
{
    var
    body: some Commands
    {
        CommandMenu("Object")
        {
            Button("Bring to Front")
            {
                self.bringToFrontCommand?.1()
            }
            .disabled(self.bringToFrontCommand?.0 ?? false)
        }
    }
    
    @FocusedValue(\.bringToFrontCommand)    private var     bringToFrontCommand
}

The commands are added to the application in the top-level scene(s):

@main
struct
RepositionableViewsApp: App
{
    var
    body: some Scene
    {
        Window("Window", id: "widow") {  }
        .commands
        {
            ObjectMenuCommands()
        }
    }
}

Future Enhancements

The disabled parameter really ought to be an autoclosure, so that the client has the option of passing a closure to determine if the menu item should be disabled (e.g. in this case, if the selected item(s) is already at the front). I tried to do that for this article, but I couldn’t get the syntax quite right, so I’ve abandoned that for now.

I’m also working on a Swift macro to automate all this boilerplate. I’m not sure how to handle certain tedious aspects. It will probably require more than one macro, unfortunately.