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

Get the Swift Dev Journal Newsletter

Subscribe and get exclusive articles, a free guide on moving from tutorials to making your first app, notices of sales on books, and anything I decide to add in the future.

    We won't send you spam. Unsubscribe at any time.