WWDC Quick Look 💓 By SwiftGGTeam
What's new in AppKit

What's new in AppKit

观看原视频

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 集成

  • NSColorNSShadow 等线程安全类遵循 Sendable 协议
  • NSImageNSColorNSSound 遵循 Transferable,方便在 SwiftUI 中做拖拽和分享
  • @ViewLoading@WindowLoading 属性包装器消除可选类型
  • Xcode 15 的 Preview 宏支持 AppKit 视图预览
  • SwiftUI 的 toolbarnavigationTitle 等修饰符现在对 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
  • 修改代码后预览自动更新
  • 支持命名预览,方便在预览面板中识别

核心启发

  1. 给表格加上列自定义菜单

    • 做什么:让 Mac 应用的表格支持用户自定义显示哪些列
    • 为什么值得做:三行代码获得原生菜单、本地化、状态恢复,零成本提升用户体验
    • 怎么开始:实现 tableView(_:userCanChangeVisibilityOf:) 委托方法
  2. 用检查器面板替代自定义右侧面板

    • 做什么:把现有的自定义右侧面板改成标准 inspector
    • 为什么值得做:获得全窗口高度、标准 toolbar 切换按钮、自动布局,代码更少
    • 怎么开始:用 NSSplitViewItem(inspectorWithViewController:) 替换现有 split view item
  3. 给状态变化加上符号动画

    • 做什么:在 WiFi 连接、蓝牙配对、电池充电等状态变化时播放 symbol 动画
    • 为什么值得做:用视觉反馈替代弹窗提示,界面更生动
    • 怎么开始:创建 symbol image,调用 addSymbolEffect(.bounce).variableColor
  4. 用 Preview 宏加速 AppKit UI 开发

    • 做什么:给自定义 NSView/NSViewController 添加 Xcode Preview
    • 为什么值得做:不用编译运行就能看到 UI 效果,调整布局更快
    • 怎么开始:在视图文件顶部加 #Preview { YourView() }
  5. 在 SwiftUI 中复用 AppKit 的 Transferable 类型

    • 做什么:在 SwiftUI 视图里拖拽 NSImage、NSColor
    • 为什么值得做:AppKit 类型现在遵循 Transferable,可以直接用于 SwiftUI 的 Drag and Drop
    • 怎么开始:在 SwiftUI 中使用 .draggable(image),其中 image 是 NSImage

关联 Session

评论

GitHub Issues · utterances