集成 Apple Pencil 手绘功能,支持 PKCanvasView、PKToolPicker、PKDrawing 序列化与导出、笔画检查以及 PencilKit/PaperKit 协作与切换。适用于开发绘画应用、批注功能、手写捕捉、电子签名板、内容版本兼容的墨迹工作流,或在 iOS/iPadOS/visionOS 上构建 Apple Pencil 交互体验。
PencilKit
使用 PKCanvasView 捕捉 Apple Pencil 和手指输入,使用 PKToolPicker 管理绘画工具,使用 PKDrawing 序列化绘图数据,并在 SwiftUI 中封装 PencilKit。
目录
- 环境配置
- 从捕捉到导出的工作流
- PKCanvasView 基础用法
- PKToolPicker 工具选择器
- PKDrawing 序列化
- 内容版本兼容性
- 导出为图片
- 笔画检查与分析
- SwiftUI 集成
- 与 PaperKit 的关系
- 常见误区
- 审查清单
- 参考资料
环境配置
PencilKit 无需额外的权限声明或 Info.plist 配置。直接导入 PencilKit 并创建 PKCanvasView 即可。
import PencilKit
支持平台: iOS 13+、iPadOS 13+、Mac Catalyst 13.1+、visionOS 1.0+。
从捕捉到导出的工作流
- 捕捉: 在
canvasViewDrawingDidChange(_:)回调中读取canvasView.drawing;在新的修订版本完成后续所有检查点之前,请保留上一个已持久化的版本。 - 序列化: 调用
dataRepresentation()生成数据并执行原子写入,随后运行 解码校验/修复/重试循环。只要PKDrawing(data:)仍抛出异常,切勿将数据标记为有效。 - 版本门控: 在进行可编辑同步前应用 内容版本兼容性。若接收方无法加载该绘图,请保留完整精度的源数据,并使用已有的兼容降级方案或只读预览。
- 同步: 仅发送经过校验且兼容的数据,并在收到确认后将该版本标记为已同步。若遇到传输或冲突失败,请保留待处理的版本,排除原因后再重试,切勿丢弃最后一个已知良好的副本。
- 导出: 在调用
image(from:scale:)前,先校验绘图区域非空且缩放比例符合预期;若 Bounds 无效则跳过导出,且不要修改已序列化的绘图数据。
PKCanvasView 基础用法
PKCanvasView 是 UIScrollView 的子类,用于捕捉 Apple Pencil 和手指的输入并渲染笔画。
import PencilKit
import UIKit
class DrawingViewController: UIViewController, PKCanvasViewDelegate {
let canvasView = PKCanvasView()
override func viewDidLoad() {
super.viewDidLoad()
canvasView.delegate = self
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .black, width: 5)
canvasView.frame = view.bounds
canvasView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(canvasView)
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
// 绘图已更改 -- 在此保存或处理
}
}
绘制策略
| 策略 | 行为说明 |
|---|---|
.default |
当工具选择器可见时遵循 UIPencilInteraction.prefersPencilOnlyDrawing 设置;否则仅允许 Pencil 绘制 |
.anyInput |
Apple Pencil 和手指均可绘制 |
.pencilOnly |
仅允许 Apple Pencil 在画板上绘制 |
canvasView.drawingPolicy = .pencilOnly
在工具选择器的绘制策略控制需要遵循用户 Pencil 偏好时,为系统标准且 Pencil 优先的画板使用 .default;在电子签名板、白板或明确需要手指绘制的模式下使用 .anyInput;当手指输入绝不应产生笔画时使用 .pencilOnly。
配置画板
// 设置超大绘制区域(可滚动)
canvasView.contentSize = CGSize(width: 2000, height: 3000)
// 启用/禁用直尺工具
canvasView.isRulerActive = true
// 通过代码设置当前工具
canvasView.tool = PKInkingTool(.pencil, color: .blue, width: 3)
canvasView.tool = PKEraserTool(.vector)
PKToolPicker 工具选择器
PKToolPicker 会展示一个浮动画笔工具盘。画板会自动应用当前选中的工具。
class DrawingViewController: UIViewController {
let canvasView = PKCanvasView()
let toolPicker = PKToolPicker()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
toolPicker.addObserver(canvasView)
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()
}
}
自定义工具选择器项
可以传入指定工具来创建工具选择器。PKToolPicker(toolItems:) 和自定义工具选择器项类要求 iOS/iPadOS 18+、Mac Catalyst 18+ 以及 visionOS 2+;这些项类在 macOS 上自 macOS 26 起可用。
let toolPicker = PKToolPicker(toolItems: [
PKToolPickerInkingItem(type: .pen, color: .black, width: 5),
PKToolPickerInkingItem(type: .pencil, color: .gray, width: 5),
PKToolPickerInkingItem(type: .marker, color: .yellow, width: 12),
PKToolPickerEraserItem(type: .vector),
PKToolPickerLassoItem(),
PKToolPickerRulerItem()
])
墨迹类型
| 类型 | 说明 |
|---|---|
.pen |
平滑、具备压感防抖的钢笔 |
.pencil |
带有倾斜阴影质感的铅笔 |
.marker |
半透明马克笔/荧光笔 |
.monoline |
等宽线条笔 |
.fountainPen |
变宽书法钢笔 |
.watercolor |
可混色的水彩笔 |
.crayon |
具纹理感的蜡笔 |
.reed |
芦苇笔(iOS/iPadOS/macOS/visionOS 26+) |
内容版本
将 内容版本兼容性 作为画板和工具选择器唯一的版本映射与兼容性门控依据。
PKDrawing 序列化
PKDrawing 是包含所有笔画数据的结构体(值类型)。可将其序列化为 Data 以进行持久化存储。
// 保存绘图
func saveDrawing(_ drawing: PKDrawing) throws {
let data = drawing.dataRepresentation()
try data.write(to: fileURL, options: .atomic)
}
// 加载绘图
func loadDrawing() throws -> PKDrawing {
let data = try Data(contentsOf: fileURL)
return try PKDrawing(data: data)
}
解码校验/修复/重试循环
针对同步接收或用户提供的数据:先通过 PKDrawing(data:) 进行校验;若失败,请保留原始字节,并通过重新拉取完整版本或选择此前生成的兼容副本进行修复;接着重试解码。只有在重试成功后才将绘图赋值给画布。若恢复依然失败,请保持源数据不变,展示错误提示或可用的只读预览,切勿使用 try? 静默掩盖失败。
do {
canvasView.drawing = try PKDrawing(data: correctedData) // 重试
} catch {
showReadOnlyPreview(for: document, loadError: error)
}
合并绘图
var drawing1 = PKDrawing()
let drawing2 = PKDrawing()
drawing1.append(drawing2)
// 非破坏性合并
let combined = drawing1.appending(drawing2)
变换绘图
let scaled = drawing.transformed(using: CGAffineTransform(scaleX: 2, y: 2))
let translated = drawing.transformed(using: CGAffineTransform(translationX: 100, y: 0))
内容版本兼容性
对于同步、迁移、版本降级或跨设备编辑任务,请使用 requiredContentVersion 作为兼容性门控;当旧版本客户端必须保持可编辑时,请显式指定 maximumSupportedContentVersion。
let targetVersion: PKContentVersion = .version1
canvasView.maximumSupportedContentVersion = targetVersion
toolPicker.maximumSupportedContentVersion = targetVersion
switch drawing.requiredContentVersion {
case .version1:
// 早期马克笔、钢笔和铅笔墨迹集
syncEditable(drawing)
case .version2:
// iPadOS 17 时代的墨迹:等宽笔、书法钢笔、水彩笔、蜡笔
syncIfRecipientsSupportVersion2(drawing)
case .version3, .version4:
// 后续推出的新特性,如笔杆倾角/旋转数据(barrel-roll)和芦苇笔
syncEditableOnlyToCurrentClients(drawing)
@unknown default:
showReadOnlyPreview(for: drawing)
}
若某份绘图所需的版本高于接收方所能加载的版本,请为支持该特性的客户端保留高保真的 PKDrawing,并为旧设备提供只读预览或单独的降级副本,切勿静默覆盖。更深入的版本兼容对照表请参阅 references/pencilkit-patterns.md。
导出为图片
根据绘图数据生成 UIImage。
func exportImage(from drawing: PKDrawing, scale: CGFloat = 2.0) -> UIImage {
drawing.image(from: drawing.bounds, scale: scale)
}
// 导出指定区域
let region = CGRect(x: 0, y: 0, width: 500, height: 500)
let scale = UITraitCollection.current.displayScale
let croppedImage = drawing.image(from: region, scale: scale)
笔画检查与分析
可以访问单条笔画、对应的墨迹属性以及控制点。
for stroke in drawing.strokes {
let ink = stroke.ink
print("墨迹类型: \(ink.inkType), 颜色: \(ink.color)")
print("渲染包围盒: \(stroke.renderBounds)")
// 访问路径点
let path = stroke.path
print("点数: \(path.count), 创建时间: \(path.creationDate)")
// 沿路径按距离插值采样
for point in path.interpolatedPoints(by: .distance(10)) {
print("坐标位置: \(point.location), 压力值: \(point.force)")
}
}
通过代码构建笔画
仅在需要通过代码生成墨迹路径时,才需查阅 通过代码构建笔画;常规的绘制和笔画检查无需使用这些高级构造器。
SwiftUI 集成
在 SwiftUI 中,可将 PKCanvasView 包装在 UIViewRepresentable 中使用。
import SwiftUI
import PencilKit
struct CanvasView: UIViewRepresentable {
@Binding var drawing: PKDrawing
@Binding var toolPickerVisible: Bool
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
canvas.drawingPolicy = .anyInput
canvas.drawing = drawing
context.coordinator.toolPicker.addObserver(canvas)
return canvas
}
func updateUIView(_ canvas: PKCanvasView, context: Context) {
if canvas.drawing != drawing {
canvas.drawing = drawing
}
let toolPicker = context.coordinator.toolPicker
toolPicker.setVisible(toolPickerVisible, forFirstResponder: canvas)
if toolPickerVisible { canvas.becomeFirstResponder() }
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, PKCanvasViewDelegate {
let parent: CanvasView
let toolPicker = PKToolPicker()
init(_ parent: CanvasView) {
self.parent = parent
super.init()
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
parent.drawing = canvasView.drawing
}
}
}
对于 SwiftUI 包装器,请使用规范的 绘制策略 对照表来配置输入策略。
在 SwiftUI 中使用
struct DrawingScreen: View {
@State private var drawing = PKDrawing()
@State private var showToolPicker = true
var body: some View {
CanvasView(drawing: $drawing, toolPickerVisible: $showToolPicker)
.ignoresSafeArea()
}
}
与 PaperKit 的关系
PaperKit(iOS 26+)在 PencilKit 的基础上进行了扩展,提供了包含形状、文本框、图片、贴纸和放大镜在内的完整标记(markup)体验。如果除了自由手绘外还需要结构化标记功能,请配合使用同级的 paperkit skill。
| 功能特性 | PencilKit | PaperKit |
|---|---|---|
| 自由手绘 | 是 | 是 |
| 形状与线条 | 否 | 是 |
| 文本框 | 否 | 是 |
| 图片与贴纸 | 否 | 是 |
<!-- truncated for translation batch; full body continues in source -->






