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

What's new in AppKit

观看原视频

Highlight

macOS Ventura 为 AppKit 应用加入 Stage Manager 窗口协作规则、Settings 风格表单、NSComboButtonNSColorWell 新样式、工具栏自定义 API、SF Symbols 4 可变符号,以及新的分享与协作入口。


核心内容

macOS Ventura 改了很多 Mac 应用的日常细节。

这些细节看起来分散:窗口、设置页、按钮、颜色选择、工具栏、表格、符号、分享。它们背后是同一个问题:Mac 应用需要更好地适应系统级工作流。

Stage Manager 会把不活跃的窗口收起来,让当前窗口留在中央。文档窗口适合这种行为。面板、弹窗、Settings、浮动工具则不应该挤走当前窗口。AppKit 现在会根据窗口类型和 collectionBehavior 帮 Stage Manager 判断窗口角色。

Settings 也变了。系统应用从 System Preferences 改名为 System Settings,控制密集的设置界面采用新的表单视觉样式。AppKit 会自动把应用菜单里的 “Preferences” 更新为 “Settings”,但窗口标题、说明文字和本地化字符串仍然要开发者自己检查。

控件层面,NSComboButton 把主操作和下拉菜单放进同一个控件。NSColorWell 获得新的外观、minimal 与 expanded 两种样式。NSToolbar 增加了更细的自定义约束。NSTableView 对可变行高表格改用懒计算,超大表格首次加载更快。

最后,SF Symbols 4 增加 450 多个符号和可变符号。分享面板也升级为更丰富的 popover,并提供预览元数据、标准菜单项和协作邀请入口。

详细内容

Stage Manager 需要窗口声明自己的角色

01:00)Stage Manager 会把非活跃窗口移出工作区,让活跃窗口位于中央。演讲明确区分了两类窗口:文档一类的 primary window 应该进入这种交换模型;panel、popover、settings、modal window 等辅助窗口应该留在当前窗口之上。

AppKit 已经有 NSWindow API 描述这些行为。Stage Manager 会尊重窗口的 collectionBehavior。如果包含 .auxiliary.moveToActiveSpace.stationary.transient,窗口不会把中央的活动窗口挤走。

// 概念代码:transcript 明确列出这些 collectionBehavior 选项。
let panel: NSWindow = makeInspectorPanel()

panel.collectionBehavior.insert(.auxiliary)
panel.collectionBehavior.insert(.moveToActiveSpace)

关键点:

  • panel 代表检查器、工具面板这类辅助窗口。
  • .auxiliary 告诉系统这个窗口服务于当前工作流,角色不同于主文档窗口。
  • .moveToActiveSpace 让窗口跟随当前 Space,避免用户切换工作区后丢失上下文。
  • Stage Manager、Space、全屏都会读取同一组 collectionBehavior 信息。

这部分的实践很直接:不要把所有窗口都当作文档窗口。Settings、面板、临时工具和弹窗应该用合适的窗口类型或 collection behavior 表达意图。

Settings 风格表单适合增量采用 SwiftUI

02:42)System Preferences 在 Ventura 改名为 System Settings。AppKit 会在使用新 SDK 构建后,自动把应用菜单里的 “Preferences” 改为 “Settings”。演讲同时提醒,窗口标题、描述标签和其他控件里的本地化文字要自己搜索和更新。

04:23)新的 Settings 界面使用控制密集的表单样式。SwiftUI 的 Form 可以直接采用这种视觉语言,适合从独立的 Settings 窗口开始增量引入 SwiftUI。

enum AirDropVisbility: String, CaseIterable, Identifiable {
    case nobody = "No One"
    case contactsOnly = "Contacts Only"
    case everyone = "Everyone"

    var id: String { rawValue }
    var label: String { rawValue }
    var symbolName: String {
        switch self {
        case .nobody: return "person.crop.circle.badge.xmark"
        case .contactsOnly: return "person.2.circle"
        case .everyone: return "person.crop.circle.badge.checkmark"
        }
    }
}

struct ExampleFormView: View {
    @State private var name: String = "Mac Studio"
    @State private var screenSharingEnabled: Bool = true
    @State private var fileSharingEnabled: Bool = false
    @State private var airdropVisibility = AirDropVisbility.contactsOnly

    var body: some View {
        Form {
            TextField("Computer Name", text: $name)
            Toggle("Screen Sharing", isOn: $screenSharingEnabled)
            Toggle("File Sharing", isOn: $fileSharingEnabled)
            Picker("AirDrop", selection: $airdropVisibility) {
                ForEach(AirDropVisbility.allCases) {
                    Label($0.label, systemImage: $0.symbolName)
                        .labelStyle(.titleAndIcon)
                        .tag($0)
                }
            }
        }
        .formStyle(.grouped)
    }
}

关键点:

  • AirDropVisbility 用枚举定义 Settings 中的选项,并提供 label 与 SF Symbol 名称。
  • @State 保存表单状态,适合 Settings 窗口中立即可编辑的选项。
  • Form 放入 TextFieldTogglePicker,让界面结构跟设置项一致。
  • Label(..., systemImage:) 给 Picker 选项加上系统符号。
  • .labelStyle(.titleAndIcon) 同时显示文字与图标。
  • .formStyle(.grouped) 应用演讲中的 grouped form style,让视觉、滚动和布局由 SwiftUI 处理。

如果应用已经有 AppKit Settings 窗口,可以先把一组独立设置做成 SwiftUI Form,再嵌入现有窗口。演讲把 Settings 窗口称为采用 SwiftUI 的好入口,因为它通常是应用界面中的独立区域。

新控件减少自制组合控件

05:37)AppKit 在 Ventura 增加 NSComboButton。它把一次点击的主操作和额外选项菜单合并到同一个控件中。Mail 里的“移动到预测文件夹”就是典型场景:默认位置一键可达,其他文件夹仍然可以从菜单选择。

NSComboButton 有两种样式。split 样式有独立的箭头区域,专门打开菜单。unified 样式看起来更像普通按钮,点击执行主操作,长按显示菜单。

// 概念代码:transcript 只说明控件角色与两种样式。
let archiveButton = NSComboButton()

archiveButton.menu = archiveMenu
archiveButton.style = .split
archiveButton.target = self
archiveButton.action = #selector(archiveToSuggestedMailbox(_:))

关键点:

  • NSComboButton 用一个控件表达“默认动作 + 更多选项”。
  • menu 承载额外目标,例如移动到其他邮箱或文件夹。
  • .split 对应演讲中的默认样式,箭头区域只负责菜单。
  • targetaction 保留普通按钮的一次点击主操作。

07:21NSColorWell 的默认外观自动更新,不需要迁移代码。它还增加 minimal 和 expanded 两种样式。minimal 样式在 rollover 时显示 disclosure arrow,并弹出快速调色板。expanded 样式把快速选择和完整 NSColorPanel 放在同一个控件里。

// 概念代码:transcript 明确列出 colorWellStyle 的三个 case。
let colorWell = NSColorWell()

colorWell.colorWellStyle = .minimal

关键点:

  • NSColorWell 默认外观会自动现代化。
  • .minimal 适合工具栏或检查器里占位较小的颜色入口。
  • .expanded 适合 iWork 式的明确入口:左侧快速选择,右侧打开完整面板。
  • pulldown target/action 可以接管下拉区域,用自定义 popover 或菜单替代系统调色板。

工具栏自定义可以写出规则

09:40NSToolbar 的新 API 解决两个常见问题:有些项目不能被用户移走;有些项目只能出现在工具栏的某些区域。

演讲列出两个 delegate 方法。toolbarImmovableItemIdentifiers 返回不能移动或删除的 item 标识。toolbar(_:itemIdentifier:canBeInsertedAt:) 可以对重排、插入、移除进行判断。

// 概念代码:方法名来自 transcript,用于展示规则位置。
func toolbarImmovableItemIdentifiers(_ toolbar: NSToolbar) -> Set<NSToolbarItem.Identifier> {
    [filterItemIdentifier]
}

func toolbar(
    _ toolbar: NSToolbar,
    itemIdentifier: NSToolbarItem.Identifier,
    canBeInsertedAt index: Int
) -> Bool {
    allowedSection.contains(index)
}

关键点:

  • toolbarImmovableItemIdentifiers 适合 Mail 过滤按钮这类必须停在固定位置的项目。
  • 返回的 identifier 在自定义模式里不会被移动或删除。
  • canBeInsertedAt 让应用按位置判断插入是否合法。
  • allowedSection.contains(index) 表达“只能放在某一段”的规则。

10:56centeredItemIdentifiers 支持多个居中项目。工具栏可自定义时,这些项目可以增删,但只能在居中组内重排。NSToolbarItem.possibleLabels 用于可变标题。工具栏会按最长标题计算宽度,避免状态切换时其他项目左右跳动。

// 概念代码:属性名来自 transcript。
toolbar.centeredItemIdentifiers = [cropIdentifier, adjustIdentifier, filterIdentifier]

muteItem.possibleLabels = ["Mute", "Unmute"]

关键点:

  • centeredItemIdentifiers 把一组工具固定在工具栏中央区域。
  • 居中组内部仍然可以按用户设置重排。
  • possibleLabels 预先告诉工具栏同一个 item 会出现哪些本地化标题。
  • 工具栏按最长标题预留宽度,状态切换时布局保持稳定。

可变行高表格不再急着测量所有行

13:43NSTableView 过去为了滚动体验,会提前计算所有行高。可变行高表格在数据量很大时,首次加载会被这些计算拖慢。

Ventura 改成懒计算。NSTableView 只测量当前 viewport 附近的行。未测量行使用已测量行得出的运行估算高度。滚动时,表格再请求真实行高,并维持正确的滚动位置。

// 概念代码:delegate 回调仍然存在,但调用时机在 Ventura 中改变。
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
    heightCache[row] ?? estimatedHeight(for: row)
}

关键点:

  • heightOfRow 仍然是提供行高的位置。
  • Ventura 会按需要请求行高,不再保证启动时一次性请求全部行。
  • 代码不要依赖 delegate 调用的具体时机。
  • 优化自动应用于 NSTableView 和 SwiftUI List,不需要应用 opt in。

SF Symbols 4 增加 preferred rendering mode 和 variable symbols

15:32)SF Symbols 4 在 Ventura 增加 450 多个新符号。符号渲染模式仍然包括 monochrome、hierarchical、palette 和 multicolor。新增的 preferred rendering mode 允许符号声明自己更适合的默认渲染方式。运行时 AppKit 会自动使用这个偏好。

17:28)Variable symbol 用一个 0 到 1 之间的浮点值表达数量。符号内部有阈值,路径会根据传入值变化。演讲举了 Wi‑Fi 信号强度和音量的例子。

// 概念代码:transcript 说明新增 initializer 比现有 symbol initializer 多 value 参数。
let signalLevel = 0.75
let image = NSImage(
    systemSymbolName: "wifi",
    variableValue: signalLevel,
    accessibilityDescription: "Wi-Fi signal"
)

关键点:

  • systemSymbolName 选择 SF Symbol。
  • variableValue 是 0 到 1 的浮点数。
  • 符号如果没有定义 variable thresholds,这个值会被忽略。
  • 符号如果定义了 thresholds,路径会按数值变化。
  • variable symbol 可以和 palette、multicolor 等渲染方式配合。

分享入口升级为 popover,并支持协作邀请

18:50)Ventura 的分享体验从旧的 share menu 升级为信息更丰富的 sharing popover。它能展示文档信息和 suggested people,同时保留原来 NSSharingServicePicker 的 API 与 delegate 方法。

如果分享的是 file URL,NSSharingServicePicker 可以自动填充图标、名称和文件元数据。如果分享的是自定义类型,可以让 item 遵循 NSPreviewRepresentableActivityItem,提供底层分享对象、标题、图片 provider 和图标 provider。AppKit 还提供 NSPreviewRepresentingActivityItem 作为便利类。

// 概念代码:API 名称来自 transcript,用于展示自定义分享 item 的组成。
let previewItem = NSPreviewRepresentingActivityItem(
    item: itemProvider,
    title: documentTitle,
    image: previewImage,
    icon: documentIcon
)

let picker = NSSharingServicePicker(items: [previewItem])

关键点:

  • NSPreviewRepresentingActivityItem 把分享对象和预览元数据放在一起。
  • itemProvider 是真正要分享的内容。
  • titleimageicon 用于 sharing popover 的头部展示。
  • 图片生成成本较高时,演讲建议用 NSItemProvider 延迟提供。

20:44)如果分享入口来自菜单,NSSharingServicePicker 可以创建 standardShareMenuItem。把它加入主菜单或上下文菜单后,用户选择菜单项会打开标准 sharing popover。上下文菜单场景中,popover 会锚定到产生菜单的 view。

// 概念代码:属性名来自 transcript。
let shareItem = picker.standardShareMenuItem
contextMenu.addItem(shareItem)

关键点:

  • standardShareMenuItem 避免手工枚举 sharing services。
  • 菜单入口仍然进入系统标准 sharing popover。
  • 应用可以继续使用 picker 的过滤和自定义服务能力。
  • 协作型应用可以继续研究 CloudKit、iCloud Drive 或自有服务器接入的邀请流程。

核心启发

1. 做一个 Stage Manager 适配清单

  • 做什么:遍历应用里的文档窗口、Settings 窗口、检查器、浮动面板和弹窗,给每类窗口写明角色。
  • 为什么值得做:Stage Manager 会根据窗口类型和 collectionBehavior 决定是否把现有窗口移出中央区域。
  • 怎么开始:检查辅助窗口是否应该使用 .auxiliary.moveToActiveSpace.stationary.transient

2. 把 Settings 的一个页面迁移到 SwiftUI Form

  • 做什么:选择一组独立设置,用 SwiftUI Form 重写,并应用 .formStyle(.grouped)
  • 为什么值得做:Settings 窗口通常边界清晰,适合在 AppKit 应用中增量引入 SwiftUI。
  • 怎么开始:从只有 TextFieldTogglePicker 的页面开始,保留原来的 AppKit 窗口容器。

3. 用 NSComboButton 替换双按钮或分段控件

  • 做什么:把“默认动作 + 更多目标”的控件合并成一个 NSComboButton
  • 为什么值得做:演讲明确提到过去常用 segmented control 拼出这种交互,现在有专用控件。
  • 怎么开始:找出工具栏里相邻的主按钮和下拉按钮,主动作放在 button action,额外选择放进 menu。

4. 给工具栏自定义增加规则

  • 做什么:限制关键 toolbar item 的移动、删除和可插入位置。
  • 为什么值得做:Ventura 的 NSToolbar delegate 新增了不可移动项目和插入位置判断。
  • 怎么开始:实现 toolbarImmovableItemIdentifiers,再用 canBeInsertedAt 限制只能出现在特定区域的 item。

5. 给分享对象补齐预览元数据

  • 做什么:让自定义分享内容在 sharing popover 中显示标题、预览图和图标。
  • 为什么值得做:Ventura 的分享 popover 会展示更多文档信息,并支持 suggested people 与协作入口。
  • 怎么开始:对自定义分享对象使用 NSPreviewRepresentableActivityItemNSPreviewRepresentingActivityItem,菜单入口使用 standardShareMenuItem

关联 Session

评论

GitHub Issues · utterances