Highlight
AppKit and UIKit
NSView/UIViewinstances can now directly respond to property changes in@Observablemodels and redraw automatically, without manualneedsDisplay = truecalls. Three new APIs,NSGestureRecognizerRepresentable,NSHostingMenu, andNSHostingSceneRepresentation, 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
@Observablemacro makes everyvarproperty 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:
@Animatablelets the struct participate in SwiftUI’s animation system@AnimatableIgnoredmarks a property that does not participate in animation interpolation, such as amodelreference- The
Canvasclosure 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:
NSHostingViewinherits fromNSViewand 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:
makeNSGestureRecognizerreturns your existing gesture recognizer subclasshandleNSGestureRecognizerActionis called when the gesture fires- Wrapping model changes in
withAnimationlets 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:
NSHostingMenuis a subclass ofNSMenuand accepts a SwiftUIViewas its root view- SwiftUI’s
keyboardShortcutmodifier maps to AppKit menu shortcuts Bindable(model).huecreates a two-way binding to an@Observableproperty.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:
NSHostingSceneRepresentationwraps SwiftUIScenevalues in a form AppKit can recognize- Call
addSceneRepresentationinsideapplicationWillFinishLaunchingto inject the scenes scenes.environment.openSettings()exposes an action for opening the settings window, which can be called from an old-code@IBAction- The
isInsertedparameter onMenuBarExtracontrols 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 { ... }
Related Sessions
- 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
@Observableto 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