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 上的最小集成只需要一个 viewDidLoad(03: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
- Squeeze the most out of Apple Pencil — Apple Pencil 挤压手势与工具选择器交互的官方实践
- Introducing PencilKit — PencilKit 画布与工具选择器的入门介绍
- Dive deeper into Writing Tools — Writing Tools 在文本控件里直接做校对、改写、转换
- Enhance child safety with PermissionKit — PermissionKit 在通信类 App 里增强儿童沟通安全
- Discover Apple-Hosted Background Assets — Background Assets 新能力,由 Apple 托管资源
评论
GitHub Issues · utterances