Modernize your AppKit app
Modernize AppKit apps with gesture recognizers, control events, keyboard navigation, state restoration, and macOS 27 visual updates.
TL;DR
- Replace mouseDown overrides and tracking loops with view-based APIs, NSControl.Events, and NSGestureRecognizer for better cross-framework behavior.
- Improve keyboard access by enabling automatic key-view-loop recalculation and using NSStatusItem expanded interface sessions for custom menu bar UI.
- Make quit and relaunch seamless with preventsApplicationTerminationWhenModal, window identifiers, NSWindowRestoration, and explicit UI-state encoding.
- Adopt macOS 27 UI refinements such as Liquid Glass updates and NSViewCornerConfiguration for concentric rounded corners.
Modern input: move beyond mouseDown tracking
AppKit apps should prefer modern event-handling APIs over mouseDown overrides and manual tracking loops. Gesture recognizers provide a common event model across AppKit, SwiftUI, and Mac Catalyst/UIKit, and allow AppKit to supply platform behaviors without custom low-level event code.
For common mouseDown use cases, AppKit usually has a dedicated higher-level API. Use selection properties and delegate callbacks for collection/table/outline selection, menu APIs for contextual menus, modern dragging delegates for drag and drop, and NSTextSelectionManager for text selection in custom views.
- Observe selected on NSCollectionViewItem and NSTableRowView, or use selection delegate callbacks such as NSTableViewDelegate and NSOutlineViewDelegate.
- Use NSView.defaultMenu when all instances share a menu, NSResponder.menu for per-responder menus, or NSView.menuForEvent(_:) for event-dependent menus.
- Use modern pasteboard writer delegate methods on NSTableView, NSCollectionView, NSOutlineView, and NSBrowser for dragging.
- Use NSTextSelectionManager in macOS 27 to bring macOS text-selection behavior to non-NSTextView custom views.
Modern dragging delegate
Return an NSPasteboardWriting item from a table view dragging delegate instead of implementing drag behavior from low-level mouse tracking.
func tableView(_ tableView: NSTableView,
pasteboardWriterForRow row: Int) -> (any NSPasteboardWriting)? {
let pasteboardItem = NSPasteboardItem()
pasteboardItem.setString(..., forType: .string)
return pasteboardItem
}Control events and gesture recognizers
NSControl.Events brings a UIKit-like target/action model for user-driven tracking state changes to standard AppKit controls such as buttons and sliders. This avoids subclassing controls just to observe tracking transitions.
For interactions that are not covered by standard controls, attach AppKit gesture recognizers to views. If standard recognizers are not enough, create custom NSGestureRecognizer subclasses. Because gesture recognizers work through the view hierarchy, overlapping sibling views can block input; resize the overlay or override hit testing when clicks should pass through.
- Use NSControl.addTarget(_:action:for:) for control tracking state changes.
- Use NSGestureRecognizer for custom view interactions instead of manual tracking loops.
- If an overlay view should not intercept clicks, override hitTest(_:) and return nil.
Register a control event
Registers target/action handling for a specific NSControl.Events value without subclassing NSButton.
let button = NSButton()
button.addTarget(
self,
action: #selector(trackingEndedOutsideHandler),
for: .trackingEndedOutside
)Let hit testing fall through an overlay
Allows underlying content to receive mouse events when an overlay view should not participate in hit testing.
override func hitTest(_ point: NSPoint) -> NSView? {
return nil
}Keyboard navigation and status items
Keyboard navigation is central to macOS usability and accessibility. When full keyboard access is enabled, Tab and Shift-Tab move focus through the key view loop. If views are dynamically added or removed, let AppKit maintain that loop automatically.
Status items need explicit handling depending on their behavior. Menu-backed status items already behave like menu bar menus. Action-only status items should configure the NSStatusItem button with target/action, while status items that show custom transient UI should use the expanded interface session API so AppKit can manage keyboard focus correctly.
- Set window.autorecalculatesKeyViewLoop = true to update focus order as the hierarchy changes.
- For a status item action, configure NSStatusItem.button with target/action and optional image.
- For a custom status item view, set the view and add target/action to the status item.
- For status items that display a custom window, implement NSStatusItemExpandedInterfaceDelegate and cancel the expandedInterfaceSession when dismissing.
Automatically maintain the key view loop
Lets AppKit recalculate Tab navigation order when the window's view hierarchy changes.
window.autorecalculatesKeyViewLoop = trueStatus item expanded interface delegate
Uses AppKit's expanded interface lifecycle so custom menu bar UI participates correctly in keyboard focus behavior.
lightStatusItem.expandedInterfaceDelegate = self
extension LightAppDelegate: NSStatusItemExpandedInterfaceDelegate {
func statusItem(_ statusItem: NSStatusItem,
didBegin session: NSStatusItemExpandedInterfaceSession) {
// Show window
}
func statusItemDidEndExpandedInterfaceSession(_ statusItem: NSStatusItem,
animated: Bool) {
// Hide window
}
func selectedAction() {
// Take the action
lightStatusItem.expandedInterfaceSession?.cancel()
}
}Graceful termination and state restoration
A modern Mac app should quit without unnecessary blocking and relaunch into the same UI state. Blocking termination is appropriate for data-loss scenarios, but sheets and modals that do not require intervention should allow the app to terminate, especially during system restarts.
State restoration has three parts: opt windows into restoration, encode the UI state needed to reconstruct them, and decode that state after relaunch. The session emphasizes saving UI reconstruction identifiers rather than re-serializing document or database contents.
- Set preventsApplicationTerminationWhenModal to false for sheets or modals that do not strictly need user input before quit.
- Assign window.identifier and, for common non-document windows, setFrameAutosaveName(_:) so AppKit can restore frame and space placement.
- Set window.isRestorable = true and window.restorationClass to an NSWindowRestoration type.
- Override encodeRestorableState(with:) and restoreState(with:) for UI state, and call invalidateRestorableState() when relevant UI state changes.
- Always call the restoreWindow completionHandler, even on failure, because AppKit waits for every restorable window.
Allow termination for noncritical modal UI
Prevents a noncritical sheet or modal from blocking app termination.
window.preventsApplicationTerminationWhenModal = falseOpt a window into restoration
Identifies a restorable window, enables frame autosave for a common window, and names the restoration class.
window.identifier = NSUserInterfaceItemIdentifier(WindowIdentifiers.mainWindow)
window.setFrameAutosaveName(WindowIdentifiers.mainWindow)
window.isRestorable = true
window.restorationClass = WindowRestorationHandler.selfEncode and invalidate restorable UI state
Stores only the identifiers needed to reconstruct UI state and invalidates the saved state when selection changes.
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(selectedProduct?.identifier.uuid,
forKey: RestorationKeys.productIdentifier)
}
splitViewController.onProductSelected = { [weak self] product in
self?.invalidateRestorableState()
}Restore windows and window UI
Recreates each restorable window by identifier, then decodes per-window UI state.
class WindowRestorationHandler: NSObject, NSWindowRestoration {
static func restoreWindow(
withIdentifier identifier: NSUserInterfaceItemIdentifier,
state: NSCoder,
completionHandler: @escaping (NSWindow?, Error?) -> Void
) {
if identifier == .mainWindow,
let window = appDelegate.mainWindowController?.window {
completionHandler(window, nil)
} else if identifier == .imageWindow {
let controller = ImageWindowController()
appDelegate.imageWindowControllers.append(controller)
completionHandler(controller.window, nil)
} else {
completionHandler(nil, error)
}
}
}
override func restoreState(with coder: NSCoder) {
super.restoreState(with: coder)
if let productId = coder.decodeObject(of: [NSString.self],
forKey: RestorationKeys.productIdentifier) as? String {
splitViewController?.selectedProductId = productId
}
}macOS 27 design updates and concentric corners
Apps that adopted Liquid Glass in macOS 26 receive several macOS 27 refinements automatically. The automatic NSScrollEdgeEffectStyle can resolve to a hard-edge effect for free-floating text such as window titles; sidebars extend to window edges; sidebar selections use semi-bold text; bordered toolbar items over sidebars adopt Liquid Glass.
macOS 27 also adds an interactive glass effect for controls and containers of interactive controls, intended to be used sparingly. For rounded content near container corners, NSViewCornerConfiguration lets a view compute corner radii that remain visually concentric with its container.
- Use the new glass interaction effect for controls, buttons, or glass containers of interactive controls-not every glass surface.
- Use NSView.cornerConfiguration to describe view corner behavior.
- Use NSViewCornerRadius.containerConcentric(_:) so corners adapt to the container shape, with a minimum radius for consistent rounding.
- Use .uniformCorners(radius:) when all four corners should share the same radius behavior.
Concentric corner configuration
Makes a custom view's rounded corners follow the curve of the containing view while preserving a minimum corner radius.
class LocalWeatherView: NSView {
override var cornerConfiguration: NSViewCornerConfiguration? {
let radius: NSViewCornerRadius = .containerConcentric(minimumCornerRadius)
return .uniformCorners(radius: radius)
}
}Resources
- Use SwiftUI with AppKit
- Restoring your app's state with AppKit
- Gestures
- TN3212: Adopting gesture recognizers for Sidecar touch support
- NSControl.Events
Related Sessions
Modernize your UIKit app
Update UIKit apps for iOS 27 resizable iPhone environments, scene lifecycle, adaptive layout APIs, modern bars, Apple Intelligence, and Xcode agent skills.
Dive into lazy stacks and scrolling with SwiftUI
Understand how SwiftUI LazyVStack and LazyHStack estimate layout, load subviews, prefetch work, and support smoother programmatic scrolling.