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.”
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.