NSSplitView on macOS: Divider Autosave, Constraints, and Pane Lifecycles
Make AppKit split views resilient with stable autosave names, pane constraints, collapse policy, orientation-aware layout, and divider regression tests.
Split views are a compact way to expose navigation, content, and inspectors without opening a separate window. Their divider position is user-adjustable state, while pane constraints and available window size determine whether that state can still be honored. A split view that autosaves a raw position without a stable name may forget the user’s choice; a split view whose minimums exceed a small window can clip important content or become impossible to resize. Reliable behavior comes from treating pane layout as a constrained, adaptive system rather than a fixed pair of pixel widths.
AppKit offers NSSplitView and the higher-level NSSplitViewController with NSSplitViewItem children. Prefer the controller architecture when panes are represented by view controllers and use the split view’s documented APIs for configuration. Do not set properties or call methods Apple marks as unsupported through the controller’s managed split view; customize the controller before its view loads when a custom split view is required.
Orientation and divider coordinates
The term “vertical split” is easy to misread. isVertical == true means the divider is vertical and panes sit side by side; false means the divider is horizontal and panes stack top to bottom. Divider indices are zero-based. With a vertical divider, index zero is the leading divider; with a horizontal divider, index zero is the top divider. Name variables and tests according to divider orientation, not pane direction.
func configure(_ splitView: NSSplitView) {
splitView.isVertical = true // A vertical divider, side-by-side panes.
splitView.autosaveName = "ProjectBrowserSplit"
}
Set an autosave name that is stable across launches and unique to the split view’s role. A nil or empty name disables autosaving. Do not reuse the same name for unrelated windows; their pane layouts could overwrite one another. A versioned name is appropriate only when the structure changes so much that the older positions are no longer meaningful. Otherwise, preserve the same name so users retain their layout after routine app updates.
Autosaved geometry should be treated as a starting preference, not a guarantee. The window may be smaller, the display arrangement may have changed, or localization may have increased the minimum width of a pane. Let the split view enforce valid constraints and verify that important controls remain reachable after restoration.
Minimum sizes and delegate policy
Every pane should have a realistic minimum size based on its content. AppKit’s split-view delegate can specify minimum and maximum divider positions and can participate in deciding whether a divider may move. Avoid a hard-coded maximum that prevents the content pane from using available room after the user resizes the window. Calculate constraints from current content and layout margins, and update them when the window’s size class or inspector visibility changes.
If one pane can collapse, define exactly which pane and when collapse is allowed. A collapsed navigation sidebar should retain a path for reopening it, such as a toolbar item or menu command. Do not let a divider be dragged into a state where controls are present but inaccessible. For a controller-managed split view, use the split view controller’s item APIs and collapse behavior rather than bypassing the controller’s lifecycle.
final class PaneDelegate: NSObject, NSSplitViewDelegate {
func splitView(
_ splitView: NSSplitView,
constrainMinCoordinate proposedMinimumPosition: CGFloat,
ofSubviewAt dividerIndex: Int
) -> CGFloat {
// Illustrative only: production values should come from pane content
// and should account for the current window size and layout direction.
max(proposedMinimumPosition, 220)
}
}
This delegate method is only one part of divider policy; its behavior must be paired with the corresponding maximum constraint and tested with the actual hierarchy. Constraints expressed in a hardcoded point size can fail under localization or accessibility text sizes. Use them only as a baseline and let layout constraints express the content’s true minimum needs.
Layout direction, resizing, and restoration
A side-by-side split should respect leading and trailing semantics. In right-to-left interfaces, avoid interpreting “left pane” as synonymous with “navigation pane.” Use leading/trailing terminology and verify how the split view and its children respond to layout direction. If product requirements pin a pane to a physical edge, make that a deliberate rule and test it under localization.
As a window resizes, AppKit asks the split view to lay out its subviews around the divider. Avoid manually setting pane frames from unrelated resize callbacks while Auto Layout constraints are also active. Choose one layout authority. When observing willResizeSubviews or didResizeSubviews, use those notifications for derived state or instrumentation, not as a second competing layout engine. Debounce expensive downstream work such as rebuilding a large collection when divider movement emits frequent resize events.
Autosaving the divider does not automatically restore every part of the workspace. If the split view belongs to a restorable window, ensure its autosave identity and the window’s restoration lifecycle are configured independently. Test clean launch, restoration after window resize, display changes, and a newer app version with adjusted pane constraints. Never assume a previously valid saved position remains valid in the current geometry.
View controller containment
With NSSplitViewController, each NSSplitViewItem represents a child controller and its pane role. Use stable child-controller identity and keep each pane responsible for its own state. Loading a hidden or collapsed pane should not trigger unnecessary synchronous work. If the content comes from disk or a service, show a bounded loading state and keep the main thread responsive.
When panes share one model, define which controller owns mutations and how the other pane observes them. The navigation pane should generally emit a stable selected identifier; the detail pane should resolve that ID against the current model. Do not hand off an index path captured before the split view changed. When a pane is removed or replaced, cancel work that belongs to its previous controller and ensure late async completions cannot repopulate a discarded view.
Accessibility and keyboard use
A divider must be discoverable and operable through supported keyboard and accessibility paths, not only by mouse dragging. Verify the system’s split-view interactions with VoiceOver, full keyboard access, and larger text. Pane titles should communicate purpose, and a collapsed pane should have an accessible control that announces its expanded state. Do not use a custom thin divider that removes a reasonable interaction target without adding another accessible resizing mechanism.
Keyboard focus should remain meaningful after a pane collapses or a divider moves. If the focused view is inside a hidden pane, transfer focus to a sensible visible control or preserve a restoration target for reopening. Test tab traversal both before and after collapse, and confirm that commands such as search or delete are routed to the intended pane through the responder chain.
Verify constraints with a matrix
Test minimum and maximum window sizes, long localized strings, large text, both interface directions if supported, pane collapse and expansion, external display disconnection, and restoration of a previously saved divider. For each state, assert that required controls remain visible and that divider movement does not produce negative or zero content areas. Run tests with realistic data so a minimum width based on an empty view does not mask clipping once labels and controls are present.
Log layout values only as diagnostic geometry: orientation, divider index, available extent, proposed and accepted position, and pane min/max. Do not infer a user preference from one transient resize callback. A production split view has a stable autosave identity, content-aware constraints, explicit collapse behavior, one layout authority, and a tested keyboard/accessibility path.
Related:
- NSCollectionView Diffable Data Sources: Stable IDs and Snapshot Updates
- AppKit Responder Chains: First Responder, Actions, and Keyboard Routing
Sources: