From 1989feee22e23e97c7b3ab7efe49748853295f82 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 10:58:34 +0000 Subject: [PATCH] feat: persist sidebar width across launches (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire an NSSplitView autosave name into NavigationSplitView's underlying AppKit split view so the sidebar's drag-to-resize position is remembered in UserDefaults and restored on next launch. SplitViewAutosave.swift installs an invisible NSViewRepresentable that walks up the view hierarchy from the sidebar, finds the enclosing NSSplitView, and assigns autosaveName = "ScarfMainSidebar". AppKit handles persistence from there — no manual UserDefaults or @AppStorage plumbing needed. ContentView also gets navigationSplitViewColumnWidth(min:ideal:max:) bounds so first-launch (before any autosave exists) lands at a sensible 240pt ideal within a 180–360pt range. Refs #26 --- scarf/scarf/ContentView.swift | 1 + scarf/scarf/Navigation/SidebarView.swift | 1 + .../scarf/Navigation/SplitViewAutosave.swift | 57 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 scarf/scarf/Navigation/SplitViewAutosave.swift 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)) + } +}