SwiftUI newbie here. I'm struggling to understand how @FocusedBinding and FocusedValues work when building menus for a MacOS app. I'm trying to build the Apple HIG UI pattern with buttons in the window toolbar for changing the list view, and matching menu items in the View menu. Much like Finder windows have the four different view modes.
I have gone through the Apple's Landmarks tutorial, the Frameworks Engineer's example code in Apple dev forum, and Majid's tutorial.
The Apple documentation says FocusedValues is "a collection of state exported by the focused view and its ancestors." I assume the collection is global and I can set a focusedValue in any child View, and read or bind to any of the FocusedValues from anywhere in my code.
Therefore I don't understand why my first example below works, but the second one doesn't?
This works:
import SwiftUI
@main
struct TestiApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.frame(minWidth: 200, minHeight: 300)
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}
.windowStyle(HiddenTitleBarWindowStyle())
.commands {
MenuCommands()
}
}
}
struct ContentView: View {
// In the working version of the code selectedView is defined here
// in the ContentView, which is a direct child of the WindowGroup
// that has the .commands modifier.
@State private var selectedView: Int = 0
// For demonstration purposes I have simplified the authorization
// code to a hardcoded boolean.
private var isAuthorized: Bool = true
var body: some View {
switch isAuthorized {
case true:
// focusedValue is set here to the selectedView binding.
// I don't really understand why do it here, but it works.
AuthorizedView(selectedView: $selectedView)
.focusedValue(\.selectedViewBinding, $selectedView)
default:
NotYetAuthorizedView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AuthorizedView: View {
// selectedView is passed to this view as an argument and
// bound from ContentView.
@Binding var selectedView: Int
var body: some View {
List {
Text("Something here")
Text("Something more")
Text("Even more")
}
.toolbar {
// The Picker element sets and gets the bound selectedView value
Picker("View", selection: $selectedView) {
Text("View 1").tag(0)
Text("View 2").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
struct NotYetAuthorizedView: View {
var body: some View {
VStack {
Text("You need permission to access this ")
.padding()
}
}
}
struct MenuCommands: Commands {
private struct MenuContent: View {
// Command menu binds the selectedView value through focusedValues.
// MenuContent is a View, because otherwise the binding doesn't
// work (I read there's a bug in SwiftUI...).
@FocusedBinding(\.selectedViewBinding) var selectedView: Int?
var body: some View {
Button("View 1") {
selectedView = 0
}
.keyboardShortcut("1")
Button("View 2") {
selectedView = 1
}
.keyboardShortcut("2")
}
}
var body: some Commands {
CommandGroup(before: .toolbar) {
MenuContent()
}
}
}
struct SelectedViewBinding: FocusedValueKey {
typealias Value = Binding<Int>
}
extension FocusedValues {
var selectedViewBinding: SelectedViewBinding.Value? {
get { self[SelectedViewBinding.self] }
set { self[SelectedViewBinding.self] = newValue }
}
}
But if I make the following changes to ContentView and AuthorizedView, the project compiles fine but the binding between selectedView and the command menus no longer works:
struct ContentView: View {
// selectedView definition has been removed from ContentView
// and moved to AuthorizedView.
private var isAuthorized: Bool = true
var body: some View {
switch isAuthorized {
case true:
AuthorizedView()
// Also setting the focusedValue here has been removed
default:
NotYetAuthorizedView()
}
}
}
struct AuthorizedView: View {
// Moved selectedView definition here
@State private var selectedView: Int = 0
var body: some View {
List {
Text("Something here")
Text("Something more")
Text("Even more")
}
.toolbar {
Picker("View", selection: $selectedView) {
Text("View 1").tag(0)
Text("View 2").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.focusedValue(\.selectedViewBinding, $selectedView)
// I am now setting the focusedValue here, which seems
// more logical to me...
}
}
}
I like the second example better, because the selectedView state is encapsulated in the AuthorizedView where it belongs.