WWDC Quick Look 💓 By SwiftGGTeam
Meet PaperKit

Meet PaperKit

观看原视频

Highlight

PaperKit 是 Apple 自己的标记框架,之前只在系统 App 里用(Notes、截图、QuickLook、Journal)。现在第三方 App 可以直接集成。


核心内容

之前想在自己的 App 里做出像 Notes、截图标注那样的「画一笔 + 加形状 + 拖图片 + 文本框」体验,几乎要从零造轮子:自己管理图层、自己处理 PencilKit 绘画与标记元素的混排、自己设计插入菜单、自己适配 macOS。一个不太核心的功能,往往要写几千行代码。

WWDC25 上 Apple 把自家用了多年的 PaperKit 框架对外开放(00:07)。这是 Apple 系统级标记体验的底层,Notes、Screenshots、QuickLook、Journal 都跑在它上面。macOS Tahoe 这次也原生支持(00:57)。

框架由三个核心组件构成:PaperMarkupViewController 是交互式画布,负责显示和编辑标记与 PencilKit 绘画;PaperMarkup 是数据模型容器,负责保存、加载、渲染;MarkupEditViewController(iOS / iPadOS / visionOS)和 MarkupToolbarViewController(macOS)负责插入形状、图片、文本框等元素(02:31)。把这三块拼起来,就有了和系统 App 一致的标注能力。


详细内容

iOS 上的最小集成只需要一个 viewDidLoad03:47)。

override func viewDidLoad() {
    super.viewDidLoad()

    let markupModel = PaperMarkup(bounds: view.bounds)
    let paperViewController = PaperMarkupViewController(markup: markupModel, supportedFeatureSet: .latest)
    view.addSubview(paperViewController.view)
    addChild(paperViewController)
    paperViewController.didMove(toParent: self)
    becomeFirstResponder()

    let toolPicker = PKToolPicker()
    toolPicker.addObserver(paperViewController)

    pencilKitResponderState.activeToolPicker = toolPicker
    pencilKitResponderState.toolPickerVisibility = .visible

    toolPicker.accessoryItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(plusButtonPressed(_:)))
}

关键点:

  • PaperMarkup(bounds:) 创建数据模型,用 view.bounds 保证渲染上下文正确。
  • PaperMarkupViewController(markup:supportedFeatureSet:) 创建画布,传入 .latest 拿到当前所有标记能力。
  • addChild + didMove(toParent:) 是标准 UIKit 容器视图控制器的接入方式。
  • PKToolPicker 通过 addObserver 把画布注册成观察者,工具切换时画布自动响应。
  • pencilKitResponderState.activeToolPicker 是 UIResponder 上的新 API,用来指定当前响应者的工具选择器(04:32)。
  • toolPickerVisibility 控制可见性,设为隐藏时双击和挤压手势依然可用,方便实现迷你工具选择器(05:03)。
  • toolPicker.accessoryItem 把一个 + 按钮塞到工具选择器上,作为插入菜单入口。

macOS 上结构基本一致,区别只在插入 UI 用工具栏而非弹出菜单(06:11)。

override func viewDidLoad() {
    super.viewDidLoad()

    let markupModel = PaperMarkup(bounds: view.bounds)
    let paperViewController = PaperMarkupViewController(markup: markupModel, supportedFeatureSet: .latest)
    view.addSubview(paperViewController.view)
    addChild(paperViewController)

    let toolbarViewController = MarkupToolbarViewController(supportedFeatureSet: .latest)
    toolbarViewController.delegate = paperViewController
    view.addSubview(toolbarViewController.view)

    setupLayoutConstraints()
}

关键点:

  • macOS 用 MarkupToolbarViewController 替代 iOS 的弹出 MarkupEditViewController
  • toolbarViewController.delegate = paperViewController 把工具栏的插入回调连到画布。
  • 画布与工具栏都使用同一份 FeatureSet,保证可用工具一致。

保存数据用 delegate 回调(07:18)。

func paperMarkupViewControllerDidChangeMarkup(_ paperMarkupViewController: PaperMarkupViewController) {
    let markupModel = paperMarkupViewController.markup
    Task {
        let data = try await markupModel.dataRepresentation()
        try data.write(toFile: paperKitDataURL)
    }
}

关键点:

  • paperMarkupViewControllerDidChangeMarkup 在每次画布变更时触发。
  • markupModel.dataRepresentation() 是异步方法,返回 Data,可直接落盘。
  • PaperMarkupViewController 也实现了 Observable,如果偏好状态驱动可以替代 delegate(07:33)。

向前兼容(forward compatibility)建议保存一份缩略图,在版本不匹配时显示(08:02)。

func updateThumbnail(_ markupModel: PaperMarkup) async throws {
    let thumbnailSize = CGSize(width: 200, height: 200)
    let context = makeCGContext(size: thumbnailSize)
    context.setFillColor(gray: 1, alpha: 1)
    context.fill(renderer.format.bounds)

    await markupModel.draw(in: context, frame: CGRect(origin: .zero, size: thumbnailSize))

    thumbnail = context.makeImage()
}

关键点:

  • 自己创建一个 CGContext 作为渲染目标。
  • markupModel.draw(in:frame:)async 方法,把当前模型绘制到 context。
  • 把生成的 CGImage 与 markup 数据一起保存,是 Notes 等系统 App 的做法。

定制工具集走「从 .latest 减法」(09:02)。

var featureSet: FeatureSet = .latest

featureSet.remove(.text)
featureSet.insert(.stickers)

featureSet.colorMaximumLinearExposure = 4
toolPicker.colorMaximumLinearExposure = 4

let paperViewController = PaperMarkupViewController(supportedFeatureSet: featureSet)
let markupEditViewController = MarkupEditViewController(supportedFeatureSet: featureSet)

关键点:

  • FeatureSet.latest 包含框架当前所有能力,未来新增功能自动获得。
  • remove / insert 增减具体能力,比如去掉文本框、加上 sticker。
  • colorMaximumLinearExposure 大于 1 即开启 HDR 标记,画布和工具选择器都要设置(09:30)。
  • 推荐值 4 是讲师在示例 App 里用的,也可以读取 UIScreen / NSScreen 的实际 headroom。
  • HDR 开启后会解锁 PencilKit 新增的书法笔(Reed tool)(10:22)。

最后一个常用扩展点是自定义背景(10:50)。

let template = UIImage(named: "MyTemplate.jpg")
let templateView = UIImageView(image: template)
paperViewController.contentView = templateView

关键点:

  • contentView 接受任意 UIView,所有标记和绘画都会渲染在它之上。
  • 适合做模板纸(横线、点阵、菜谱卡片)这类「底图固定 + 上层涂鸦」的场景。

SwiftUI 里用 UIViewControllerRepresentable 包一层即可接入(06:46)。


核心启发

1. 用 PaperKit 给笔记 / 阅读 / 教育类 App 一键加上「Apple 同款」标注能力

为什么值得做:以前自己实现一套手写 + 形状 + 图片混排的标记,工作量极大;现在通过 PaperKit 几十行代码就能复用 Notes / Journal 的体验。

怎么开始:先用最小集成跑通 iOS(PaperMarkup + PaperMarkupViewController + PKToolPicker),再用 delegate 回调把数据持久化到自己的存储层。

2. 从 FeatureSet.latest 出发做减法,而不是从空集逐个添加

为什么值得做:.latest 自动跟随框架升级,未来新工具不用改代码就能拿到;从空集 insert 容易遗漏新功能,维护成本更高。

怎么开始:声明 var featureSet: FeatureSet = .latest,仅对你的 App 不需要的工具调用 remove,比如纯阅读批注 App 可以 remove(.stickers)

3. 保存缩略图做向前兼容,避免新版本数据在旧版本崩溃或显示空白

为什么值得做:用户可能在 iOS 27 编辑后回到 iOS 26 打开同一份文件,新元素无法解析。落盘缩略图能保证旧版本至少看得到内容。

怎么开始:在 delegate 回调里同时调用 markupModel.dataRepresentation() 与缩略图渲染,把两份数据一起存;加载时检查版本,不匹配就显示缩略图。

4. 用 pencilKitResponderState.toolPickerVisibility 做迷你工具选择器

为什么值得做:工具选择器隐藏后双击 / 挤压手势仍可用,可以做出更紧凑的自定义工具栏 UI,又不丢交互。

怎么开始:把工具选择器设为不可见,自行绘制简化的工具按钮;让按钮调用 PKToolPicker 的状态 API 切换工具。


关联 Session

评论

GitHub Issues · utterances