fix(settings): make Settings its own top-level branch, not swapped content
The .id() fix on the detail pane wasn't enough — still reported as the document staying visible with only the sidebar switching. NavigationSplitView bridges to NSSplitViewController on macOS, and apparently doesn't reliably replace already-mounted detail content (a NavigationStack with real push history) for something unrelated inside one persisting split view instance, identity hints or not. Restructured so `navigation.isShowingSettings` picks between two entirely separate NavigationSplitView instances (settingsContent / mainContent) at the top of body, instead of branching on content inside a single one. A different top-level view hierarchy is a guaranteed full teardown of whatever AppKit was holding onto — no split-view instance persists across the switch for it to get confused about reusing. Also dropped the now-redundant `isShowingSettings` guards scattered through mainContent's toolbar/leadingToolbarContent — moot once mainContent only ever renders while Settings isn't showing.
This commit is contained in:
@@ -40,14 +40,51 @@ struct ContentView_macOS: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationSplitView {
|
// A totally separate top-level branch, not content swapped inside
|
||||||
Group {
|
// one persistent `NavigationSplitView` — that was tried first (an
|
||||||
|
// `if/else` inside a single split view, then an explicit `.id()` on
|
||||||
|
// just the detail pane) and neither reliably replaced the detail
|
||||||
|
// pane's content on macOS: `NavigationSplitView` bridges to
|
||||||
|
// `NSSplitViewController`, and swapping a `NavigationStack` with
|
||||||
|
// real push history for a plain view inside one persisting instance
|
||||||
|
// doesn't propagate the way plain SwiftUI identity rules would
|
||||||
|
// suggest. A different `if` branch is a genuinely different view
|
||||||
|
// hierarchy, so there's no existing split view instance for AppKit
|
||||||
|
// to get confused about reusing.
|
||||||
if navigation.isShowingSettings {
|
if navigation.isShowingSettings {
|
||||||
|
settingsContent
|
||||||
|
} else {
|
||||||
|
mainContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var settingsContent: some View {
|
||||||
|
NavigationSplitView {
|
||||||
SettingsSidebarList(
|
SettingsSidebarList(
|
||||||
selection: selectedSettingsSectionBinding,
|
selection: selectedSettingsSectionBinding,
|
||||||
onDone: { navigation.isShowingSettings = false }
|
onDone: { navigation.isShowingSettings = false }
|
||||||
)
|
)
|
||||||
} else {
|
.navigationSplitViewColumnWidth(min: 220, ideal: 260)
|
||||||
|
} detail: {
|
||||||
|
SettingsView(section: navigation.selectedSettingsSection ?? .appearance)
|
||||||
|
}
|
||||||
|
.navigationTitle("")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigation) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "gearshape.fill")
|
||||||
|
Text("Settings")
|
||||||
|
}
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(.fill.tertiary, in: Capsule())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mainContent: some View {
|
||||||
|
NavigationSplitView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SidebarSearchField(text: $globalSearchQuery)
|
SidebarSearchField(text: $globalSearchQuery)
|
||||||
Divider()
|
Divider()
|
||||||
@@ -61,19 +98,9 @@ struct ContentView_macOS: View {
|
|||||||
sidebar
|
sidebar
|
||||||
AccountFooter()
|
AccountFooter()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationSplitViewColumnWidth(min: 220, ideal: 260)
|
.navigationSplitViewColumnWidth(min: 220, ideal: 260)
|
||||||
} detail: {
|
} detail: {
|
||||||
detail
|
detail
|
||||||
// NavigationSplitView on macOS doesn't reliably tear down the
|
|
||||||
// detail pane's previous content when swapping between very
|
|
||||||
// different subtrees (a NavigationStack with a document
|
|
||||||
// pushed vs. plain SettingsView) off an implicit branch
|
|
||||||
// alone — an explicit identity change forces a real
|
|
||||||
// teardown/remount instead of it silently leaving the old
|
|
||||||
// document visible underneath.
|
|
||||||
.id(navigation.isShowingSettings)
|
|
||||||
}
|
}
|
||||||
// Empty rather than a real value: with one, the native title rendered
|
// Empty rather than a real value: with one, the native title rendered
|
||||||
// in the same toolbar row as the custom `.navigation` pill below,
|
// in the same toolbar row as the custom `.navigation` pill below,
|
||||||
@@ -108,7 +135,7 @@ struct ContentView_macOS: View {
|
|||||||
// `CollectionOverviewView`, so on Home it was a dead end: a
|
// `CollectionOverviewView`, so on Home it was a dead end: a
|
||||||
// user could click it, type, and nothing would happen. The
|
// user could click it, type, and nothing would happen. The
|
||||||
// sidebar's global search already covers "search everything."
|
// sidebar's global search already covers "search everything."
|
||||||
if !navigation.isShowingSettings && !(isShowingHome && documentPath.isEmpty) {
|
if !(isShowingHome && documentPath.isEmpty) {
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
contextualSearchField
|
contextualSearchField
|
||||||
}
|
}
|
||||||
@@ -178,12 +205,7 @@ struct ContentView_macOS: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var leadingToolbarContent: some View {
|
private var leadingToolbarContent: some View {
|
||||||
Group {
|
Group {
|
||||||
if navigation.isShowingSettings {
|
if !trimmedGlobalQuery.isEmpty {
|
||||||
HStack(spacing: 6) {
|
|
||||||
Image(systemName: "gearshape.fill")
|
|
||||||
Text("Settings")
|
|
||||||
}
|
|
||||||
} else if !trimmedGlobalQuery.isEmpty {
|
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
Text("Search")
|
Text("Search")
|
||||||
@@ -262,7 +284,6 @@ struct ContentView_macOS: View {
|
|||||||
globalSearchQuery = ""
|
globalSearchQuery = ""
|
||||||
selectedCollection = collection
|
selectedCollection = collection
|
||||||
isContextualSearchExpanded = true
|
isContextualSearchExpanded = true
|
||||||
navigation.isShowingSettings = false
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
isContextualSearchFocused = true
|
isContextualSearchFocused = true
|
||||||
}
|
}
|
||||||
@@ -273,13 +294,11 @@ struct ContentView_macOS: View {
|
|||||||
/// hierarchy immediately rather than just the leaf.
|
/// hierarchy immediately rather than just the leaf.
|
||||||
private func selectDocumentChain(_ collection: OutlineCollection, _ chain: [OutlineDocument]) {
|
private func selectDocumentChain(_ collection: OutlineCollection, _ chain: [OutlineDocument]) {
|
||||||
selectedCollection = collection
|
selectedCollection = collection
|
||||||
navigation.isShowingSettings = false
|
|
||||||
replaceDocumentPath(with: chain)
|
replaceDocumentPath(with: chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flat-list / search-result clicks: no known ancestors, single-level push.
|
/// Flat-list / search-result clicks: no known ancestors, single-level push.
|
||||||
private func openDocument(_ document: OutlineDocument) {
|
private func openDocument(_ document: OutlineDocument) {
|
||||||
navigation.isShowingSettings = false
|
|
||||||
replaceDocumentPath(with: [document])
|
replaceDocumentPath(with: [document])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,14 +317,7 @@ struct ContentView_macOS: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var detail: some View {
|
private var detail: some View {
|
||||||
if navigation.isShowingSettings {
|
if let apiClient = session.apiClient {
|
||||||
// Deliberately not a `NavigationStack` destination or a separate
|
|
||||||
// window — it's swapped into the same detail pane Home/
|
|
||||||
// collections use, alongside the real sidebar (now showing
|
|
||||||
// `SettingsSidebarList` instead of the collections tree) rather
|
|
||||||
// than a page with its own nested mini sidebar.
|
|
||||||
SettingsView(section: navigation.selectedSettingsSection ?? .appearance)
|
|
||||||
} else if let apiClient = session.apiClient {
|
|
||||||
// A fresh NavigationStack per collection (or when entering/leaving
|
// A fresh NavigationStack per collection (or when entering/leaving
|
||||||
// search), so switching either also clears any pushed document.
|
// search), so switching either also clears any pushed document.
|
||||||
NavigationStack(path: $documentPath) {
|
NavigationStack(path: $documentPath) {
|
||||||
|
|||||||
Reference in New Issue
Block a user