WWDC Quick Look 💓 By SwiftGGTeam
Use SwiftUI with AppKit and UIKit

Use SwiftUI with AppKit and UIKit

Watch original video

Highlight

AppKit and UIKit NSView/UIView instances can now directly respond to property changes in @Observable models and redraw automatically, without manual needsDisplay = true calls. Three new APIs, NSGestureRecognizerRepresentable, NSHostingMenu, and NSHostingSceneRepresentation, also let older projects gradually embed SwiftUI components, gestures, and scenes without changing their overall architecture.

Core Content

The Pain of Manually Refreshing NSView

Imagine maintaining a macOS lighting-control app with three NSSlider controls for Hue, Saturation, and Brightness. When the user drags the Hue slider, the track colors of the Saturation and Brightness sliders also need to change, because their track color depends on the currently selected Hue value.

In AppKit, that means writing a lot of boilerplate: every time a model property changes, you manually find every affected view and set needsDisplay = true one by one. Because the three sliders affect one another, the code quickly becomes tangled.

(03:10)

@Observable Automatic Tracking Comes to AppKit

Apple extends the Observation framework’s automatic tracking to AppKit and UIKit. As long as your model class uses the @Observable macro, and your draw(_:), updateConstraints(), layout(), or updateLayer() method reads a model property, the framework automatically registers the dependency. When the property changes, the view redraws automatically.

You do not need to change any drawing logic. Draw exactly as before, but you no longer need to write needsDisplay = true.

(03:38)

When to Move a Component to SwiftUI

The presenter wants to replace the lighting app’s UI with a new circular color picker: an outer Hue ring, two inner semicircles for Saturation and Brightness, and a color preview in the middle. Both the drawing logic and interaction model need to be rewritten.

This kind of “rewrite a component from scratch” scenario is the best time to introduce SwiftUI. You do not need to rewrite the entire app just to use SwiftUI; adopt it where you need a new implementation.

(05:52)

Put an Existing Gesture Recognizer into SwiftUI

The presenter has a custom Force Click gesture recognizer that is already used elsewhere in the app. Now he wants to add it to the new SwiftUI color picker: pressing firmly on the trackpad resets Saturation and Brightness to 100%.

Wrap it with NSGestureRecognizerRepresentable, without rewriting any gesture logic, and attach it to a SwiftUI view through .gesture(). This gesture can coexist with SwiftUI’s native DragGesture without interfering with it.

(08:14)

Write Menus in SwiftUI and Attach Them to the AppKit Main Menu

Force Click is not available on every Mac, such as Magic Mouse or the MacBook Neo trackpad. The presenter decides to add a menu item as an alternative.

He writes a ColorMenu in SwiftUI, containing buttons, keyboard shortcuts, and a palette picker. Then he wraps it as an NSMenu with NSHostingMenu and attaches it directly to the AppKit main menu bar. Keyboard shortcuts and animated transitions in the menu work natively.

(09:42)

Dynamically Inject a SwiftUI Scene into an Old AppDelegate

The presenter wants to add a Menu Bar Extra so users can quickly adjust light brightness without opening the main window. He builds this with SwiftUI’s MenuBarExtra scene and Settings scene, then dynamically injects them into the existing NSApplicationDelegate through NSHostingSceneRepresentation inside applicationWillFinishLaunching.

There is no need to convert the app to an @main plus App protocol structure. The old architecture stays in place, and new features can still use SwiftUI scenes.

(11:36)

Details

Automatic Refresh with @Observable in AppKit

(03:38)

First, convert the data model to @Observable:

import Observation

@Observable @MainActor
final class ColorModel {
    var hue: Double = 0.6
    var saturation: Double = 1.0
    var brightness: Double = 1.0
}

Then, inside the custom NSSliderCell’s drawKnob, read model.hue directly. AppKit automatically tracks this access and calls redraw when hue changes.

Supported AppKit methods include:

  • NSView.draw(_:)
  • NSView.updateConstraints()
  • NSView.layout()
  • NSView.updateLayer()
  • Corresponding methods on NSViewController

UIKit support is broader and also includes UIButton, UICollectionViewCell, and more.

For backward compatibility to macOS 15 and iOS 18, add the corresponding key to Info.plist:

  • macOS 15: NSObservationTrackingEnabled
  • iOS 18: UIObservationTrackingEnabled

The new systems in 2026 enable this by default, without configuration.

Key points:

  • The @Observable macro makes every var property in the class participate in the observation system automatically
  • Reading a property inside a supported method automatically establishes a dependency
  • Backward compatibility requires manually enabling the switch in Info.plist

Implement a Circular Color Picker in SwiftUI with Canvas

(06:28)

import SwiftUI
import Observation

@Animatable
struct HSBColorPicker: View {
    var hue: Double
    var saturation: Double
    var brightness: Double
    @AnimatableIgnored var model: ColorModel

    init(model: ColorModel) {
        self.model = model
        self.hue = model.hue
        self.saturation = model.saturation
        self.brightness = model.brightness
    }

    var body: some View {
        Canvas { context, size in
            let metrics = PickerMetrics(size: size)
            drawPicker(in: &context, metrics: metrics,
                       hue: hue, saturation: saturation, brightness: brightness)
        }
        .contentShape(Circle())
        .modifier(ColorPickerDragGesture(model: model))
        .aspectRatio(1, contentMode: .fit)
    }
}

Canvas provides an immediate-mode drawing API, similar to AppKit’s drawRect. Each redraw receives a GraphicsContext, and you can directly call commands such as stroke and fill. Existing CoreGraphics code can be reused through withCGContext.

Key points:

  • @Animatable lets the struct participate in SwiftUI’s animation system
  • @AnimatableIgnored marks a property that does not participate in animation interpolation, such as a model reference
  • The Canvas closure signature is (GraphicsContext, CGSize) -> Void
  • .contentShape(Circle()) limits the hit-testing area to the circle

Embed a SwiftUI View in the AppKit View Hierarchy

(07:21)

let hostingView = NSHostingView(
    rootView: HSBColorPicker(model: model)
)

NSHostingView is a subclass of NSView, so it can be placed directly inside any AppKit view hierarchy. Because ColorModel is already @Observable, data changes automatically synchronize into the SwiftUI view without additional bridging code.

Key points:

  • NSHostingView inherits from NSView and can be used like a normal AppKit view
  • Once the data model has been moved to @Observable, mixed SwiftUI/AppKit development becomes nearly frictionless

Reuse NSGestureRecognizer in SwiftUI

(08:14)

import SwiftUI
import AppKit

struct ForceClickReset: NSGestureRecognizerRepresentable {
    var model: ColorModel

    func makeNSGestureRecognizer(context: Context) -> ForceClickGestureRecognizer {
        ForceClickGestureRecognizer()
    }

    func handleNSGestureRecognizerAction(
        _ recognizer: ForceClickGestureRecognizer,
        context: Context
    ) {
        withAnimation {
            model.saturation = 1
            model.brightness = 1
        }
    }
}

final class ForceClickGestureRecognizer: NSGestureRecognizer {
    private var didActivate = false

    override func pressureChange(with event: NSEvent) {
        if event.stage >= 2 && !didActivate {
            didActivate = true
            state = .ended
        }
    }

    override func mouseDown(with event: NSEvent) {
        didActivate = false
        state = .possible
    }

    override func mouseUp(with event: NSEvent) {
        didActivate = false
        state = .possible
    }
}

Use it in a SwiftUI view like a native gesture:

HSBColorPicker(model: model)
    .gesture(ForceClickReset(model: model))

Key points:

  • makeNSGestureRecognizer returns your existing gesture recognizer subclass
  • handleNSGestureRecognizerAction is called when the gesture fires
  • Wrapping model changes in withAnimation lets SwiftUI handle animated transitions automatically
  • It can coexist with SwiftUI’s native DragGesture

Write an AppKit Main Menu in SwiftUI

(09:42)

struct ColorMenu: View {
    var model: ColorModel

    private static let hues: [(name: String, hue: Double)] = [
        ("Red", 0), ("Yellow", 0.17), ("Green", 0.33),
        ("Cyan", 0.5), ("Blue", 0.67), ("Purple", 0.83),
    ]

    var body: some View {
        Button("Full Intensity") {
            withAnimation {
                model.saturation = 1
                model.brightness = 1
            }
        }
        .keyboardShortcut(.upArrow, modifiers: [.command, .shift])

        Button("Blackout") {
            withAnimation {
                model.brightness = 0
            }
        }
        .keyboardShortcut(.downArrow, modifiers: [.command, .shift])

        Divider()

        Picker("Color", selection: Bindable(model).hue) {
            ForEach(Self.hues, id: \.hue) { entry in
                Label(entry.name, systemImage: "circle.fill")
                    .tint(Color(hue: entry.hue, saturation: 1, brightness: 1))
                    .tag(entry.hue)
            }
        }
        .pickerStyle(.palette)
    }
}

Attach it to the AppKit main menu:

@MainActor
class AppDelegate: NSObject, NSApplicationDelegate {
    let colorModel = ColorModel()

    func setupMainMenu() {
        let mainMenu = NSMenu()

        let colorMenu = NSHostingMenu(rootView: ColorMenu(model: colorModel))
        colorMenu.title = "Color"

        let colorMenuItem = NSMenuItem()
        colorMenuItem.submenu = colorMenu
        mainMenu.addItem(colorMenuItem)
    }
}

Key points:

  • NSHostingMenu is a subclass of NSMenu and accepts a SwiftUI View as its root view
  • SwiftUI’s keyboardShortcut modifier maps to AppKit menu shortcuts
  • Bindable(model).hue creates a two-way binding to an @Observable property
  • .pickerStyle(.palette) displays the picker as a palette in the menu

Dynamically Inject a SwiftUI Scene into an Old AppDelegate

(11:36)

@MainActor
class AppDelegate: NSObject, NSApplicationDelegate {
    let model = AppModel()
    var openSettingsAction: (() -> Void)?

    func applicationWillFinishLaunching(_ notification: Notification) {
        let scenes = NSHostingSceneRepresentation {
            LightMenuBarExtra(appModel: model)
            LightSettings(appModel: model)
        }
        NSApplication.shared.addSceneRepresentation(scenes)
        openSettingsAction = {
            scenes.environment.openSettings()
        }
    }

    @IBAction func openSettings(_ sender: Any?) {
        openSettingsAction?()
    }
}

struct LightMenuBarExtra: Scene {
    var appModel: AppModel

    var body: some Scene {
        MenuBarExtra("Light Mix", systemImage: "lightbulb.fill",
                     isInserted: Bindable(appModel).showMenuBarExtra) {
            MenuBarContent(appModel: appModel)
        }
        .menuBarExtraStyle(.window)
    }
}

struct LightSettings: Scene {
    var appModel: AppModel

    var body: some Scene {
        Settings {
            SettingsView(appModel: appModel)
        }
    }
}

Key points:

  • NSHostingSceneRepresentation wraps SwiftUI Scene values in a form AppKit can recognize
  • Call addSceneRepresentation inside applicationWillFinishLaunching to inject the scenes
  • scenes.environment.openSettings() exposes an action for opening the settings window, which can be called from an old-code @IBAction
  • The isInserted parameter on MenuBarExtra controls whether the menu bar icon is shown

Key Takeaways

1. Add a SwiftUI settings panel to an old macOS utility app

Many older apps use XIBs or hand-written code for settings screens, which are expensive to maintain. Rewrite the settings UI as a SwiftUI Settings scene and inject it into the existing AppDelegate with NSHostingSceneRepresentation. Organize settings with Form plus TabView, and the code can be half the size of the original XIB.

Entry API: NSHostingSceneRepresentation { Settings { ... } }

2. Move complex custom drawing to SwiftUI Canvas

If you have a custom chart or color picker written with CoreGraphics, consider moving it to SwiftUI’s Canvas. The drawing logic barely needs to change. Canvas’s GraphicsContext maps closely to CGContext, and withCGContext lets you directly access the native context to reuse old code.

Entry API: Canvas { context, size in ... }

3. Build a menu bar utility with SwiftUI

Many developers want to add a menu bar shortcut to an app without restructuring the whole architecture. Now you can add a MenuBarExtra scene directly inside an existing NSApplicationDelegate with just a few lines of code.

Entry API: MenuBarExtra("Title", systemImage: "icon") { ... }

4. Reuse existing complex gesture recognizers

If you have a custom NSGestureRecognizer that took a long time to refine, such as one for pressure or multi-finger gestures, you do not need to rewrite it with SwiftUI’s Gesture protocol. Wrap it with NSGestureRecognizerRepresentable and use it directly.

Entry API: implement the NSGestureRecognizerRepresentable protocol

5. Move the model layer to @Observable first

This is the lowest-cost, highest-return first step. Convert core data models to @Observable, and existing NSView instances immediately gain automatic refresh. When you later need to build new UI, the model is already ready for SwiftUI consumption, without another migration.

Entry API: @Observable class MyModel { ... }

  • What’s new in SwiftUI — An overview of new SwiftUI features, including more ways to use APIs such as Canvas and MenuBarExtra from this session
  • Drag and Drop — A deeper look at drag interactions in SwiftUI, complementing the gesture system in this session
  • SwiftData — Persistent data management that can be combined with @Observable to build a complete data-driven architecture
  • AppKit modernization — New AppKit features and modernization directions
  • Bring multiple windows to your SwiftUI app (WWDC22) — A reference video recommended by the presenter, with a deeper explanation of SwiftUI Scene usage

Comments

GitHub Issues · utterances