Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

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 ↗

Transcript all transcripts

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 swift · at 3:35 ↗
// Modern dragging delegate methods
func tableView(_ tableView: NSTableView,
        pasteboardWriterForRow row: Int) -> (any NSPasteboardWriting)? {
    let pasteboardItem = NSPasteboardItem()
    pasteboardItem.setString(..., forType: .string)
    return pasteboardItem
}
Control events swift · at 4:55 ↗
// Use control events
let button = NSButton()
button.addTarget(
    self,
    action: #selector(trackingEndedOutsideHandler),
    for: .trackingEndedOutside
)
hitTest override swift · at 5:44 ↗
override func hitTest(_ point: NSPoint) -> NSView? {
    return nil
}
autorecalculatesKeyViewLoop swift · at 6:24 ↗
window.autorecalculatesKeyViewLoop = true
Expanded interface delegate — setup swift · at 7:37 ↗
// 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 swift · at 7:52 ↗
// 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 swift · at 8:16 ↗
// 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 swift · at 9:45 ↗
window.preventsApplicationTerminationWhenModal = false
Set window identifiers for state restoration swift · at 10:18 ↗
// Set window identifiers for state restoration
@MainActor 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 swift · at 11:04 ↗
// Preserve state to recreate the UI
@MainActor class MainWindowController: NSWindowController, NSWindowDelegate {
    // ...
    override func encodeRestorableState(with coder: NSCoder) {
        super.encodeRestorableState(with: coder)
        // ...
        coder.encode(selectedProduct?.identifier.uuid,
                    forKey: RestorationKeys.productIdentifier)
        // ...
    }
    // ...
}
invalidateRestorableState swift · at 11:50 ↗
// Invalidate restorable state when the view hierarchy changes
@MainActor class MainWindowController: NSWindowController, NSWindowDelegate {
    // ...
    convenience init() {
        // ...
        splitViewController.onProductSelected = { [weak self] product in
            self?.invalidateRestorableState()
        }
    }
}
restoreWindow(withIdentifier:) swift · at 12:26 ↗
// 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 swift · at 13:29 ↗
// Restore window UI
@MainActor 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 swift · at 16:11 ↗
// Subclass NSView to override cornerConfiguration
class LocalWeatherView: NSView {
    // ...
    override var cornerConfiguration: NSViewCornerConfiguration? {
        let radius: NSViewCornerRadius = .containerConcentric(minimumCornerRadius)
        return .uniformCorners(radius: radius)
    }
    // ...
}

Resources