2026 SwiftUI & UI Frameworks
WWDC26 · 18 min · SwiftUI & UI Frameworks
Modernize your AppKit app
Bring your AppKit app up to date with modern macOS conventions. Dive into handling input with control events and gesture recognizers, moving beyond traditional tracking loops. Enhance keyboard navigation in your app, implement graceful state restoration after restarts, and take advantage of new corner concentricity APIs that let your interface blend seamlessly with the macOS aesthetic.
Watch at developer.apple.com ↗Chapters
- 0:00 — Introduction
- 1:06 — Modern input
- 1:27 — Modern event handling with gesture recognizers
- 2:25 — Selection, context menus, and drag and drop
- 3:52 — Text selection in custom views
- 4:26 — Control events and gesture recognizers
- 5:51 — Keyboard navigation and status items
- 8:57 — Continuity across launches
- 9:08 — Graceful app termination
- 9:55 — State restoration
- 14:09 — Design updates
- 14:24 — Liquid Glass updates in macOS 27
- 15:41 — Concentricity
- 16:59 — Next steps
Code shown on screen · 14 snippets
Modern dragging delegate
// Modern dragging delegate methods
func tableView(_ tableView: NSTableView,
pasteboardWriterForRow row: Int) -> (any NSPasteboardWriting)? {
let pasteboardItem = NSPasteboardItem()
pasteboardItem.setString(..., forType: .string)
return pasteboardItem
} Control events
// Use control events
let button = NSButton()
button.addTarget(
self,
action: #selector(trackingEndedOutsideHandler),
for: .trackingEndedOutside
) hitTest override
override func hitTest(_ point: NSPoint) -> NSView? {
return nil
} autorecalculatesKeyViewLoop
window.autorecalculatesKeyViewLoop = true Expanded interface delegate — setup
// Set the expanded interface delegate
@main class LightAppDelegate: NSObject, NSApplicationDelegate {
lazy var lightStatusItem: NSStatusItem = { ... }()
func applicationDidFinishLaunching(_ notification: Notification) {
// ...
lightStatusItem.expandedInterfaceDelegate = self
}
} Expanded interface delegate — methods
// Implement the delegate methods
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
// Cancel session to request window dismissal
lightStatusItem.expandedInterfaceSession?.cancel()
}
} Expanded interface delegate — cancel
// Cancel the session when dismissing
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
// Cancel session to request window dismissal
lightStatusItem.expandedInterfaceSession?.cancel()
}
} preventsApplicationTerminationWhenModal
window.preventsApplicationTerminationWhenModal = false Set window identifiers for state restoration
// Set window identifiers for state restoration
class MainWindowController: NSWindowController, NSWindowDelegate {
// ...
convenience init() {
let window = NSWindow( ... )
// ...
window.identifier = NSUserInterfaceItemIdentifier(WindowIdentifiers.mainWindow)
window.setFrameAutosaveName(WindowIdentifiers.mainWindow)
window.isRestorable = true
window.restorationClass = WindowRestorationHandler.self
// ...
}
} encodeRestorableState
// Preserve state to recreate the UI
class MainWindowController: NSWindowController, NSWindowDelegate {
// ...
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
// ...
coder.encode(selectedProduct?.identifier.uuid,
forKey: RestorationKeys.productIdentifier)
// ...
}
// ...
} invalidateRestorableState
// Invalidate restorable state when the view hierarchy changes
class MainWindowController: NSWindowController, NSWindowDelegate {
// ...
convenience init() {
// ...
splitViewController.onProductSelected = { [weak self] product in
self?.invalidateRestorableState()
}
}
} restoreWindow(withIdentifier:)
// Restore windows
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)
}
}
} restoreState
// Restore window UI
class MainWindowController: NSWindowController, NSWindowDelegate {
//...
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
}
//...
}
} cornerConfiguration
// Subclass NSView to override cornerConfiguration
class LocalWeatherView: NSView {
// ...
override var cornerConfiguration: NSViewCornerConfiguration? {
let radius: NSViewCornerRadius = .containerConcentric(minimumCornerRadius)
return .uniformCorners(radius: radius)
}
// ...
} Resources
Related sessions
-
14 min