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

What's new in AppKit

观看原视频

Highlight

macOS Monterey 为 AppKit 带来五大更新:控件自定义着色、SF Symbols 3 分层渲染、TextKit 2 非线性排版引擎、Swift 5.5 并发特性深度集成、以及 Shortcuts 自动化支持。

核心内容

控件着色与按钮设计更新

macOS Monterey 允许为单个按钮、分段控件和滑块设置自定义着色,通过 bezelColorselectedSegmentColortrackFillColor 实现。这在 Touch Bar 上已可用,现在扩展到窗口内控件。

一个关键设计变化:Push Button 点击时不再使用强调色高亮,而是与其他可点击元素保持一致。自定义绘制时需要使用 interiorBackgroundStyle 判断底层样式。

SF Symbols 3 的分层渲染

SF Symbols 3 引入两种新渲染模式:

  • Hierarchical:单一颜色,通过不透明度区分层次
  • Palette:为每一层单独指定颜色

还支持符号变体映射,如从空心 heart 自动获取填充版本。

TextKit 2 的非线性布局

TextKit 2 采用非线性布局系统,大文档只需排版可见区域附近的段落,避免从头到尾线性计算。TextEdit 的纯文本文档和 AppKit 文本字段已在 Big Sur 中悄悄使用 TextKit 2。

Swift 并发集成

AppKit 将 NSResponderNSViewNSViewController 等核心类标记为 @MainActor,编译器层面保证 UI 操作在主线程执行。NSColorSampler 等异步 API 提供 async/await 版本。

详细内容

控件着色与背景样式判断(04:18

// 为按钮设置语义化颜色
let preorderButton = NSButton(title: "Preorder", target: self, action: #selector(preorder))
preorderButton.bezelColor = NSColor.systemOrange

// 为分段控件设置选中颜色
let segmentControl = NSSegmentedControl()
segmentControl.selectedSegmentColor = NSColor.systemGreen

// 为滑块设置轨道填充色
let slider = NSSlider()
slider.trackFillColor = NSColor.systemBlue

自定义绘制时判断背景样式:

class CustomButtonCell: NSButtonCell {
    override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
        // interiorBackgroundStyle 反映按钮 bezel 的底层样式
        // .normal — 无色状态(普通按钮)
        // .emphasized — 彩色/强调状态(着色按钮、默认按钮、toggle 开启状态)
        if interiorBackgroundStyle == .emphasized {
            // 在彩色背景上绘制白色内容
            drawWhiteContent()
        } else {
            // 在无色背景上绘制深色内容
            drawDarkContent()
        }
    }
}

关键点:

  • 着色按钮在所有激活状态都显示着色,与普通按钮的白色/灰色不同
  • 避免着色按钮与默认按钮(Default Button)混淆,两者都有彩色外观
  • 必须用标签或图标辅助传达控件用途,不能仅靠颜色
  • interiorBackgroundStyle 替代检查 highlight 状态来判断绘制策略

颜色拾取器(14:40

@IBAction func pickColor(_ sender: Any?) {
    Task {
        guard let color = await NSColorSampler().sample() else { return }
        textField.textColor = color
    }
}

关键点:

  • NSColorSampler().sample() 是 async 方法,用户从屏幕任意位置选取颜色
  • 调用后线程让出,等待用户完成选择后继续执行
  • 可以直接用在 guard let 中,代码比 completion handler 更自然

菜单快捷键自动本地化

macOS Monterey 自动处理菜单快捷键的本地化:

// 自动映射不可用的快捷键
// Command-backslash 在日文键盘上没有反斜杠键
// 系统会自动映射为等效的、可输入的快捷键

// 方向性快捷键在 RTL 语言中自动镜像
// Command-[ 在 RTL 语言中自动变为 Command-]

// 对于绝对方向的操作(如左对齐),禁用镜像
let alignLeftItem = NSMenuItem(title: "Align Left", action: #selector(alignLeft), keyEquivalent: "[")
alignLeftItem.allowsAutomaticKeyEquivalentMirroring = false

// 完全禁用某个菜单项的本地化
let customItem = NSMenuItem(title: "Custom", action: #selector(custom), keyEquivalent: "k")
customItem.allowsAutomaticKeyEquivalentLocalization = false

// 整个应用禁用自动本地化
func applicationShouldAutomaticallyLocalizeKeyEquivalents(_ application: NSApplication) -> Bool {
    return false
}

关键点:

  • 大多数应用不需要手动干预,系统自动处理
  • 仅在快捷键有绝对方向含义时禁用镜像(如 Align Left)
  • 应用级禁用是最后手段,优先使用菜单项级控制

Swift 属性包装器自动刷新视图

class ConfigurableView: NSView {
    // 属性变化时自动调用 setNeedsDisplay
    @Invalidating(.display) var foregroundColor: NSColor = .label

    // 属性变化时自动调用 setNeedsLayout
    @Invalidating(.layout) var spacing: CGFloat = 8

    // 属性变化时自动调用 setNeedsUpdateConstraints
    @Invalidating(.constraints) var maxWidth: CGFloat = 200

    // 属性变化时自动调用 invalidateIntrinsicContentSize
    @Invalidating(.intrinsicContentSize) var title: String = ""

    // 组合多个无效化操作
    @Invalidating(.display, .layout) var cornerRadius: CGFloat = 0
}

关键点:

  • @InvalidatingNSView 的嵌套属性包装器
  • 属性类型必须遵循 Equatable,包装器只在值真正变化时才执行无效化
  • 内置支持 .display.layout.constraints.intrinsicContentSize.restorableState
  • 可通过 NSViewInvalidating 协议定义自定义无效化行为

Shortcuts 集成

AppKit 应用通过 Services 支持 Shortcuts:

// 实现 validRequestor 方法,声明可接受/提供的数据类型
extension MyView: NSServicesMenuRequestor {
    override func validRequestor(forSendType sendType: NSPasteboard.PasteboardType?,
                                 returnType: NSPasteboard.PasteboardType?) -> Any? {
        if returnType == .string {
            return self
        }
        return super.validRequestor(forSendType: sendType, returnType: returnType)
    }

    func readSelection(from pboard: NSPasteboard) -> Bool {
        // 从 pasteboard 读取数据
        return true
    }

    func writeSelection(to pboard: NSPasteboard, types: [String]) -> Bool {
        // 向 pasteboard 写入数据
        return true
    }
}

关键点:

  • 如果应用已支持 Services,则已自动支持 Shortcuts
  • Shortcuts 出现在 Services 菜单所在的所有位置
  • AppKit 通过响应者链检查每个 responder 能否提供/接受指定类型的数据

核心启发

  1. 做什么:为 Mac 版设计工具添加屏幕取色功能

    • 为什么值得做NSColorSampler 的 async/await API 只需一行代码即可实现专业取色器
    • 怎么开始:在取色按钮的 action 中调用 await NSColorSampler().sample(),获取颜色后应用到选中元素
  2. 做什么:在 Mac 版文件管理器中用 SF Symbols 3 的 Palette 模式创建彩色标签图标

    • 为什么值得做:Palette 模式可以为符号的不同层指定不同颜色,标签图标可以背景层灰色、前景层彩色
    • 怎么开始:使用 NSImage.SymbolConfiguration(paletteColors: [backgroundColor, foregroundColor]) 配置符号渲染
  3. 做什么:为 Mac 版文本编辑器迁移到 TextKit 2 以支持超大文档

    • 为什么值得做:TextKit 2 的非线性布局在大文档上性能显著提升,只排版可见区域
    • 怎么开始:创建 NSTextLayoutManager 替代 NSLayoutManager,使用 NSTextContentStorage 管理文本内容
  4. 做什么:在 AppKit 应用中用 @Invalidating 简化自定义视图属性

    • 为什么值得做:消除大量 didSet 中的 setNeedsDisplay/setNeedsLayout 样板代码
    • 怎么开始:将自定义视图的配置属性加上 @Invalidating(.display)@Invalidating(.layout) 注解

关联 Session

评论

GitHub Issues · utterances