Highlight
macOS Sonoma 为 AppKit 带来了列自定义菜单、检查器面板、符号动画效果、菜单重写、合作式应用激活、NSView 默认不裁剪绘制内容等改进,让 Mac 应用开发更简洁高效。
核心内容
控件 API 的进化
过去,给 NSTableView 添加列自定义菜单需要开发者自己实现完整的 UI 和状态保存逻辑。代码量不小,还要处理本地化。macOS Sonoma 引入了 tableView(_:userCanChangeVisibilityOf:) 委托方法,三行代码就能让 AppKit 接管一切——包括菜单本地化、列状态恢复。
NSProgressIndicator 也终于和 Foundation 的 Progress 类型打通了。以前要手动监听进度变化、更新 UI,现在直接赋值给 observedProgress 属性,后台线程的进度变化会自动同步到进度条。
按钮的 bezel style 也做了现代化改造。新增的 .automatic 样式会根据按钮所在位置自动选择最合适的样式:在窗口中用 push button,在 toolbar 中用 toolbar 样式。旧的样式名称从描述外观改成了描述语义用途,比如 “Recessed” 变成了 “Accessory Bar”,意图更清晰。
检查器面板:终于不用自己拼
Mac 应用里的右侧面板(inspector)是常见设计模式,但以前没有标准实现,每个开发者都要自己用 NSSplitView 拼凑。 Sonoma 新增了 inspectorWithViewController 初始化器,配合 inspectorTrackingSeparator 和 .toggleInspector toolbar item,一套标准的检查器面板几分钟就能搭好。而且支持回退到 macOS Big Sur。
菜单重写:性能提升 + 新功能
AppKit 的菜单实现从底层重写,完全基于 Cocoa。内存和 CPU 占用大幅降低。同时带来了四个新功能:
- Section headers:用
sectionHeader(title:)一行代码创建分组标题 - Palette menus:水平排列的菜单项,适合做颜色选择器等
- Selection behaviors:
.selectAny和.selectOne控制多选/单选逻辑 - Badges:菜单项支持字符串徽章和计数徽章(新消息、提醒、更新)
合作式应用激活
以前调用 activate(ignoringOtherApps:) 会强制切换应用,打断用户输入。Sonoma 改成了请求式激活,系统会判断当前上下文是否适合切换。新增的 yieldActivation API 让当前活跃应用可以主动把激活权交给目标应用,切换更优雅。旧的 API 已废弃。
NSView 不再默认裁剪
NSView 默认 clipsToBounds = true 导致很多绘制问题:文字阴影被截断、徽章显示不全、复杂字形超出边界。 Sonoma 把默认行为改成了不裁剪,同时 hit testing 仍按几何边界计算。开发者可以用新的 clipsToBounds 属性(回退到 10.9)按需控制。
符号效果与 HDR
SF Symbols 支持动画效果了:bounce、replace、pulse。用 addSymbolEffect 一行代码就能给 symbol image 加上动画。NSImageView 也支持 HDR 内容显示,在 XDR 屏幕上自动呈现高动态范围。
Swift 和 SwiftUI 集成
NSColor、NSShadow等线程安全类遵循Sendable协议NSImage、NSColor、NSSound遵循Transferable,方便在 SwiftUI 中做拖拽和分享@ViewLoading和@WindowLoading属性包装器消除可选类型- Xcode 15 的 Preview 宏支持 AppKit 视图预览
- SwiftUI 的
toolbar、navigationTitle等修饰符现在对 NSWindow 生效
详细内容
列自定义菜单
(01:36)
func tableView(_ tableView: NSTableView,
userCanChangeVisibilityOf column: NSTableColumn) -> Bool {
return column.identifier != "Name"
}
关键点:
userCanChangeVisibilityOf是 NSTableViewDelegate 的新方法- 返回
true表示该列可以被用户隐藏 - AppKit 自动生成菜单、处理本地化、保存恢复隐藏状态
Progress 与 NSProgressIndicator 联动
(01:53)
func fetchData() {
let url = URL(string: "https://developer.apple.com/wwdc23/")!
let task = URLSession.shared.dataTask(with: .init(url: url))
progressIndicator.observedProgress = task.progress
task.resume()
}
关键点:
observedProgress是 NSProgressIndicator 的新属性- 赋值后进度条自动跟随 Progress 对象更新
- 支持后台线程,不需要手动切回主线程
添加检查器面板
(03:48)
let inspectorItem = NSSplitViewItem(inspectorWithViewController: inspectorViewController)
splitViewController.addSplitViewItem(inspectorItem)
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[.toggleSidebar, .sidebarTrackingSeparator, .flexibleSpace, .addPlant,
.inspectorTrackingSeparator, .flexibleSpace, .toggleInspector]
}
关键点:
inspectorWithViewController创建全高度右侧面板inspectorTrackingSeparator放在 toggleInspector 前面,让按钮对齐面板位置- 支持回退到 macOS Big Sur
Toolbar 弹出框
(04:38)
func toolbarAction(_ toolbarItem: NSToolbarItem) {
let popover = NSPopover()
popover.contentViewController = PopoverViewController()
popover.show(relativeTo: toolbarItem)
}
关键点:
show(relativeTo:)支持以 toolbar item 为锚点- toolbar item 在溢出菜单中时,popover 自动锚定到溢出按钮
符号动画效果
(18:30)
wifiImageView.image = NSImage(systemSymbolName: "wifi", accessibilityDescription: "wifi icon")
wifiImageView.addSymbolEffect(.variableColor.iterative, options: .repeating)
关键点:
- 只对 symbol image 有效
.variableColor.iterative是逐层变色效果.repeating让动画循环播放
@ViewLoading 消除可选类型
(24:56)
class ViewController: NSViewController {
@ViewLoading var datePicker: NSDatePicker
var date = Date() {
didSet {
datePicker.dateValue = date
}
}
override func loadView() {
super.loadView()
datePicker = NSDatePicker()
datePicker.dateValue = date
view.addSubview(datePicker)
}
}
关键点:
@ViewLoading确保属性在loadViewIfNeeded后可用- 不需要把属性声明为 Optional
- 访问时不需要解包或 guard
AppKit 视图预览
(25:26)
#Preview("Tree Species") {
let treeCellView = TreeCellView()
treeCellView.species = .spruce
return treeCellView
}
关键点:
- Xcode 15 的 Preview 宏支持 NSView 和 NSViewController
- 修改代码后预览自动更新
- 支持命名预览,方便在预览面板中识别
核心启发
-
给表格加上列自定义菜单
- 做什么:让 Mac 应用的表格支持用户自定义显示哪些列
- 为什么值得做:三行代码获得原生菜单、本地化、状态恢复,零成本提升用户体验
- 怎么开始:实现
tableView(_:userCanChangeVisibilityOf:)委托方法
-
用检查器面板替代自定义右侧面板
- 做什么:把现有的自定义右侧面板改成标准 inspector
- 为什么值得做:获得全窗口高度、标准 toolbar 切换按钮、自动布局,代码更少
- 怎么开始:用
NSSplitViewItem(inspectorWithViewController:)替换现有 split view item
-
给状态变化加上符号动画
- 做什么:在 WiFi 连接、蓝牙配对、电池充电等状态变化时播放 symbol 动画
- 为什么值得做:用视觉反馈替代弹窗提示,界面更生动
- 怎么开始:创建 symbol image,调用
addSymbolEffect(.bounce)或.variableColor
-
用 Preview 宏加速 AppKit UI 开发
- 做什么:给自定义 NSView/NSViewController 添加 Xcode Preview
- 为什么值得做:不用编译运行就能看到 UI 效果,调整布局更快
- 怎么开始:在视图文件顶部加
#Preview { YourView() }
-
在 SwiftUI 中复用 AppKit 的 Transferable 类型
- 做什么:在 SwiftUI 视图里拖拽 NSImage、NSColor
- 为什么值得做:AppKit 类型现在遵循 Transferable,可以直接用于 SwiftUI 的 Drag and Drop
- 怎么开始:在 SwiftUI 中使用
.draggable(image),其中 image 是 NSImage
关联 Session
- What’s new in SwiftUI — SwiftUI 新特性,与 AppKit 集成相关
- Animate symbols in your app — SF Symbols 动画效果的详细介绍
- Build programmatic UI with Xcode Previews — 使用 Preview 宏预览 AppKit 视图
- Support HDR images in your apps — NSImageView HDR 支持的深入讲解
评论
GitHub Issues · utterances