In AppKit, NSViewController
and its subclasses form the structural
backbone of macOS user-interface architecture.
This guide delivers a comprehensive, example-driven exploration of AppKit
view controllers—from bare-bones lifecycle management to advanced,
multi-pane container controllers—equipping you with practical fluency for real-world projects.
The canonical flow mirrors UIKit, but with macOS-specific nuances:
init(nibName:bundle:)
or SwiftUI bridging via NSHostingController
loadView()
(override to create views programmatically)viewDidLoad()
– one-time setup after view graph
viewWillAppear()
/ viewDidAppear()
viewWillDisappear()
/ viewDidDisappear()
final class DashboardController: NSViewController {
override func loadView() {
self.view = NSView()
view.wantsLayer = true
}
override func viewDidLoad() {
super.viewDidLoad()
let label = NSTextField(labelWithString: "Hello, Dashboard")
label.font = .systemFont(ofSize: 24, weight: .semibold)
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
Provides resizable split panes; ideal for Finder-like layouts.
let splitVC = NSSplitViewController()
let sidebar = NSViewController()
sidebar.view = NSView(frame: .zero)
sidebar.title = "Sidebar"
let content = DashboardController()
content.title = "Main"
splitVC.addSplitViewItem(NSSplitViewItem(sidebarWithViewController: sidebar))
splitVC.addSplitViewItem(NSSplitViewItem(viewController: content))
Manages horizontal tab interface. macOS 11+ uses unified toolbar style automatically.
A page-flipping, swipe-driven container (think Preview’s thumbnails).
If you migrate from UIKit’s UICollectionViewController
, adopt
NSCollectionView
plus a vanilla NSViewController
or subclass.
On macOS, loose coupling remains king. Use
NotificationCenter.default.post(name:object:userInfo:)
to broadcast model changes, or adopt delegate protocols for
one-to-one conversations.
Storyboards allow drag-and-drop relationships.
Pure code offers version-control clarity.
The hybrid approach is common: layout in Interface Builder,
dynamic wiring in viewDidLoad()
.
Build an app featuring a folder sidebar and a file list that updates reactively.
SidebarController
handling
NSOutlineView
of folders.FilesController
with
NSTableView
.NSSplitViewController
.Notification.Name("PathChanged")
.
extension Notification.Name { static let PathChanged = Self("PathChanged") }
final class SidebarController: NSViewController, NSOutlineViewDelegate {
/* ... */
func outlineViewSelectionDidChange(_ notification: Notification) {
guard let path = selectedPath else { return }
NotificationCenter.default.post(name: .PathChanged,
object: self,
userInfo: ["path": path])
}
}
final class FilesController: NSViewController {
@objc private func reload(_ note: Notification) {
if let path = note.userInfo?["path"] as? URL {
files = try? FileManager.default.contentsOfDirectory(at: path,
includingPropertiesForKeys: nil)
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(reload(_:)),
name: .PathChanged, object: nil)
}
}
Method | Description | Important Parameters |
---|---|---|
init(nibName:bundle:) |
Initialises with a xib file. |
nibName: Optional String of the XIB.bundle: Bundle? containing the nib.
|
loadView() |
Override to assign self.view manually. |
None—must set view property. |
viewWillAppear() |
Invoked just before view is added to a window. | animated does not exist in AppKit. |
representedObject |
Generic property for model injection. | Any Swift type. |
prepare(for segue: NSStoryboardSegue, sender: Any?) |
Fired before storyboard transition. |
segue: Supplies destination VC. sender: The origin initiator. |
sidebarWithViewController(_ vc) |
Creates a collapsible sidebar item. Width obeys sidebar metrics. |
contentListWithViewController(_ vc) |
For master-detail lists (macOS 13+ style). |
addTabViewItem(_:) |
Adds a tab; item holds its own view controller. |
selectedTabViewItemIndex |
Get/set currently visible tab index. |
Extract business logic into model layers; leverage Swift packages.
Use NSView.layoutSubtreeIfNeeded()
and
NSView.fittingSize
to interrogate constraints.
Declare identifiers as static constants to prevent typos.
From viewWillAppear()
, populate NSTouchBar
or adopt modern NSToolbarItemGroup
.