其他
在 Apple 平台用 PencilKit 实现手写绘图…
适用于 iOS/iPadOS/Mac Catalyst/visionOS 上需要 Apple Pencil 或手指绘图、工具栏、签名、批注、手写采集、PKDrawing 序列化同步导出,以及 PencilKit/PaperKit 交接的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 39/100 ×0.15
适用场景
在 Apple 平台用 PencilKi特定
解决思路
- Setup:PencilKit 不需要 entitlement 或 Info.plist 条目;在需要绘图的文件中 import PencilKit,并创建 PKCanvasView。
- 平台可用性:iOS 13+、iPadOS 13+、Mac Catalyst 13.1+、visionOS 1.0+。
- Capture-to-Export 第 1 步 Capture:在 canvasViewDrawingDidChange(_:) 中读取 canvasView.drawing;保留上一版已持久化 revision,直到新 revision 完成其余 checkpoints。
- Capture-to-Export 第 2 步 Serialize:用 dataRepresentation() 生成数据,原子写入,并执行 decode validate/fix/retry loop;当 PKDrawing(data:) 仍抛错时不要把 bytes 标记为有效。
- Capture-to-Export 第 3 步 Version-gate:在可编辑同步前应用 Content Version Compatibility;如果接收方无法加载 drawing,保留 full-fidelity source,并使用已有兼容 fallback 或只读 preview。
- Capture-to-Export 第 4 步 Sync:只发送已验证且兼容的数据,收到 acknowledgement 后再标记 revision 已同步;传输或冲突失败时保留 pending revision,解决原因后重试,不要丢弃最后 good copy。
- Capture-to-Export 第 5 步 Export:调用 image(from:scale:) 前验证 drawing region 非空且 scale 符合预期;bounds 无效时跳过导出,且不要改动已序列化 drawing。
- PKCanvasView Basics:PKCanvasView 是 UIScrollView 子类,可捕获 Apple Pencil 和手指输入并渲染 strokes。
- PKCanvasView Basics 配置:设置 canvasView.delegate = self。
- PKCanvasView Basics 配置:设置 canvasView.drawingPolicy = .anyInput(按需选择策略)。
- PKCanvasView Basics 配置:设置 canvasView.tool = PKInkingTool(.pen, color: .black, width: 5)。
- PKCanvasView Basics 配置:设置 canvasView.frame = view.bounds。
- PKCanvasView Basics 配置:设置 canvasView.autoresizingMask = [.flexibleWidth, .flexibleHeight]。
- PKCanvasView Basics 配置:view.addSubview(canvasView)。
- PKCanvasView Basics 实现:实现 canvasViewDrawingDidChange(_:) 来保存或处理绘图变化。
- Drawing Policies:.default 在 tool picker 可见时遵循 UIPencilInteraction.prefersPencilOnlyDrawing,否则 Pencil-only。
- Drawing Policies:.anyInput 允许 pencil 和 finger 都可绘制。
- Drawing Policies:.pencilOnly 只允许 Apple Pencil 触摸在 canvas 上绘制。
- Drawing Policies 选择:需要系统标准 Pencil-primary canvas 且 tool picker 的 drawing-policy 控件应跟随用户 Pencil 偏好时使用 .default。
- Drawing Policies 选择:签名板、白板或明确允许手指绘制模式时使用 .anyInput。
- Drawing Policies 选择:手指输入绝不应产生 strokes 时使用 .pencilOnly。
- Configuring the Canvas:设置大绘图区并可滚动,例如 canvasView.contentSize = CGSize(width: 2000, height: 3000)。
- Configuring the Canvas:用 canvasView.isRulerActive = true 启用/禁用 ruler。
- Configuring the Canvas:用 canvasView.tool = PKInkingTool(.pencil, color: .blue, width: 3) 或 canvasView.tool = PKEraserTool(.vector) 以编程方式设置当前工具。
- PKToolPicker:PKToolPicker 显示浮动绘图工具面板,canvas 会自动采用选中工具。
- PKToolPicker 显示:在 viewDidAppear(_:) 中调用 toolPicker.addObserver(canvasView)。
- PKToolPicker 显示:调用 toolPicker.setVisible(true, forFirstResponder: canvasView)。
- PKToolPicker 显示:调用 canvasView.becomeFirstResponder()。
- Custom Tool Picker Items:用 PKToolPicker(toolItems:) 创建带特定工具的 tool picker。
- Custom Tool Picker Items 可用性:PKToolPicker(toolItems:) 和自定义 tool picker item 类需要 iOS/iPadOS 18+、Mac Catalyst 18+、visionOS 2+;这些 item 类在 macOS 上从 macOS 26 开始可用。
- Custom Tool Picker Items 示例:PKToolPickerInkingItem(type: .pen, color: .black, width: 5)。
- Custom Tool Picker Items 示例:PKToolPickerInkingItem(type: .pencil, color: .gray, width: 5)。
- Custom Tool Picker Items 示例:PKToolPickerInkingItem(type: .marker, color: .yellow, width: 12)。
- Custom Tool Picker Items 示例:PKToolPickerEraserItem(type: .vector)。
- Custom Tool Picker Items 示例:PKToolPickerLassoItem()。
- Custom Tool Picker Items 示例:PKToolPickerRulerItem()。
- Ink Types:.pen 为平滑、压感 pen。
- Ink Types:.pencil 为带倾斜阴影的纹理 pencil。
- Ink Types:.marker 为半透明 highlighter。
- Ink Types:.monoline 为等宽 pen。
- Ink Types:.fountainPen 为变宽 calligraphy pen。
- Ink Types:.watercolor 为可混合 watercolor brush。
- Ink Types:.crayon 为纹理 crayon。
- Ink Types:.reed 为 reed pen(iOS/iPadOS/macOS/visionOS 26+)。
- Content Versions:把 Content Version Compatibility 作为 canvas 和 tool picker 的单一 version map 与 compatibility gate。
- PKDrawing Serialization:PKDrawing 是保存所有 stroke data 的 value type(struct);为持久化将其序列化为 Data。
- PKDrawing Serialization Save:let data = drawing.dataRepresentation(); try data.write(to: fileURL, options: .atomic)。
- PKDrawing Serialization Load:let data = try Data(contentsOf: fileURL); return try PKDrawing(data: data)。
- Decode Validate/Fix/Retry Loop:对同步或用户提供的数据,先用 PKDrawing(data:) validate。
- Decode Validate/Fix/Retry Loop:decode 失败时保留原始 bytes。
- Decode Validate/Fix/Retry Loop:通过重新获取完整 revision 或选择之前生成过的兼容副本来 fix 原因。
- Decode Validate/Fix/Retry Loop:然后 retry decode;只有 retry 成功后才赋值 drawing。
- Decode Validate/Fix/Retry Loop:如果 recovery 仍失败,保持 source 不变,并显示错误或可用只读 preview,不要用 try? 抑制失败。
- Combining Drawings:var drawing1 = PKDrawing(); let drawing2 = PKDrawing(); drawing1.append(drawing2)。
- Combining Drawings:非 mutating 合并用 let combined = drawing1.appending(drawing2)。
- Transforming Drawings:let scaled = drawing.transformed(using: CGAffineTransform(scaleX: 2, y: 2))。
- Transforming Drawings:let translated = drawing.transformed(using: CGAffineTransform(translationX: 100, y: 0))。
- Content Version Compatibility:sync、migration、downgrade 或跨设备编辑任务中,用 requiredContentVersion 作为 compatibility gate。
- Content Version Compatibility:当旧客户端必须继续编辑时,选择显式的 maximumSupportedContentVersion。
- Content Version Compatibility:设置 canvasView.maximumSupportedContentVersion = targetVersion。
- Content Version Compatibility:设置 toolPicker.maximumSupportedContentVersion = targetVersion。
- Content Version Compatibility:switch drawing.requiredContentVersion,.version1 表示旧 marker/pen/pencil ink set,调用 syncEditable(drawing)。
- Content Version Compatibility:.version2 表示 iPadOS 17 时代 inks(monoline、fountain pen、watercolor、crayon),调用 syncIfRecipientsSupportVersion2(drawing)。
- Content Version Compatibility:.version3 和 .version4 表示 barrel-roll data、Reed Pen 等后续功能,调用 syncEditableOnlyToCurrentClients(drawing)。
- Content Version Compatibility:@unknown default 调用 showReadOnlyPreview(for: drawing)。
- Content Version Compatibility:如果 drawing 需要比接收方所能加载的更新的版本,为支持客户端保留 full-fidelity PKDrawing,并提供只读 preview 或单独 fallback,不要静默覆盖。
- Content Version Compatibility:查阅 references/pencilkit-patterns.md 获取更深入的 compatibility table。
- Exporting to Image:用 drawing.image(from: drawing.bounds, scale: scale) 从 drawing 生成 UIImage。
- Exporting to Image:导出特定区域时 let region = CGRect(x: 0, y: 0, width: 500, height: 500); let scale = UITraitCollection.current.displayScale; let croppedImage = drawing.image(from: region, scale: scale)。
- Stroke Inspection:遍历 for stroke in drawing.strokes,读取 stroke.ink。
- Stroke Inspection:打印 ink.inkType 和 ink.color。
- Stroke Inspection:读取 stroke.renderBounds。
- Stroke Inspection:let path = stroke.path; 读取 path.count 和 path.creationDate。
- Stroke Inspection:用 path.interpolatedPoints(by: .distance(10)) 插值,读取 point.location 和 point.force。
- Constructing Strokes Programmatically:只有生成 ink paths 时才查阅 references/pencilkit-patterns.md#constructing-strokes-programmatically;普通绘制和 inspection 不需要高级 constructors。
- SwiftUI Integration:用 UIViewRepresentable 包装 PKCanvasView。
- SwiftUI Integration makeUIView:创建 PKCanvasView。
- SwiftUI Integration makeUIView:设置 canvas.delegate = context.coordinator。
- SwiftUI Integration makeUIView:设置 canvas.drawingPolicy = .anyInput。
- SwiftUI Integration makeUIView:设置 canvas.drawing = drawing。
- SwiftUI Integration makeUIView:context.coordinator.toolPicker.addObserver(canvas)。
- SwiftUI Integration makeUIView:返回 canvas。
- SwiftUI Integration updateUIView:if canvas.drawing != drawing { canvas.drawing = drawing }。
- SwiftUI Integration updateUIView:let toolPicker = context.coordinator.toolPicker; toolPicker.setVisible(toolPickerVisible, forFirstResponder: canvas)。
- SwiftUI Integration updateUIView:if toolPickerVisible { canvas.becomeFirstResponder() }。
- SwiftUI Integration makeCoordinator:返回 Coordinator(self)。
- SwiftUI Integration Coordinator:class Coordinator: NSObject, PKCanvasViewDelegate。
- SwiftUI Integration Coordinator:let toolPicker = PKToolPicker()。
- SwiftUI Integration Coordinator:init(_ parent: CanvasView) 中保存 parent 并调用 super.init()。
- SwiftUI Integration Coordinator:canvasViewDrawingDidChange(_:) 中设置 parent.drawing = canvasView.drawing。
- SwiftUI Integration:对 SwiftUI wrappers,使用 Drawing Policies 表设置 input policy。
- SwiftUI Usage:@State private var drawing = PKDrawing()。
- SwiftUI Usage:@State private var showToolPicker = true。
- SwiftUI Usage:body 中使用 CanvasView(drawing: $drawing, toolPickerVisible: $showToolPicker).ignoresSafeArea()。
- PaperKit Relationship:PaperKit(iOS 26+)在 PencilKit 之上提供完整 markup 体验,包括 shapes、text boxes、images、stickers、loupes。
- PaperKit Relationship:需要结构化 markup 而不只是 freeform drawing 时使用 sibling paperkit skill。
- PaperKit Relationship 能力对比:PencilKit 支持 freeform drawing,不支持 shapes、text boxes、images、stickers、loupes、markup toolbar、markup insertion UI,数据模型为 PKDrawing。
- PaperKit Relationship 能力对比:PaperKit 支持 freeform drawing、shapes、text boxes、images、stickers、loupes、markup toolbar,markup insertion UI 用 MarkupEditViewController、MarkupToolbarViewController,数据模型为 PaperMarkup。
- PaperKit Relationship:PaperKit 底层使用 PencilKit;PaperMarkupViewController 接受 PKTool 作为 drawingTool 属性,PaperMarkup 可 append PKDrawing。
- Common Mistakes:显示 tool picker 时不要只 setVisible;必须同时调用 canvasView.becomeFirstResponder()。
- Common Mistakes:不要为同一 canvas 创建多个 tool pickers;每个 canvas 一个 PKToolPicker,并保存为 property,不要在每次 appearance 新建。
- Common Mistakes:不要忽略 content versions 的向后兼容;在同步 editable drawings 前对 canvas 和 tool picker 应用 Content Version Compatibility gate。
- Common Mistakes:不要用 dataRepresentation() 比较 drawings;它用于持久化和交换,精确值比较用 PKDrawing equality,视觉/近似比较检查 strokes 或 rendered images。
- Common Mistakes 正确示例:判断精确相等用 if drawing1 == drawing2。
- Review Checklist:确认 PKCanvasView.drawingPolicy 符合 canonical policy table。
- Review Checklist:确认 PKToolPicker 保存为 property,而不是每次 appearance 重新创建。
- Review Checklist:确认调用 canvasView.becomeFirstResponder() 以显示 tool picker。
- Review Checklist:确认在显示 picker 前把 canvas 添加为 PKToolPicker observer。
- Review Checklist:确认 drawing 通过 dataRepresentation() 序列化,并通过 PKDrawing(data:) 加载。
- Review Checklist:确认使用 canvasViewDrawingDidChange delegate method 跟踪变化。
- Review Checklist:如果需要向后兼容,确认在 canvas 和 tool picker 上都设置 maximumSupportedContentVersion。
- Review Checklist:确认 custom tool picker item 代码对 iOS/iPadOS 18+ 和 visionOS 2+ 做了 guard。
- Review Checklist:确认导出图片使用适合设备的 scale factor。
- Review Checklist:确认 SwiftUI wrapper 通过检查 drawing != binding 避免无限更新循环。
- Review Checklist:确认 image export 前检查 drawing bounds(空 drawing 的 bounds 为 .zero)。
- References:扩展 PencilKit patterns(advanced strokes、content versions、delegates)见 references/pencilkit-patterns.md。
- References:查阅官方 PencilKit framework、PKCanvasView、PKDrawing、PKToolPicker、PKInkingTool、PKStroke、Drawing with PencilKit、Configuring the PencilKit tool picker 文档。
适用边界 · 注意事项
- 目标平台不是 iOS/iPadOS/Mac Catalyst/visionOS,或无法引入 PencilKit/PKCanvasView 时,不适用。
- 需要结构化 markup(shapes、text boxes、images、stickers、loupes、markup toolbar/insertion UI)时,不要仅用 PencilKit,应改用 PaperKit(iOS 26+)。
- 接收方客户端不支持 drawing 所需 content version 且必须可编辑同步时,不应直接覆盖为可编辑版本;应保留 full-fidelity source 并给只读 preview/fallback。