Providing the current document to menu commands


New in SwiftUI macOS12 and iPadOS15 we can now use .focusedSceneValue this operator works in a similar way to .focusedValue but does not require the wrapped view to be focused. Instead, it emits values based on if the scene is focused.

Note in iPadOS 15 Beta 3 the commands section continues to show the file browser commands when the document is open. FB9337142


This is just what we have been asking for so that we can pass data from our focused scene to the .commands section of our apps. In the older SDK I had to build my own framework for this, but now we can do it just using these functions provided to us in SwiftUI 🎉.


Let us consider a simple Document-based App that used the DocumentGroup(newDocument:) API. In our commands section, we may wish to not only mutate this document but also based on the document state we might want to disable/enable some commands. For this post, we will be creating an uppercased command that uppercases the document's text but is only enabled if the text is not already all uppercased


Providing the current focused windows document to the commands


First, let us declare an extension on FocusedValues


extension FocusedValues {
  struct DocumentFocusedValues: FocusedValueKey {
    typealias Value = Binding<ExampleShortcutsDocument>
  }

  var document: Binding<ExampleShortcutsDocument>? {
    get {
      self[DocumentFocusedValues.self]
    }
    set {
      self[DocumentFocusedValues.self] = newValue
    }
  }
}


Notice here that we are using a Binding to the document rather than just the raw document, we do this since the document itself is a Struct and therefore so that the commands menu can mutate it we need to pass a binding.


Luckily the DocumentGroup(newDocument:) already provides us with a Binding to our document.


DocumentGroup(newDocument: ExampleShortcutsDocument()) { file in
  ContentView(document: file.$document)
  .focusedSceneValue(\.document, file.$document)
}


Accessing the document from the commands section


We need to make use of the @FocusedBinding property wrapper. When creating our command it's is best to break these out into their own view.


struct UppercasedText: View {
   
  @FocusedBinding(\.document)
  var document
   
  var body: some View {
    Button {
      guard let text = document?.text else {
        return
      }
      document?.text = text.uppercased()
    } label: {
      Text("Uppercased")
    }
    .disabled(document == nil || document?.text == document?.text.uppercased() )
    .keyboardShortcut("u", modifiers: [.command, .shift])
  }
}


And finally, to add this to the app we can update our main App struct:


@main
struct ExampleShortcutsApp: App {
  var body: some Scene {
    DocumentGroup(newDocument: ExampleShortcutsDocument()) { file in
      ContentView(document: file.$document)
      .focusedSceneValue(\.document, file.$document)
    }
    .commands {
      CommandMenu("Document") {
        UppercasedText()
      }
    }
  }
}