关于 pencilkit
面向 Apple 平台开发者的 PencilKit 技能,涵盖手写绘制、工具选择、数据持久化与图像导出,并强调版本兼容和同步时的数据校验。
主要功能
- 01捕获 Apple Pencil 和手指输入
- 02配置画布与绘图工具
- 03序列化及校验 PKDrawing
- 04检查笔画并组合或变换
- 05导出绘图图像
- 06集成 SwiftUI 画布
适用场景
- 01构建手写绘图界面
- 02持久化并同步绘图数据
- 03将绘图导出为图像
- 04在 SwiftUI 中嵌入画布
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill pencilkit安装后在 Claude Code 中输入/pencilkit确认已加载。
附带文件evals/evals.jsonreferences/pencilkit-patterns.md
详细介绍
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:)之前,验证绘图区域非空且比例符合预期。如果边界无效,则跳过导出,不要更改序列化后的绘图。
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) {
// Drawing changed -- save or process
}
}
绘图策略
| 策略 | 行为 |
|---|---|
.default | 工具选取器可见时遵循 UIPencilInteraction.prefersPencilOnlyDrawing;否则仅允许 Pencil 绘图 |
.anyInput | 铅笔和手指均可绘图 |
.pencilOnly | 画布仅响应 Apple Pencil 触摸来绘图 |
canvasView.drawingPolicy = .pencilOnly
当工具选取器的绘图策略控件应遵循用户的 Pencil 偏好时,系统标准的 Pencil 优先画布请使用 .default。签名板、白板或明确允许手指绘图的模式请使用 .anyInput。如果不应让手指输入创建笔画,请使用 .pencilOnly。
配置画布
// Set a large drawing area (scrollable)
canvasView.contentSize = CGSize(width: 2000, height: 3000)
// Enable/disable the ruler
canvasView.isRulerActive = true
// Set the current tool programmatically
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 26 起可在 macOS 上使用。
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 以便持久化。
// Save
func saveDrawing(_ drawing: PKDrawing) throws {
let data = drawing.dataRepresentation()
try data.write(to: fileURL, options: .atomic)
}
// Load
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) // retry
} catch {
showReadOnlyPreview(for: document, loadError: error)
}
合并绘图
var drawing1 = PKDrawing()
let drawing2 = PKDrawing()
drawing1.append(drawing2)
// Non-mutating
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:
// Older marker, pen, and pencil ink set
syncEditable(drawing)
case .version2:
// iPadOS 17-era inks: monoline, fountain pen, watercolor, crayon
syncIfRecipientsSupportVersion2(drawing)
case .version3, .version4:
// Later features such as barrel-roll data and Reed Pen
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)
}
// Export a specific region
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 type: \(ink.inkType), color: \(ink.color)")
print("Bounds: \(stroke.renderBounds)")
// Access path points
let path = stroke.path
print("Points: \(path.count), created: \(path.creationDate)")
// Interpolate along the path
for point in path.interpolatedPoints(by: .distance(10)) {
print("Location: \(point.location), force: \(point.force)")
}
}
以编程方式构造笔画
仅在生成墨迹路径时加载 以编程方式构造笔画;普通绘图和检查不需要高级构造方法。
SwiftUI 集成
将 PKCanvasView 封装在 UIViewRepresentable 中,以用于 SwiftUI。
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,提供完整的标注体验。如果需要结构化标注,而非仅自由绘图,请使用同级的 paperkit 技能。
| 能力 | PencilKit | PaperKit |
|---|---|---|
| 自由绘图 | 是 | 是 |
| 形状和线条 | 否 | 是 |
| 文本框 | 否 | 是 |
| 图像和贴纸 | 否 | 是 |
| 放大镜 | 否 | 是 |
| 标注工具栏 | 否 | 是 |
| 标注插入界面 | 否 | MarkupEditViewController、MarkupToolbarViewController |
| 数据模型 | PKDrawing | PaperMarkup |
PaperKit 在底层使用 PencilKit:PaperMarkupViewController 的 drawingTool 属性接受 PKTool,而 PaperMarkup 可以追加 PKDrawing。
常见错误
不要:忘记对工具选取器调用 becomeFirstResponder
只有当关联的响应者是第一响应者时,工具选取器才会显示。
// WRONG: Tool picker never shows
toolPicker.setVisible(true, forFirstResponder: canvasView)
// CORRECT: Also become first responder
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()
不要:为同一画布创建多个工具选取器
每个画布使用一个 PKToolPicker。额外创建会造成视觉冲突。
// WRONG
func viewDidAppear(_ animated: Bool) {
let picker = PKToolPicker() // New picker every appearance
picker.setVisible(true, forFirstResponder: canvasView)
}
// CORRECT: Store picker as a property
let toolPicker = PKToolPicker()
不要:忽略向后兼容性的内容版本
同步可编辑绘图之前,请对画布和工具选取器应用内容版本兼容性门槛。
不要:通过数据表示形式比较绘图
dataRepresentation() 用于持久化和交换,不用于比较。精确比较值时请使用 PKDrawing 相等性;进行视觉或近似比较时,请检查笔画或渲染图像。
// WRONG
if drawing1.dataRepresentation() == drawing2.dataRepresentation() { }
// CORRECT
if drawing1 == drawing2 { }
审查清单
-
PKCanvasView.drawingPolicy遵循规范策略表 - 将
PKToolPicker存储为属性,而不是每次显示时重新创建 - 调用
canvasView.becomeFirstResponder()以显示工具选择器 - 显示工具选择器前,将画布添加为
PKToolPicker观察者 - 使用
dataRepresentation()序列化绘图,并通过PKDrawing(data:)加载 - 使用
canvasViewDrawingDidChange委托方法跟踪更改 - 如需向后兼容,在画布和工具选择器上设置
maximumSupportedContentVersion - 为 iOS/iPadOS 18+ 和 visionOS 2+ 添加自定义工具选择器项代码的条件保护
- 导出的图像使用适合设备的比例因子
- SwiftUI 包装器通过检查
drawing != binding来避免无限更新循环 - 导出图像前检查绘图边界(空绘图的边界为
.zero)
参考资料
- PencilKit 扩展模式(高级笔画、内容版本、委托):references/pencilkit-patterns.md
- PencilKit 框架
- PKCanvasView
- PKDrawing
- PKToolPicker
- PKInkingTool
- PKStroke
- 使用 PencilKit 绘图
- 配置 PencilKit 工具选择器