FocusedBinding, along with the
FocusedValue property wrapper, works in conjunction with the new Commands API to establish data dependencies in menu bar menus and key commands where the source of truth lives somewhere in the key/main window's focused view hierarchy. The resulting menu items and key commands are sensitive to the user's focus, and can be enabled, disabled, and customized accordingly.
The following example runs on macOS and iOS, but unfortunately some bugs in the seed prevent it from behaving as expected. On macOS, a custom menu bar menu exposes a command that is supposed to mutate state in the main window, but state-sharing between windows and the main menu is broken. On iOS, the same command is supposed to be exposed as a key command with a shortcut that appears in the keyboard shortcut discovery HUD, but in fact the Command and Keyboard Shortcut APIs are disabled on iOS. Support for all of this will come in a future seed.
Doing menus like this is a bit of a change from the old AppKit way—instead of searching the responder chain for a view that can validate the menu item and dynamically select an action, we arrange things so that the menu item can form a data dependency on some source of truth in the focused view hierarchy. The menu item uses the data to validate itself and perform its action.
Code Block // This example runs on macOS, iOS, and iPadOS. |
// |
// Big Sur Seed 1 has some known issues that prevent state-sharing between |
// windows and the main menu, so this example doesn't currently behave as |
// expected on macOS. Additionally, the Commands API is disabled on iOS in Seed |
// 1. These issues will be addressed in future seeds. |
// |
// The Focused Value API is available on all platforms. The Commands and |
// Keyboard Shortcut APIs are available on macOS, iOS, iPadOS, and |
// tvOS—everywhere keyboard input is accepted. |
|
@main |
struct MessageApp : App { |
var body: some Scene { |
WindowGroup { |
MessageView() |
} |
.commands { |
MessageCommands() |
} |
} |
} |
|
struct MessageCommands : Commands { |
// Try to observe a binding to the key window's `Message` model. |
// |
// In order for this to work, a view in the key window's focused view |
// hierarchy (often the root view) needs to publish a binding using the |
// `View.focusedValue(_:_:)` view modifier and the same `\.message` key |
// path (anologous to a key path for an `Environment` value, defined |
// below). |
@FocusedBinding(\.message) var message: Message? |
|
// FocusedBinding is a binding-specific convenience to provide direct |
// access to a wrapped value. |
// |
// `FocusedValue` is a more general form of the property wrapper, designed |
// to work with all value types, including bindings. The following is |
// functionally equivalent, but places the burden of unwrapping the bound |
// value on the client. |
// @FocusedValue(\.message) var message: Binding<Message>? |
|
var body: some Commands { |
CommandMenu("Message") { |
Button("Send", action: { message?.send() }) |
.keyboardShortcut("D") // Shift-Command-D |
.disabled(message?.text.isEmpty ?? true) |
} |
} |
} |
|
struct MessageView : View { |
@State var message = Message(text: "Hello, SwiftUI!") |
|
var body: some View { |
TextEditor(text: $message.text) |
.focusedValue(\.message, $message) |
.frame(idealWidth: 600, idealHeight: 400) |
} |
} |
|
struct Message { |
var text: String |
// ... |
|
mutating func send() { |
print("Sending message: \(text)") |
// ... |
} |
} |
|
struct FocusedMessageKey : FocusedValueKey { |
typealias Value = Binding<Message> |
} |
|
extension FocusedValues { |
var message: FocusedMessageKey.Value? { |
get { self[FocusedMessageKey.self] } |
set { self[FocusedMessageKey.self] = newValue } |
} |
} |