AppKit 的新功能
What’s new in AppKit
2024年6月10日
一句话判断
macOS Sequoia 的 AppKit 更新是近几年来最实在的一批:Window Tiling、SwiftUI Menu 和 Animation 集成、Writing Tools 自动适配,每一个都是 Mac 开发者日常会用到的功能,值得逐条过一遍。
这场 Session 讲了什么
macOS Sequoia 给 AppKit 带来了一波系统级新功能和框架层面的 API 改进。系统级方面,Writing Tools 提供了拼写检查之外的进阶写作辅助——结构、清晰度、语气都能帮你调整,而且你的 app 自动获得这些能力。Genmoji 让用户可以创建自定义表情(注意,这些是图片而非 Unicode 字符,显示和存储需要额外适配)。Image Playground 则是一个全新的图像生成体验,通过 ImagePlaygroundViewController 可以几步就集成到 app 中。
Window Tiling 是今年 macOS 最直观的新功能之一。把窗口拖到屏幕边缘就能自动填充半屏或四分之一屏,按住 Option 拖动可以快速预览最近边缘的吸附效果,菜单栏和标题栏都提供了排列选项,而且两个并排的窗口可以同步调整大小。这个功能对所有现有 app 自动生效,但要做好需要检查窗口的最小/最大尺寸约束。
框架层面的改进更加重磅。NSHostingMenu 让你可以用 SwiftUI 定义菜单并在 AppKit 中使用,实现了菜单代码的跨框架共享。更激动人心的是,NSView 现在支持用 SwiftUI Animation 类型做动画,包括自定义动画,而且这些动画是可中断、可重定向的。
值得深挖的点
SwiftUI Animation 与 NSView 的融合标志着两个框架的边界在消融
NSAnimationContext 现在可以接受 SwiftUI Animation 类型,这意味着你在 AppKit 中也能用上 SwiftUI 的完整动画工具集——spring 动画、自定义 timing curve、甚至自定义 Animation 协议的实现。而且这些动画继承了 SwiftUI 动画的核心优势:可中断和可重定向。
这个改动的重要性不在于技术本身,而在于它释放的信号。Apple 一直在说 SwiftUI 和 AppKit 可以共存,但”共存”和”融合”是两个层次。能用 SwiftUI 定义菜单(NSHostingMenu)、用 SwiftUI Animation 驱动 NSView 动画,说明两个框架正在从”可以一起用”进化到”共享同一套抽象”。对于大型 Mac app 的开发者来说,这意味着你可以渐进式地引入 SwiftUI 的表达能力,而不需要重写整个 UI 层。
Window Tiling 的适配细节比你想象的多
Window Tiling 自动生效不等于不需要适配。你需要关注几个方面:窗口的最小尺寸是否能容纳半屏或四分之一屏的布局;resizeIncrements 属性是否设置正确(比如 Terminal 中以字符宽高为增量);新窗口的级联定位是否合理——Sequoia 新增了 cascadingReferenceFrame 属性来获取已有窗口的未吸附 frame,用于正确定位新窗口。
容易忽略的一点是,当你使用 NSWindowController 时,窗口级联定位在 Sequoia 中已经自动处理了。但如果你手动管理窗口创建,就需要自己用新 API 来确保行为一致。
代码片段
使用 NSHostingMenu 在 AppKit 中嵌入 SwiftUI 菜单
import SwiftUI
import AppKit
// 用 SwiftUI 定义菜单内容
struct MyMenuContent: View {
@Binding var isFeatureEnabled: Bool
@Binding var selectedMode: DisplayMode
var body: some View {
// Toggle 用于开关选项
Toggle("启用高级模式", isOn: $isFeatureEnabled)
// Picker 用于多选一
Picker("显示模式", selection: $selectedMode) {
Text("紧凑").tag(DisplayMode.compact)
Text("标准").tag(DisplayMode.standard)
Text("宽松").tag(DisplayMode.relaxed)
}
Divider()
// Button 用于执行操作
Button("重置为默认值") {
resetToDefaults()
}
}
}
// 在 AppKit 中使用
let hostingMenu = NSHostingMenu(content: MyMenuContent(
isFeatureEnabled: $settings.isFeatureEnabled,
selectedMode: $settings.displayMode
))
// 可以用在任何接受 NSMenu 的地方
let popupButton = NSPopUpButton(pullsDown: true)
popupButton.menu = hostingMenu
场景:你的 Mac app 部分用 SwiftUI 部分用 AppKit,现在可以把菜单定义统一到 SwiftUI 中。坑点:Binding 的生命周期要和菜单保持一致,菜单销毁时 Binding 不能指向已释放的对象。
用 SwiftUI Animation 驱动 NSView 动画
// 在 AppKit 中使用 SwiftUI 动画类型
NSAnimationContext.runAnimationGroup { context in
// 使用 SwiftUI 的 spring 动画
context.animation = .spring(duration: 0.4, bounce: 0.2)
// 调整布局或绘图属性
myView.frame.size.width = expandedWidth
myView.animator().alphaValue = 1.0
}
// 自定义动画也是支持的
NSAnimationContext.runAnimationGroup { context in
context.animation = .custom(myCustomAnimation)
myCustomView.frame.origin = CGPoint(x: targetX, y: targetY)
}
场景:给 AppKit 视图添加流畅的交互动画。坑点:动画是可中断的,如果用户在动画进行中触发了新操作,确保你的状态管理能正确处理动画中断。
集成 Image Playground
import AppKit
import ImagePlayground
// 初始化 Image Playground 视图控制器
let playgroundVC = ImagePlaygroundViewController()
playgroundVC.delegate = self
// 可选:设置初始概念和参考图片
playgroundVC.concepts = [.text("日落时分的海滩")]
playgroundVC.sourceImage = referenceImage
// 以 sheet 方式弹出
presentAsSheet(playgroundVC)
// 实现代理方法处理生成的图片
func imagePlaygroundViewController(_ viewController: ImagePlaygroundViewController,
didCreateImageAt imageURL: URL) {
// imageURL 位于 app 沙盒的临时目录中
// 需要及时将图片移动到持久化存储位置
insertImage(from: imageURL)
dismiss(playgroundVC)
}
场景:让你的 Mac app 支持 AI 图像生成。坑点:生成的图片 URL 在临时目录中,你需要在代理回调中及时将其移动到持久化存储位置。
最佳实践
- 检查窗口尺寸约束:在 Sequoia 上跑一遍你的 app,尝试用 Window Tiling 把窗口拖到各种位置,确认最小尺寸不会导致布局崩溃。
- 用
cascadingReferenceFrame处理新窗口定位:如果你手动创建窗口,用这个新属性获取已有窗口的未吸附 frame 来计算级联偏移。 - Writing Tools 零成本适配:对于标准 NSTextView,Writing Tools 自动生效。但如果你做了高级文本处理,需要看《Get started with Writing Tools》了解更多交互行为细节。
- Genmoji 的图片本质:新表情是图片而非字符,如果你的 app 有文本存储和渲染的自定义逻辑,需要确认能正确处理内联图片。
- 键盘触发上下文菜单:Sequoia 新增 Control-Return 快捷键打开上下文菜单,如果你的 app 自定义了菜单呈现位置,需要处理键盘触发的场景。
还有什么值得关注
- 新的
NSCursorAPI 提供了更多光标类型选择,适合绘图和设计类应用。 - SF Symbols 有更新,部分符号新增了动画支持。
NSDocument的保存流程有了新的便捷 API,减少了模板代码。- Toolbar 控制能力增强,可以更精细地定制标题栏中工具栏项的行为。