Highlight
AppKit 和 UIKit 的 NSView/UIView 现在可以直接响应
@Observable模型的属性变化并自动重绘,开发者无需手动调用needsDisplay = true;同时新增NSGestureRecognizerRepresentable、NSHostingMenu和NSHostingSceneRepresentation三个 API,让老项目在不改动整体架构的前提下,把 SwiftUI 组件、手势和 Scene 逐步嵌入现有 AppKit/UIKit 应用。
核心内容
手动刷新 NSView 的痛点
想象你维护一个 macOS 灯光控制 App,界面里有三个 NSSlider 分别控制 Hue、Saturation 和 Brightness。当你拖动 Hue 滑块时,Saturation 和 Brightness 滑块的轨道颜色也要跟着变——因为轨道颜色取决于当前选中的 Hue 值。
在 AppKit 里,这意味你要写一堆样板代码:每次模型属性变化,手动找到所有受影响的视图,逐个设置 needsDisplay = true。三个滑块之间互相影响,代码很快变成一团乱麻。
(03:10)
@Observable 自动追踪来到 AppKit
Apple 把 Observation 框架的自动追踪能力扩展到了 AppKit 和 UIKit。只要你的模型类加上 @Observable 宏,然后在 draw(_:)、updateConstraints()、layout() 或 updateLayer() 里读取了模型的属性,框架就会自动注册依赖关系。属性一变,视图自动重绘。
不需要改任何绘制逻辑。你之前怎么画,现在还怎么画——只是再也不用写 needsDisplay = true 了。
(03:38)
什么时候该把组件迁到 SwiftUI
演讲者想给灯光 App 换一个全新的圆形色盘:外圈是 Hue 环,内圈左右各一个半圆分别控制 Saturation 和 Brightness,中间是颜色预览。整个绘制逻辑和交互方式都要重写。
这种”从零开始重写一个组件”的场景,就是引入 SwiftUI 的最佳时机。不需要为了用 SwiftUI 而重写整个 App,只在需要全新实现的地方切入。
(05:52)
把老手势识别器塞进 SwiftUI
演讲者有一个自定义的 Force Click 手势识别器,已经在 App 的其他地方用了。现在他想把这个功能加到新的 SwiftUI 色盘上——用力按压 trackpad 就把 Saturation 和 Brightness 重置到 100%。
用 NSGestureRecognizerRepresentable 包一层,不用重写任何手势逻辑,直接通过 .gesture() 挂载到 SwiftUI 视图上。这个手势和 SwiftUI 原生的 DragGesture 可以共存,互不干扰。
(08:14)
用 SwiftUI 写菜单,挂到 AppKit 主菜单上
Force Click 不是每台 Mac 都支持(比如 Magic Mouse 或 MacBook Neo 的 trackpad)。演讲者决定加一个菜单项作为备选方案。
他用 SwiftUI 写了一个 ColorMenu,包含按钮、快捷键和调色板选择器。然后用 NSHostingMenu 把它包装成 NSMenu,直接挂到 AppKit 的主菜单栏上。菜单里的键盘快捷键、动画过渡全部原生工作。
(09:42)
在老 AppDelegate 里动态注入 SwiftUI Scene
演讲者想加一个 Menu Bar Extra,让用户不用打开主窗口就能快速调节灯光亮度。他用 SwiftUI 的 MenuBarExtra Scene 和 Settings Scene 写了这个功能,然后通过 NSHostingSceneRepresentation 在 applicationWillFinishLaunching 里动态注入到现有的 NSApplicationDelegate 中。
不需要把 App 改成 @main + App 协议的结构。老架构不动,新功能照样用 SwiftUI Scene。
(11:36)
详细内容
@Observable 在 AppKit 中的自动刷新
(03:38)
先把数据模型改成 @Observable:
import Observation
@Observable @MainActor
final class ColorModel {
var hue: Double = 0.6
var saturation: Double = 1.0
var brightness: Double = 1.0
}
然后在自定义 NSSliderCell 的 drawKnob 里直接读取 model.hue。AppKit 会自动追踪这个访问,并在 hue 变化时调用重绘。
这套机制支持的 AppKit 方法包括:
NSView.draw(_:)NSView.updateConstraints()NSView.layout()NSView.updateLayer()NSViewController的对应方法
UIKit 支持的范围更广,还包括 UIButton、UICollectionViewCell 等。
向后兼容到 macOS 15 和 iOS 18,需要在 Info.plist 里加对应的 key:
- macOS 15:
NSObservationTrackingEnabled - iOS 18:
UIObservationTrackingEnabled
2026 年的新系统默认开启,不需要配置。
关键点:
@Observable宏让类里所有var属性自动参与观察系统- 只要在支持的方法里读了属性,就自动建立依赖
- 向后兼容需要手动在 Info.plist 里开开关
用 Canvas 在 SwiftUI 中实现圆形色盘
(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 提供即时模式绘制 API,和 AppKit 的 drawRect 类似。每次重绘都会传入一个 GraphicsContext,你可以直接调用 stroke、fill 等命令。已有的 CoreGraphics 代码可以通过 withCGContext 复用。
关键点:
@Animatable让结构体支持 SwiftUI 动画系统@AnimatableIgnored标记不参与动画插值的属性(如model引用)Canvas的闭包签名是(GraphicsContext, CGSize) -> Void.contentShape(Circle())把点击区域限制在圆形内
把 SwiftUI 视图嵌入 AppKit 视图层级
(07:21)
let hostingView = NSHostingView(
rootView: HSBColorPicker(model: model)
)
NSHostingView 是 NSView 的子类,可以直接放到任何 AppKit 视图层级中。因为 ColorModel 已经是 @Observable,数据变化会自动同步到 SwiftUI 视图中,不需要额外的桥接代码。
关键点:
NSHostingView继承自NSView,可以像普通 AppKit 视图一样使用- 数据模型提前迁移到
@Observable后,混编变得零成本
复用 NSGestureRecognizer 到 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
}
}
在 SwiftUI 视图中像原生手势一样使用:
HSBColorPicker(model: model)
.gesture(ForceClickReset(model: model))
关键点:
makeNSGestureRecognizer返回你现有的手势识别器子类handleNSGestureRecognizerAction在手势触发时回调withAnimation包裹模型修改,SwiftUI 会自动处理动画过渡- 和 SwiftUI 原生
DragGesture可以共存
用 SwiftUI 写 AppKit 主菜单
(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)
}
}
挂到 AppKit 主菜单:
@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)
}
}
关键点:
NSHostingMenu是NSMenu的子类,接收 SwiftUIView作为根视图- SwiftUI 的
keyboardShortcut修饰符会映射到 AppKit 的菜单快捷键 Bindable(model).hue创建对@Observable属性的双向绑定.pickerStyle(.palette)在菜单中显示为调色板样式
在老 AppDelegate 中动态注入 SwiftUI Scene
(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)
}
}
}
关键点:
NSHostingSceneRepresentation把 SwiftUIScene包装成 AppKit 可识别的形式- 在
applicationWillFinishLaunching中调用addSceneRepresentation完成注入 scenes.environment.openSettings()暴露打开设置窗口的动作,可以从老代码的@IBAction中调用MenuBarExtra的isInserted参数控制菜单栏图标的显示/隐藏
核心启发
1. 给老 macOS 工具 App 加一个 SwiftUI 设置面板
很多老 App 的设置界面是用 XIB 或纯代码写的,维护成本高。用 SwiftUI 重写一个 Settings Scene,通过 NSHostingSceneRepresentation 注入到现有 AppDelegate 中。设置项用 Form + TabView 组织,代码量比原来的 XIB 少一半。
入口 API:NSHostingSceneRepresentation { Settings { ... } }
2. 把复杂的自定义绘制迁移到 SwiftUI Canvas
如果你有一个用 CoreGraphics 写的自定义图表或色盘,可以考虑迁移到 SwiftUI 的 Canvas。绘制逻辑基本不用改——Canvas 的 GraphicsContext 和 CGContext 概念一致,还可以通过 withCGContext 直接拿到原生上下文复用旧代码。
入口 API:Canvas { context, size in ... }
3. 用 SwiftUI 写菜单栏工具
很多开发者想给 App 加一个菜单栏快捷入口,但不想重构整个 App 架构。现在可以直接在现有 NSApplicationDelegate 里加一个 MenuBarExtra Scene,几行代码搞定。
入口 API:MenuBarExtra("Title", systemImage: "icon") { ... }
4. 复用现有的复杂手势识别器
如果你有一个花了大量时间调试的自定义 NSGestureRecognizer(比如支持压感、多指手势等),不需要用 SwiftUI 的 Gesture 协议重写。用 NSGestureRecognizerRepresentable 包一层就能直接用。
入口 API:实现 NSGestureRecognizerRepresentable 协议
5. 先把模型层迁移到 @Observable
这是成本最低、收益最大的第一步。把核心数据模型改成 @Observable,现有的 NSView 立刻获得自动刷新能力。等以后需要写新 UI 时,模型已经可以直接被 SwiftUI 消费,不需要再做一次迁移。
入口 API:@Observable class MyModel { ... }
关联 Session
- What’s new in SwiftUI — SwiftUI 的新特性总览,了解本 Session 中使用的 Canvas、MenuBarExtra 等 API 的更多用法
- Drag and Drop — 深入学习 SwiftUI 中的拖拽交互,与本 Session 的手势系统互补
- SwiftData — 持久化数据管理,配合
@Observable使用可以构建完整的数据驱动架构 - AppKit modernization — AppKit 的新特性和现代化改造方向
- Bring multiple windows to your SwiftUI app (WWDC22) — 演讲者推荐的参考视频,深入讲解 SwiftUI Scene 的用法
评论
GitHub Issues · utterances