关于 paperkit
面向 Apple 平台开发者的 PaperKit 指南,介绍如何构建结合 PencilKit 绘图与结构化标记的画布。涵盖标记数据保存、功能集配置及界面集成;该框架自 Apple 26 系统版本起提供,API 仍可能变化。
主要功能
- 01绘制并编辑结构化标记
- 02插入文本、形状和图像
- 03配置支持的功能集
- 04保存和加载标记数据
- 05与 PencilKit 和 SwiftUI 集成
适用场景
- 01在文档页面上绘制和添加标注
- 02用文本框、箭头和标签进行评审
- 03保存画布内容并在之后恢复
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill paperkit安装后在 Claude Code 中输入/paperkit确认已加载。
附带文件evals/evals.jsonreferences/paperkit-patterns.md
详细介绍
PaperKit
对 Beta 版本敏感。 PaperKit 是 iOS/iPadOS 26、macOS 26 和 visionOS 26 中的新功能。API 接口可能会变更。发布前请对照 Apple 当前的文档核实详情。
PaperKit 将 PencilKit 绘图与形状、文本、图像和线条等结构化标记元素结合在由 PaperMarkupViewController 管理的画布中。
目录
- 设置
- 工作流
- PaperMarkupViewController
- PaperMarkup 数据模型
- 插入控制器
- FeatureSet 配置
- 与 PencilKit 集成
- SwiftUI 集成
- 常见错误
- 审核清单
- 参考资料
工作流
- 在构建 UI 之前,选择文档边界、受支持的
FeatureSet和持久化版本。 - 创建
PaperMarkup,嵌入PaperMarkupViewController,并在视图的生命周期内保持控制器、工具选取器和插入控制器处于活动状态。 - 使用适合平台的插入界面,并确保 PencilKit 绘图始终位于 PaperKit 文档边界内。
- 在主线程之外保存,保留缩略图以处理不兼容未来版本的内容,并使用相同的功能集测试往返加载。
- 失败时,恢复原始文档字节,修正功能集/版本/控制器不匹配的问题,然后重新执行编辑、保存、重新启动、加载、缩略图回退和撤销检查。
请加载 references/paperkit-patterns.md,了解完整的平台设置、工具选取器连接、持久化、缩略图、自定义功能集、程序化构建和迁移。
设置
PaperKit 无需任何授权或特殊的 Info.plist 条目。
import PaperKit
平台可用性: iOS 26.0+、iPadOS 26.0+、Mac Catalyst 26.0+、macOS 26.0+、visionOS 26.0+。
三个核心组件:
| 组件 | 作用 |
|---|---|
PaperMarkupViewController | 用于创建和显示标记与绘图的交互式画布 |
PaperMarkup | 用于序列化所有标记元素和 PencilKit 绘图的数据模型 |
MarkupEditViewController / MarkupToolbarViewController | 用于添加标记元素的插入 UI |
PaperMarkupViewController
用于交互式标记的主要视图控制器。提供可滚动的画布,用于自由绘制 PencilKit 图形和结构化标记元素。遵循 Observable 和 PKToolPickerObserver。
基本 UIKit 设置
import PaperKit
import PencilKit
import UIKit
class MarkupViewController: UIViewController, PaperMarkupViewController.Delegate {
var paperVC: PaperMarkupViewController!
var toolPicker: PKToolPicker!
override func viewDidLoad() {
super.viewDidLoad()
let pageBounds = CGRect(origin: .zero, size: CGSize(width: 612, height: 792))
let markup = PaperMarkup(bounds: pageBounds)
let features = FeatureSet.latest
paperVC = PaperMarkupViewController(
markup: markup,
supportedFeatureSet: features
)
paperVC.delegate = self
addChild(paperVC)
paperVC.view.frame = view.bounds
paperVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(paperVC.view)
paperVC.didMove(toParent: self)
toolPicker = PKToolPicker()
toolPicker.addObserver(paperVC)
paperVC.pencilKitResponderState.activeToolPicker = toolPicker
paperVC.pencilKitResponderState.toolPickerVisibility = .visible
}
func paperMarkupViewControllerDidChangeMarkup(
_ controller: PaperMarkupViewController
) {
guard let markup = controller.markup else { return }
Task { try await save(markup) }
}
}
关键属性
| 属性 | 类型 | 描述 |
|---|---|---|
markup | PaperMarkup? | 当前数据模型 |
selectedMarkup | PaperMarkup | 当前选中的内容 |
isEditable | Bool | 画布是否接受输入 |
isRulerActive | Bool | 是否显示标尺叠加层 |
drawingTool | any PKTool | 当前启用的 PencilKit 绘图工具 |
contentView | UIView? / NSView? | 在标记下方渲染的背景视图 |
zoomRange | ClosedRange<CGFloat> | 缩放比例的最小值/最大值 |
supportedFeatureSet | FeatureSet | 已启用的 PaperKit 功能 |
触控模式
PaperMarkupViewController.TouchMode 有两种情况:.drawing 和 .selection。
paperVC.directTouchMode = .drawing // Finger draws
paperVC.directTouchMode = .selection // Finger selects elements
paperVC.directTouchAutomaticallyDraws = true // System decides based on Pencil state
内容背景
在标记图层下方设置任意视图,以显示模板、文档页面或要添加注释的图像。确保 PaperMarkup(bounds:) 的坐标空间与背景内容对齐,例如 PDF 页面或渲染图像的尺寸,以便保存的注释能够在正确位置还原:
let pageBounds = CGRect(origin: .zero, size: pageImage.size)
let imageView = UIImageView(image: pageImage)
imageView.frame = pageBounds
let markup = PaperMarkup(bounds: pageBounds)
paperVC = PaperMarkupViewController(markup: markup, supportedFeatureSet: features)
paperVC.contentView = imageView
委托回调
| 方法 | 调用时机 |
|---|---|
paperMarkupViewControllerDidChangeMarkup(_:) | 标记内容发生变化时 |
paperMarkupViewControllerDidBeginDrawing(_:) | 用户开始绘图时 |
paperMarkupViewControllerDidChangeSelection(_:) | 选中内容发生变化时 |
paperMarkupViewControllerDidChangeContentVisibleFrame(_:) | 可见框架发生变化时 |
PaperMarkup 数据模型
PaperMarkup 是一个 Sendable 结构体,用于存储所有标记元素和 PencilKit 绘图数据。
创建与持久化
// New empty model. Bounds define the saved document coordinate space.
let markup = PaperMarkup(bounds: CGRect(x: 0, y: 0, width: 612, height: 792))
// Load from saved data
let markup = try PaperMarkup(dataRepresentation: savedData)
// Save — dataRepresentation() is async throws
func save(_ markup: PaperMarkup) async throws {
let data = try await markup.dataRepresentation()
try data.write(to: fileURL)
}
以编程方式插入内容
// Text box
markup.insertNewTextbox(
attributedText: AttributedString("Annotation"),
frame: CGRect(x: 50, y: 100, width: 200, height: 40),
rotation: 0
)
// Image
markup.insertNewImage(cgImage, frame: CGRect(x: 50, y: 200, width: 300, height: 200), rotation: 0)
// Shape
let shapeConfig = ShapeConfiguration(
type: .rectangle,
fillColor: UIColor.systemBlue.withAlphaComponent(0.2).cgColor,
strokeColor: UIColor.systemBlue.cgColor,
lineWidth: 2
)
markup.insertNewShape(configuration: shapeConfig, frame: CGRect(x: 50, y: 420, width: 200, height: 100), rotation: 0)
// Line with arrow end marker
let lineConfig = ShapeConfiguration(type: .line, fillColor: nil, strokeColor: UIColor.red.cgColor, lineWidth: 3)
markup.insertNewLine(
configuration: lineConfig,
from: CGPoint(x: 50, y: 550), to: CGPoint(x: 250, y: 550),
startMarker: false, endMarker: true
)
形状类型:.rectangle、.roundedRectangle、.ellipse、.line、.arrowShape、.star、.chatBubble、.regularPolygon。
其他操作
markup.append(contentsOf: otherMarkup) // Merge another PaperMarkup
markup.append(contentsOf: pkDrawing) // Merge a PKDrawing
markup.transformContent(CGAffineTransform(...)) // Apply affine transform
markup.removeContentUnsupported(by: featureSet) // Strip unsupported elements
| 属性 | 说明 |
|---|---|
bounds | 标记的坐标空间 |
contentsRenderFrame | 所有内容的紧凑边界框 |
featureSet | 此数据模型的内容所用功能 |
indexableContent | 可提取并用于搜索索引的文本 |
在视图控制器上使用 suggestedFrameForInserting(contentInFrame:),获取一个不会与现有内容重叠的框架。
插入控制器
MarkupEditViewController(iOS、iPadOS、Mac Catalyst、visionOS)
提供弹出菜单,用于插入形状、文本框、线条和其他元素。
func showInsertionMenu(from barButtonItem: UIBarButtonItem) {
let editVC = MarkupEditViewController(
supportedFeatureSet: paperVC.supportedFeatureSet,
additionalActions: []
)
editVC.delegate = paperVC // PaperMarkupViewController conforms to the delegate
editVC.modalPresentationStyle = .popover
editVC.popoverPresentationController?.barButtonItem = barButtonItem
present(editVC, animated: true)
}
MarkupToolbarViewController(macOS、Mac Catalyst)
提供包含绘图工具和插入按钮的工具栏。可用于原生 macOS 和采用工具栏样式 UI 的 Mac Catalyst;需要 UIKit 弹出菜单的 Catalyst 应用可以使用 MarkupEditViewController。
let toolbar = MarkupToolbarViewController(supportedFeatureSet: paperVC.supportedFeatureSet)
toolbar.delegate = paperVC
addChild(toolbar)
toolbar.view.frame = toolbarContainerView.bounds
toolbarContainerView.addSubview(toolbar.view)
toolbar.didMove(toParent: self)
这两个控制器必须使用与 PaperMarkupViewController 相同的 FeatureSet。
FeatureSet 配置
FeatureSet 控制可用的标记功能。
| 预设 | 说明 |
|---|---|
.latest | 所有当前功能 — 建议从此预设开始 |
.version1 | 版本 1 中的功能 |
.empty | 不启用任何功能 |
自定义
var features = FeatureSet.latest
features.remove(.stickers)
features.remove(.images)
// Or build up from empty
var features = FeatureSet.empty
features.insert(.drawing)
features.insert(.text)
features.insert(.shapeStrokes)
可用功能
| 功能 | 说明 |
|---|---|
.drawing | 自由绘制 PencilKit 图形 |
.text | 插入文本框 |
.images | 插入图像 |
.stickers | 插入贴纸 |
.links | 链接批注 |
.loupes | 放大镜元素 |
.shapeStrokes | 形状轮廓 |
.shapeFills | 形状填充 |
.shapeOpacity | 形状不透明度控制 |
HDR 支持
在 FeatureSet 和 PKToolPicker 上将 colorMaximumLinearExposure 设为大于 1.0 的值:
var features = FeatureSet.latest
features.colorMaximumLinearExposure = 4.0
toolPicker.colorMaximumLinearExposure = features.colorMaximumLinearExposure
使用 view.window?.windowScene?.screen.potentialEDRHeadroom 来匹配设备屏幕的能力。仅支持 SDR 时使用 1.0。
形状、墨迹和线条标记
features.shapes = [.rectangle, .ellipse, .arrowShape, .line]
features.inks = [.pen, .pencil, .marker]
features.lineMarkerPositions = .all // .single, .double, .plain, or .all
与 PencilKit 集成
PaperKit 接受用于绘图的 PKTool,并可追加 PKDrawing 内容。
如果应用依赖自定义笔刷行为、原始 PKDrawing / PKStroke 分析或以自定义套索为核心的编辑,PaperKit 就不能直接替代低级别的 PKCanvasView。应由 PencilKit 管理这些工作流,并在其旁边添加 PaperKit,用于结构化审阅标记,例如标注、箭头、文本框、标签、图像印章和系统标准插入 UI。只有在低级别编辑路径不再需要管理现有绘图内容时,才使用 PaperMarkup.append(contentsOf: PKDrawing) 将现有绘图迁移或复制到 PaperKit 注释层。
import PencilKit
// Set drawing tool
paperVC.drawingTool = PKInkingTool(.pen, color: .black, width: 3)
// Merge existing PKDrawing into markup
markup.append(contentsOf: existingPKDrawing)
工具选择器设置
let toolPicker = PKToolPicker()
toolPicker.addObserver(paperVC)
paperVC.pencilKitResponderState.activeToolPicker = toolPicker
paperVC.pencilKitResponderState.toolPickerVisibility = .visible
将 toolPickerVisibility 设为 .hidden,可让选择器继续工作(响应 Pencil 手势),但不显示出来,从而启用迷你工具选择器体验。
内容版本兼容性
FeatureSet.ContentVersion 映射到 PKContentVersion:
let pkVersion = features.contentVersion.pencilKitContentVersion
SwiftUI 集成
将 PaperMarkupViewController 封装在 UIViewControllerRepresentable 中:
struct MarkupView: UIViewControllerRepresentable {
@Binding var markup: PaperMarkup
let features: FeatureSet
func makeUIViewController(context: Context) -> PaperMarkupViewController {
let vc = PaperMarkupViewController(markup: markup, supportedFeatureSet: features)
vc.delegate = context.coordinator
let toolPicker = PKToolPicker()
toolPicker.addObserver(vc)
vc.pencilKitResponderState.activeToolPicker = toolPicker
vc.pencilKitResponderState.toolPickerVisibility = .visible
context.coordinator.toolPicker = toolPicker
return vc
}
func updateUIViewController(_ vc: PaperMarkupViewController, context: Context) {
if vc.markup != markup { vc.markup = markup }
}
func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
class Coordinator: NSObject, PaperMarkupViewController.Delegate {
let parent: MarkupView
var toolPicker: PKToolPicker?
init(parent: MarkupView) { self.parent = parent }
func paperMarkupViewControllerDidChangeMarkup(
_ controller: PaperMarkupViewController
) {
if let markup = controller.markup { parent.markup = markup }
}
}
}
在创建 SwiftUI 桥接之前,根据文档或页面尺寸初始化绑定的 PaperMarkup:
struct DocumentMarkupScreen: View {
let pageSize: CGSize
@State private var markup: PaperMarkup
private let features = FeatureSet.latest
init(pageSize: CGSize) {
self.pageSize = pageSize
_markup = State(
initialValue: PaperMarkup(
bounds: CGRect(origin: .zero, size: pageSize)
)
)
}
var body: some View {
MarkupView(markup: $markup, features: features)
}
}
常见错误
| 错误 | 修复方法 |
|---|---|
| 视图、插入界面和已保存文档使用的功能集不一致 | 选择一个受支持的 FeatureSet,并在整个编辑会话中使用它。 |
| 未经版本检查便加载内容 | 验证 markup.featureSet.isSubset(of: supportedFeatureSet),或显示已保存的缩略图/回退内容。 |
| 序列化阻塞 UI,或发生不安全的重叠操作 | 在交互流程之外等待 dataRepresentation(),并对自动保存进行防抖处理。 |
| 工具选取器是局部变量 | 在控制器/视图的生命周期内保留它。 |
| 使用了不适合该平台的插入界面 | 在 macOS 上使用 MarkupToolbarViewController;在 UIKit 上使用 MarkupEditViewController,Catalyst 支持任一呈现方式。 |
审核清单
- 已添加
import PaperKit;部署目标为 iOS 26+ / macOS 26+ / visionOS 26+ -
PaperMarkup已初始化,且其边界与内容尺寸相匹配 -
PaperMarkupViewController和插入控制器使用相同的FeatureSet - 在异步上下文中调用
dataRepresentation() - 将
PKToolPicker保留为存储属性 - 在
PaperMarkupViewController上设置委托,以接收更改回调 - 加载已保存的数据时检查内容版本
- 按平台使用正确的插入控制器(macOS/Catalyst 工具栏 UI 使用
MarkupToolbarViewController;UIKit/Catalyst 弹出框使用MarkupEditViewController) - 反序列化时处理
MarkupError情况 - HDR:在
FeatureSet和PKToolPicker.colorMaximumLinearExposure上设置colorMaximumLinearExposure
参考资料
- PaperKit 文档
- 在应用中集成 PaperKit
- 认识 PaperKit — WWDC25
pencilkit技能涵盖 PencilKit 绘图、工具选取器和 PKDrawing 序列化- references/paperkit-patterns.md — 数据持久化、渲染、多平台设置、自定义功能集