WWDC.ai

Use SwiftUI with AppKit and UIKit

Incrementally adopt SwiftUI in AppKit or UIKit apps using Observation, hosting views and menus, gesture recognizer representables, and SwiftUI scenes.

Watch on Apple Developer

TL;DR

  • Use @Observable models to let AppKit and UIKit automatically invalidate drawing, layout, constraints, and other UI updates based on property reads.
  • Embed new SwiftUI components in an existing AppKit hierarchy with NSHostingView; the session demonstrates replacing slider-based drawing with a SwiftUI Canvas color picker.
  • Reuse existing AppKit gesture recognizers in SwiftUI with NSGestureRecognizerRepresentable, and build AppKit main-menu content from SwiftUI using NSHostingMenu.
  • Add complete SwiftUI scenes from an NSApplicationDelegate with NSHostingSceneRepresentation, including MenuBarExtra and Settings scenes.

Incremental SwiftUI adoption strategy

SwiftUI is intended to coexist with AppKit and UIKit, so existing apps can adopt it feature by feature rather than through a rewrite. The session uses a macOS lighting-control app as the running example, but the general approach applies across Apple platforms.

The recommended migration path starts with shared observable models, then moves individual controls, menus, gestures, and whole scenes to SwiftUI where they make sense.

  • Start by moving shared mutable UI state into @Observable model types.
  • Use SwiftUI for new or substantially rewritten UI components, especially when drawing and interaction code would change significantly.
  • Keep the existing AppKit or UIKit lifecycle and view hierarchy where it is still working.
  • Adopt SwiftUI scenes for new windows, settings, or menu bar extras without converting the whole app.

Observation in AppKit and UIKit

AppKit can automatically track reads from @Observable model properties during supported view and controller update methods. When those properties change, the relevant views update without manually setting needsDisplay.

The example replaces manual invalidation between hue, saturation, and brightness sliders with a shared ColorModel. AppKit tracks property reads inside drawing methods such as NSSliderCell.drawKnob and redraws when accessed values change.

  • AppKit observation applies to NSView.draw(_:), drawing paths called by view drawing such as NSSliderCell drawing methods, updateConstraints(), layout(), updateLayer(), and related NSViewController methods.
  • UIKit has similar Observation tracking across UIView, UIViewController, and additional UIKit types such as controls and collection view cells.
  • Back deploy AppKit integration to macOS 15 with NSObservationTrackingEnabled in Info.plist.
  • Back deploy UIKit integration to iOS 18 with UIObservationTrackingEnabled; tracking is enabled by default in the 2026 releases and later.

Observable model shared by AppKit and SwiftUI

Adding @Observable makes mutable properties participate in Observation so AppKit, UIKit, and SwiftUI can react to changes.

import Observation

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

Host SwiftUI controls inside AppKit

When a component's rendering and interaction model changes substantially, rebuilding it in SwiftUI can be simpler than evolving the existing AppKit view. The sample replaces three custom slider cells with a circular HSB color picker drawn using SwiftUI Canvas.

Canvas provides an immediate-mode drawing model similar to AppKit or UIKit drawing: each redraw receives a fresh GraphicsContext, and code issues strokes, fills, transforms, and filters. Existing Core Graphics drawing can be reused through withCGContext.

  • The SwiftUI color picker uses the same @Observable ColorModel as the previous AppKit implementation.
  • The picker draws a hue ring, saturation and brightness semicircles, and a center color preview.
  • The SwiftUI view is embedded in the existing NSView hierarchy using NSHostingView, which is itself an NSView subclass.

Embed a SwiftUI view in AppKit

Use NSHostingView to place a SwiftUI component inside an AppKit view hierarchy.

NSHostingView(
    rootView: HSBColorPicker(model: model)
)

SwiftUI Canvas shape of the color picker

The session's full sample draws with Canvas; this abbreviated version shows the immediate-mode drawing structure.

struct HSBColorPicker: View {
    var model: ColorModel

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

Bring AppKit gestures and menus into SwiftUI

Existing AppKit gesture recognizers do not need to be rewritten as SwiftUI gestures. NSGestureRecognizerRepresentable lets a SwiftUI view create and handle an NSGestureRecognizer subclass, then attach it with the standard .gesture modifier.

The sample adds a Force Click gesture that resets saturation and brightness to full intensity. Because Force Click is not available on every input device, the same action is also exposed through a SwiftUI-built main menu with keyboard shortcuts.

  • Implement makeNSGestureRecognizer(context:) to create the AppKit recognizer.
  • Implement handleNSGestureRecognizerAction(_:context:) to respond when the recognizer fires.
  • Attach the representable gesture to a SwiftUI view using .gesture.
  • Build menu content as a SwiftUI View, then wrap it in NSHostingMenu and assign it as an AppKit NSMenuItem submenu.

Wrap an AppKit gesture recognizer for SwiftUI

NSGestureRecognizerRepresentable reuses an existing AppKit recognizer from a SwiftUI view.

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
        }
    }
}

Host SwiftUI menu content in the AppKit main menu

NSHostingMenu is an NSMenu subclass, so it can be inserted into an existing AppKit menu tree.

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

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

Add SwiftUI scenes from an AppKit lifecycle

SwiftUI scenes can be added dynamically from an existing AppKit app without moving to the SwiftUI app lifecycle. The sample uses NSHostingSceneRepresentation from NSApplicationDelegate.applicationWillFinishLaunching to add a MenuBarExtra scene and a Settings scene.

The menu bar extra provides quick access to light controls. The settings window contains a toggle bound to app state that inserts or removes the menu bar extra, and NSHostingSceneRepresentation exposes an environment action for opening settings programmatically.

  • Create an NSHostingSceneRepresentation with one or more SwiftUI scenes.
  • Register it with NSApplication.shared.addSceneRepresentation(...).
  • Use MenuBarExtra(..., isInserted:) to let state control whether the menu bar item is present.
  • Use the representation's environment openSettings() action from an @IBAction when an AppKit menu command should open the SwiftUI settings scene.

Register SwiftUI scenes from an AppKit app delegate

NSHostingSceneRepresentation bridges SwiftUI scenes into an app that still uses NSApplicationDelegate.

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?()
}
Unofficial, not associated with Apple. BySuperwall

On this page

Ask AI