Accessing the Document in a SwiftUI Menu
You’re making a SwiftUI document-based Mac app. You have a menu item in your app that performs an action on the document. How do you give the menu access to the document?
Use focused values and focused bindings.
Creating a Focused Value
A focused value provides a way for a SwiftUI app to observe values from the focused view or one of its ancestors. In a document-based SwiftUI Mac app, one of the focused view’s ancestors is the document window. By using a focused value your app can access the focused document.
To create a focused value, you must write two pieces of code. The first is a struct that conforms to the FocusedValueKey
protocol. The second is a property for the document as an extension to the FocusedValues
struct.
struct DocumentFocusedValueKey: FocusedValueKey {
typealias Value = Binding<Document>
}
extension FocusedValues {
var document: DocumentFocusedValueKey.Value? {
get {
return self[DocumentFocusedValueKey.self]
}
set {
self[DocumentFocusedValueKey.self] = newValue
}
}
}
The struct creates a type alias for the document’s type, which is usually a binding to your document type.
The document
variable is the value you will use when setting the focused value. The value must match this variable name.
Notice the type of the variable, DocumentFocusedValueKey.Value?
. The first part of the type is the name of the struct you created for the focused value. The second part is the name of the type alias you created in the struct.
Setting a Focused Value
The .focusedValue
modifier sets a focused value.
.focusedValue(\.document, value)
The name of the first argument must match the name of the variable you created in the FocusedValues
extension.
Apple added the .focusedSceneValue
modifier in iOS 15 and macOS 12. This modifier works much better than .focusedValue
, which requires the person using the app to be focused on a text view or text field. If you can require iOS 15 and/or macOS 12, use .focusedSceneValue
.
.focusedSceneValue(\.document, value)
Where do you place the code to set the focused value? Attaching the focused value modifier to the content view in the app file is the most common place in a document-based SwiftUI app.
var body: some Scene {
DocumentGroup(newDocument: Document()) { file in
// The document argument matches the name
// of a property in the content view,
// giving the content view access to the document.
ContentView(document: file.$document)
// Give the app access to the document.
.focusedSceneValue(\.document, file.$document)
}
}
Focused Bindings
A focused binding gives a SwiftUI view access to the focused value. To create a focused binding, declare a variable with the @FocusedBinding
property wrapper and supply the name of the focused value in parentheses.
@FocusedBinding(\.document) var document: Document?
Using Focused Bindings in SwiftUI Menus
To use a focused binding in a SwiftUI menu, you must create a SwiftUI view for the menu items. Create the menu items in the group. The following example shows a menu with items to make text bold and italic:
struct BoldItalicView: View {
@FocusedBinding(\.document) var document: Document?
var body: some View {
Group {
Button(action: {
// In your app call a function that
// does something to the document.
applyMarkup(tagType: .bold, document: document)
}, label: {
Text("Bold")
}).keyboardShortcut("b", modifiers: [.command])
Button(action: {
applyMarkup(tagType: .italic, document: document)
}, label: {
Text("Italic")
}).keyboardShortcut("i", modifiers: [.command])
}
}
}
Creating Menu Commands
If you have a menu with multiple menu items, creating a set of menu commands lets you keep the menu creation code in its own Swift file. To create a set of menu commands, supply the views inside the CommandMenu
block.
struct MarkupCommands: Commands {
var body: some Commands {
CommandMenu("Markup") {
BoldItalicView()
// Add other menu items here
}
}
}
Add the Menu to the App
The final step is to add the menu to the app.
struct Swift_Book_BuilderApp: App {
var body: some Scene {
DocumentGroup(newDocument: Document()) { file in
ContentView(document: file.$document)
.focusedValue(\.document, file.$document)
}
.commands {
MarkupCommands()
}
}
}
By creating a struct for the menu commands, the code inside the app’s body is much cleaner.
Credits
I learned much of the information shared in this article from the following article by Lost Moa:
Provide the current document to menu commands in a SwiftUI app