使用 PaperKit 轻松添加手绘、形状以及一致的标记(Markup)体验。适用于集成 PaperMarkupViewController 进行标记编辑、添加形状识别、处理 PaperMarkup 数据模型、在文档编辑器中嵌入标记工具,或构建需要系统标准标记工具栏的批注功能。iOS 26 新增功能。
PaperKit
注意(Beta 阶段 API)。 PaperKit 是 iOS/iPadOS 26、macOS 26 及 visionOS 26 的新增框架。API 仍有可能调整,发布前请务必参考 Apple 最新的官方文档确认细节。
由 PaperMarkupViewController 管理的画布中,PaperKit 将 PencilKit 自由手绘与形状、文本、图片、线条等结构化标记元素融合在一起。
目录
- 环境配置
- 工作流程
- PaperMarkupViewController
- PaperMarkup 数据模型
- 插入控制器(Insertion Controllers)
- FeatureSet 配置
- 与 PencilKit 集成
- SwiftUI 集成
- 常见误区
- 审查清单
- 参考资料
工作流程
- 构建 UI 之前,先确定文档边界 (bounds)、支持的
FeatureSet以及持久化版本。 - 创建
PaperMarkup,嵌入PaperMarkupViewController,并在视图生命周期内保持 Controller、Tool Picker 以及 Insertion Controller 处于存活状态。 - 使用适配对应平台的插入界面,并将 PencilKit 手绘限制在 PaperKit 文档边界之内。
- 在主线程之外执行保存操作;对于向前不兼容的内容保留缩略图;使用相同的 feature set 测试完整读写加载 (round-trip loading)。
- 若加载失败,还原原始文档字节数据,修复 feature-set/版本/Controller 的不匹配问题,并重新运行“编辑、保存、重启、加载、缩略图降级回滚与撤销”检测流程。
加载 references/paperkit-patterns.md 查看完整的平台配置、工具选择器关联、持久化、缩略图、自定义功能集、代码构造与迁移指南。
环境配置
PaperKit 无需任何特殊的 Entitlements 或 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
交互式标记的核心 View Controller。提供支持滚动的画布,用于 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 特性集 |
触摸模式(Touch Modes)
PaperMarkupViewController.TouchMode 包含两种枚举值:.drawing(绘画)和 .selection(选择)。
paperVC.directTouchMode = .drawing // 手指滑动进行手绘
paperVC.directTouchMode = .selection // 手指点击选择元素
paperVC.directTouchAutomaticallyDraws = true // 系统根据 Apple Pencil 状态自动决定
内容背景
可以在标记图层下方设置任意 View,用作模板、文档页面或待标注的图片。确保 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
Delegate 回调方法
| 方法 | 触发时机 |
|---|---|
paperMarkupViewControllerDidChangeMarkup(_:) |
标记内容发生变更时 |
paperMarkupViewControllerDidBeginDrawing(_:) |
用户开始绘制时 |
paperMarkupViewControllerDidChangeSelection(_:) |
选中项发生变更时 |
paperMarkupViewControllerDidChangeContentVisibleFrame(_:) |
可见区域框架发生变化时 |
PaperMarkup 数据模型
PaperMarkup 是一个 Sendable 结构体,用于存储所有标记元素和 PencilKit 绘画数据。
创建与持久化
// 创建全新的空模型。Bounds 定义保存文档的坐标空间。
let markup = PaperMarkup(bounds: CGRect(x: 0, y: 0, width: 612, height: 792))
// 从保存的数据中加载
let markup = try PaperMarkup(dataRepresentation: savedData)
// 保存 — dataRepresentation() 为 async throws
func save(_ markup: PaperMarkup) async throws {
let data = try await markup.dataRepresentation()
try data.write(to: fileURL)
}
代码动态插入内容
// 文本框
markup.insertNewTextbox(
attributedText: AttributedString("Annotation"),
frame: CGRect(x: 50, y: 100, width: 200, height: 40),
rotation: 0
)
// 图片
markup.insertNewImage(cgImage, frame: CGRect(x: 50, y: 200, width: 300, height: 200), rotation: 0)
// 形状
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)
// 带尾部箭头的线条
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) // 合并另一个 PaperMarkup
markup.append(contentsOf: pkDrawing) // 合并一个 PKDrawing
markup.transformContent(CGAffineTransform(...)) // 应用仿射变换
markup.removeContentUnsupported(by: featureSet) // 剥离不支持的元素
| 属性 | 说明 |
|---|---|
bounds |
标记的坐标空间 |
contentsRenderFrame |
包含所有内容的紧凑包围盒 (Bounding box) |
featureSet |
当前数据模型内容所使用的特性集 |
indexableContent |
可提取用于搜索索引的文本 |
可在 View Controller 上调用 suggestedFrameForInserting(contentInFrame:) 来获取一个避免与现有内容重叠的放置 Frame。
插入控制器(Insertion Controllers)
MarkupEditViewController (iOS, iPadOS, Mac Catalyst, visionOS)
弹出一个 Popover 菜单,用于插入形状、文本框、线条和其他元素。
func showInsertionMenu(from barButtonItem: UIBarButtonItem) {
let editVC = MarkupEditViewController(
supportedFeatureSet: paperVC.supportedFeatureSet,
additionalActions: []
)
editVC.delegate = paperVC // PaperMarkupViewController 遵从此 delegate
editVC.modalPresentationStyle = .popover
editVC.popoverPresentationController?.barButtonItem = barButtonItem
present(editVC, animated: true)
}
MarkupToolbarViewController (macOS, Mac Catalyst)
提供包含绘画工具和插入按钮的工具栏。适用于原生 macOS 和 Mac Catalyst 的工具栏样式 UI;如果 Catalyst 应用希望使用 UIKit Popover,可以改用 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)
// 或者从空配置开始构建
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 或 .all
与 PencilKit 集成
PaperKit 支持接收 PKTool 进行绘画,并可追加 PKDrawing 内容。
如果应用依赖自定义画笔行为、原始 PKDrawing / PKStroke 数据分析或自定义套索编辑,PaperKit 并不是底层 PKCanvasView 的直接替代品。在这些场景下,应继续由 PencilKit 负责底层手绘工作流程,并在旁边引入 PaperKit,以实现结构化的评审标记功能(如标注框、箭头、文本框、标签、图片印章以及系统标准的插入 UI)。只有当底层编辑链路不再需要单独接管该手绘内容时,才建议使用 PaperMarkup.append(contentsOf: PKDrawing) 将已有绘制迁移或复制到 PaperKit 标记图层中。
import Penci
<!-- truncated for translation batch; full body continues in source -->






