WWDC Quick Look 💓 By SwiftGGTeam
Unwrap PaperKit

Unwrap PaperKit

观看原视频

Highlight

PaperKit 是 Apple 内部用于 Notes、Preview、Freeform 等应用的画布引擎,iOS、macOS 和 visionOS 27 起向开发者开放,提供完整的画布数据模型、元素操作和自定义装饰层能力。

核心内容

以前想要在 App 里做一个可画、可标记、可拖拽图片的画布,需要自己处理触摸事件、绘制逻辑、撤销重做、手势识别、图层管理……工作量巨大。做出来的体验也很难和系统原生应用一致。

PaperKit 就是 Apple 用来解决这个问题的统一框架。它用完代替简单的绘图工具,整的画布体验——铅笔、形状、文本、图片全部协同工作。你在 Notes 里画草图、在 Preview 里签名高亮、在 Freeform 里头脑风暴,背后都是 PaperKit。

这个框架的核心有三个层次:

数据模型层PaperMarkup 暴露了画布上所有元素的有序集合 subelements,你可以读取、创建、修改任何元素。

元素控制层:每个元素都遵循 Markup 协议,有统一的 framerotation 属性,也有各自特定的属性。新增的 allowedInteractions 让你精确控制每个元素可以被哪些方式操作——移动、调整大小、旋转、删除、样式修改、选择,都可以独立控制。

装饰层(Adornments):在画布之上叠加临时 UI 控件,比如按钮、注释、协作标记。这些控件不会成为文档的一部分,不会被保存或导出,只在编辑时存在,自动跟随缩放和滚动。

详细内容

数据模型与 subelements

PaperKit 的核心是 PaperMarkup,它代表整个画布的内容。([01:36](https://developer.apple.com/videos/play/wwdc2026/372/?time=96))

import PaperKit

func generateMarkup(pageSize: CGSize, panelFrames: [CGRect], configuration: ShapeConfiguration) -> PaperMarkup {
    var markup = PaperMarkup(bounds: CGRect(origin: .zero, size: pageSize))
    var subelements: MarkupOrderedSet = markup.subelements
    for panelFrame: CGRect in panelFrames {
        let shape = ShapeMarkup(frame: panelFrame, configuration: configuration)
        subelements.append(shape)
    }
    markup.subelements = subelements
    return markup
}

关键点

  • PaperMarkup 初始化时需要 bounds,定义画布大小
  • subelementsMarkupOrderedSet 类型,有序集合,可以读写
  • ShapeMarkup 是形状元素,初始化需要位置 frame 和配置 configuration
  • 修改 subelements 后赋值回 markup.subelements 才会生效

控制元素交互行为

默认情况下,画布上的元素可以自由选择、拖拽、删除。但某些场景需要锁定模板元素。([03:03](https://developer.apple.com/videos/play/wwdc2026/372/?time=183))

import PaperKit

func generateMarkup(pageSize: CGSize, panelFrames: [CGRect], configuration: ShapeConfiguration) -> PaperMarkup {
    var markup = PaperMarkup(bounds: CGRect(origin: .zero, size: pageSize))
    var subelements: MarkupOrderedSet = markup.subelements
    for panelFrame: CGRect in panelFrames {
        var shape = ShapeMarkup(frame: panelFrame, configuration: configuration)
        shape.allowedInteractions = .readOnly
        subelements.append(shape)
    }
    markup.subelements = subelements
    return markup
}

关键点

  • allowedInteractionsMarkupInteractions 选项集合
  • .readOnly 是组合标志,一次性禁用移动、调整大小、旋转、删除、样式修改和选择
  • 也可以单独控制,如只允许移动、不允许删除

样式修改

所有元素都有其类型特定的属性。ShapeMarkupstrokeColorfillColor([04:22](https://developer.apple.com/videos/play/wwdc2026/372/?time=262))

import PaperKit

func updatePanelColor(_ selectedColor: CGColor) {
    guard var markup: PaperMarkup = paperMarkupViewController.markup else { return }
    var subelements: MarkupOrderedSet = markup.subelements
    for element in subelements {
        guard var shape = element as? ShapeMarkup else { continue }
        shape.strokeColor = selectedColor
        shape.fillColor = selectedColor.copy(alpha: 0.15)
        subelements.updateOrAppend(shape)
    }
    markup.subelements = subelements
    markup.backgroundColor = selectedColor.copy(alpha: 0.15)
    paperMarkupViewController.markup = markup
}

关键点

  • 迭代 subelements 时需要类型转换,因为集合里是各种 Markup 协议类型
  • updateOrAppend 方法会更新已存在的元素或添加新元素
  • 修改完成后需要重新赋值给 paperMarkupViewController.markup

装饰层(Adornments)

Adornments 是叠加在画布上的临时控件,不会被保存到文档中。适合做按钮、注释、协作 UI。([05:53](https://developer.apple.com/videos/play/wwdc2026/372/?time=353))

import PaperKit

func addPanelAdornments(for page: Page) {
    var adornments: [MarkupAdornment] = []
    for (panelIndex, panel) in page.panels.enumerated() {
        let adornmentID = UUID()
        adornmentPanelMapping[adornmentID] = panelIndex
        let center = CGPoint(x: panel.midX, y: panel.midY)
        let adornment = MarkupAdornment(
            id: adornmentID,
            anchor: .canvas(location: center),
            imageConfiguration: .systemImage("photo.badge.plus"),
            dragRegion: .fixed,
            scalesWithZoom: false
        )
        adornments.append(adornment)
    }
    paperMarkupViewController.adornments = adornments
}

关键点

  • MarkupAdornmentUUID 唯一标识,用于后续处理点击事件
  • anchor.canvas(location:) 锚定到画布坐标,会自动跟随缩放滚动
  • imageConfiguration 支持 SF Symbols
  • dragRegion: .fixed 表示按钮位置固定,scalesWithZoom: false 表示不随缩放放大

处理装饰层点击

import ImagePlayground
import PaperKit

func paperMarkupViewController(_ paperMarkupViewController: PaperMarkupViewController, didTapAdornmentWithID id: UUID) {
    guard let panelIndex = adornmentPanelMapping[id] else { return }
    activeImageGenerationPanelIndex = panelIndex

    let imagePlaygroundViewController = ImagePlaygroundViewController()
    imagePlaygroundViewController.delegate = self
    present(imagePlaygroundViewController, animated: true)
}

关键点

  • 实现代理方法接收装饰层点击事件
  • 通过 id 映射到具体的业务对象(如漫画面板索引)
  • 可以弹出其他界面(如 Image Playground)

插入生成的图片

import ImagePlayground
import PaperKit

func imageViewController(_ imageViewController: ImagePlaygroundViewController, didCreateImageAt imageURL: URL) {
    guard let panelFrame = activeGenerationPanelFrame,
          let paperMarkupViewController = pageViewController.paperViewController,
          var markup = paperMarkupViewController.markup,
          let image = UIImage(contentsOfFile: imageURL.path) else { return }

    let imageMarkup = ImageMarkup(frame: panelFrame, image: image)
    markup.subelements.append(imageMarkup)
    paperMarkupViewController.markup = markup
}

关键点

  • ImageMarkup 用于创建图片元素,需要位置和 UIImage
  • 直接 appendsubelements 即可插入
  • 更新 markup 会触发视图刷新

与 PencilKit 的关系

PaperKit 基于 PencilKit 构建,支持 Apple Pencil 绘图。每个笔画都会成为 markup 元素,可以使用 PencilKit 的模型 API。这些 API 现在支持字符识别和贝塞尔曲线路径转换。

核心启发

漫画创作 App:像 demo 那样做漫画编辑器,用 allowedInteractions 锁定面板布局,用 adornments 添加”生成图片”按钮,结合 Image Playground 让用户快速创作。入口:ShapeMarkup + MarkupAdornment

填色书 App:预置线稿模板(read-only),用户只能在指定区域填色。通过 subelements 迭代找到特定形状,修改 fillColor

批注协作工具:用 adornments 显示协作者的在线状态、光标位置、评论标记。这些信息不会保存到文档,但实时更新。入口:MarkupAdornment + 定时刷新。

手写笔记搜索:利用 PencilKit 的字符识别 API,把手写内容转成文本,然后插入 TextMarkup 元素,让笔记可搜索。

模板填充工具:创建各种文档模板(发票、合同、表单),用 readOnly 锁定模板结构,用户只能填写空白区域。

关联 Session

评论

GitHub Issues · utterances