diff --git a/scarf/scarf/ContentView.swift b/scarf/scarf/ContentView.swift index 9e60787..a67c211 100644 --- a/scarf/scarf/ContentView.swift +++ b/scarf/scarf/ContentView.swift @@ -14,6 +14,7 @@ struct ContentView: View { var body: some View { NavigationSplitView { SidebarView() + .navigationSplitViewColumnWidth(min: 180, ideal: 240, max: 360) } detail: { detailView .toolbar { diff --git a/scarf/scarf/Navigation/SidebarView.swift b/scarf/scarf/Navigation/SidebarView.swift index dc62d73..ee91083 100644 --- a/scarf/scarf/Navigation/SidebarView.swift +++ b/scarf/scarf/Navigation/SidebarView.swift @@ -59,5 +59,6 @@ struct SidebarView: View { } .listStyle(.sidebar) .navigationTitle("Scarf") + .splitViewAutosaveName("ScarfMainSidebar") } } diff --git a/scarf/scarf/Navigation/SplitViewAutosave.swift b/scarf/scarf/Navigation/SplitViewAutosave.swift new file mode 100644 index 0000000..0810fac --- /dev/null +++ b/scarf/scarf/Navigation/SplitViewAutosave.swift @@ -0,0 +1,57 @@ +import AppKit +import SwiftUI + +/// Makes the enclosing `NSSplitView` remember its divider positions across +/// app launches. `NavigationSplitView` is backed by `NSSplitViewController`, +/// whose split view honours `autosaveName` — AppKit writes the divider +/// offsets to `UserDefaults` on drag and restores them on the next launch. +/// +/// Usage: attach `.splitViewAutosaveName("…")` to a child of the split view +/// (the sidebar is a good choice). The modifier installs an invisible helper +/// that walks up the view hierarchy on first layout, finds the `NSSplitView`, +/// and assigns its autosave name. Subsequent launches restore the divider +/// positions before the window appears. +/// +/// The name is also used to key the entry in `UserDefaults` (AppKit stores +/// it as `NSSplitView Subview Frames `), so changing the name resets +/// the remembered width. Pick a stable string and leave it alone. +struct SplitViewAutosaveFinder: NSViewRepresentable { + let autosaveName: String + + func makeNSView(context: Context) -> NSView { + let view = NSView() + // Defer the hierarchy walk until after SwiftUI has attached this + // view to its host window — at makeNSView time the view has no + // superview yet, so we can't find the split view above us. + DispatchQueue.main.async { [weak view] in + guard let view else { return } + SplitViewAutosaveFinder.apply(autosaveName, startingFrom: view) + } + return view + } + + func updateNSView(_ nsView: NSView, context: Context) {} + + private static func apply(_ name: String, startingFrom view: NSView) { + var current: NSView? = view + while let node = current { + if let split = node as? NSSplitView { + // Only set once — reassigning clobbers AppKit's restore path. + if split.autosaveName != NSSplitView.AutosaveName(name) { + split.autosaveName = NSSplitView.AutosaveName(name) + } + return + } + current = node.superview + } + } +} + +extension View { + /// Persist the enclosing `NavigationSplitView` / `NSSplitView` divider + /// positions to `UserDefaults` under `autosaveName`. Attach to any child + /// of the split view (the sidebar works well). + func splitViewAutosaveName(_ autosaveName: String) -> some View { + background(SplitViewAutosaveFinder(autosaveName: autosaveName)) + } +}