Skip to content

refactor(window): collapse 3 NSHostingControllers to 1 via NavigationSplitView#1032

Closed
datlechin wants to merge 9 commits intomainfrom
refactor/single-hosting-root-window
Closed

refactor(window): collapse 3 NSHostingControllers to 1 via NavigationSplitView#1032
datlechin wants to merge 9 commits intomainfrom
refactor/single-hosting-root-window

Conversation

@datlechin
Copy link
Copy Markdown
Member

Summary

Architectural cleanup: replace MainSplitViewController (NSSplitViewController + 3 separate NSHostingControllers for sidebar/detail/inspector) with a single NSHostingController<MainEditorRootView> whose body is NavigationSplitView { sidebar } detail: { detail } .inspector(...). The single SwiftUI rendering root replaces three independent ones.

This is option β from the perf refactor planning thread. Ships in one PR. Window/tab/route management (TabWindowController + EditorWindow + WindowManager) is intentionally untouched — that's the WindowGroup migration which is a separate, larger, runtime-test-required PR.

What changes

Deleted:

  • MainSplitViewController.swift (~497 lines) — NSSplitViewController shell with 3 split items
  • SidebarContainerViewController.swift (~132 lines) — NSViewController wrapper that hosted an NSSearchField above an NSHostingController
  • InspectorVisibilityProxy protocol uses (now redundant)

Added:

  • MainEditorWindowState.swift@Observable class that owns currentSession, sessionState, rightPanelState, inspectorPresented, sidebarColumnVisibility, windowTitle. Replaces the state-owning portion of MainSplitViewController. Handles connectionStatusDidChange observation.
  • MainEditorRootView.swift — SwiftUI root. NavigationSplitView { sidebar } detail: { MainContentView } .inspector(isPresented:). Sidebar search uses .searchable(text:placement:.sidebar) — replaces SidebarContainerViewController's NSSearchField.

Modified:

  • TabWindowController.swift — instantiates MainEditorWindowState + NSHostingController<MainEditorRootView> instead of MainSplitViewController. Owns the MainWindowToolbar directly (was previously installed by the split VC). Observes windowState.sessionState so the toolbar can be installed when a delayed connection completes.
  • MainContentCoordinator.swiftinspectorProxy: InspectorVisibilityProxy? and splitViewController: MainSplitViewController? weak refs replaced with editorWindowState: MainEditorWindowState?. Inspector toggle, sidebar tab switch, etc. all flow through this object.
  • MainWindowToolbar.swiftcoordinator.splitViewController?.setSidebarTab(...) becomes coordinator.editorWindowState?.setSidebarTab(...). NSSplitView KVO observer (which depended on the deleted MainSplitViewController.splitView) replaced by adding _ = coordinator.editorWindowState?.sidebarColumnVisibility to the existing withObservationTracking loop. Cleaner — observation framework instead of NSNotification fan-out.
  • MainContentView.swift / MainContentCommandActions.swift / MainContentView+Setup.swift — updated 3 call sites to use the new editorWindowState API.

Net code: -202 lines (522 added, 724 deleted across 10 files).

Why this is the right scope

Per WWDC 2020 "Use SwiftUI with AppKit" and Apple's apps (Xcode, Pages, Numbers): the canonical pattern is NSWindowController + a single NSHostingController containing the entire SwiftUI tree. Multiple NSHostingControllers per window is a known performance footgun — each is its own SwiftUI rendering root with independent ViewGraph, AttributeGraph, layout engine, and AppKit constraint negotiation.

The previous architecture had 3 hosting roots (sidebar + detail + inspector) plus 10 more in MainWindowToolbar's items. Each root re-evaluated its body and negotiated intrinsic sizes through NSHostingView.SizeConstraints.update(from:) independently. The 1.71s connect-window-open spike captured in the Time Profiler trace had _layoutSubtreeWithOldSize cascading 6 levels deep, with SizeFittingLayoutComputer.Engine.sizeThatFits and NSAttributedString.MetricsCache.metrics as leaves.

Collapsing the 3 layout-region hosting roots into one means:

  • One ViewGraph, one AttributeGraph for sidebar+detail+inspector layout
  • One pass through _layoutSubtreeWithOldSize instead of three
  • One intrinsic-size negotiation, settled by NavigationSplitView's column layout

The 10 toolbar hosting roots stay for now (NSToolbar.delegate-driven items can't directly become SwiftUI ToolbarContent while the window is hosted via NSHostingController — SwiftUI's .toolbar modifier needs scene context). Their migration is part of the WindowGroup PR.

Test plan (caller responsibility)

⚠️ This PR rewrites a substantial part of the window architecture. I cannot runtime-test in the development conversation. The build and lint pass; runtime testing falls to the merger.

Critical paths to verify:

  • Cold launch + Welcome → click Connect: window appears, sidebar populates with table list, data grid loads
  • Connection from dock context menu (Open Recent): same
  • Sidebar collapse/expand via toolbar button + Cmd+Ctrl+S
  • Switch sidebar tab (Tables ↔ Favorites) via toolbar buttons
  • Sidebar search: type in the search field at the top of the sidebar, table list filters
  • Switch to Favorites tab: search field placeholder changes to "Filter favorites", filters favorites
  • Inspector toggle via toolbar / Cmd+Option+I / menu: panel slides in/out
  • Inspector content: column inspector + query result summary + AI chat tabs
  • auto-show inspector on row selection (when dataGrid.autoShowInspector is on)
  • Cmd+W on last tab clears to "No tabs open" via commandActions.closeTab() (EditorWindow override unchanged)
  • Multi-window: open a second connection, both windows independent
  • Native macOS tab grouping (same connection windows tab together) still works
  • State restoration: app remembers inspectorPresented and sidebar column visibility across launches

If any of these break, follow-up fix PRs are expected.

Build & lint

  • xcodebuild Debug arm64: BUILD SUCCEEDED
  • swiftlint --strict on all 8 changed files: 0 violations
  • No em-dashes / AI-vibe phrases in added lines

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin datlechin closed this May 6, 2026
@datlechin datlechin deleted the refactor/single-hosting-root-window branch May 6, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant