关于 pencilkit-drawing
面向 Apple 平台开发者的 PencilKit 技能,讲解用 Swift 捕获手写输入、管理画笔并保存或导出绘图,也介绍 SwiftUI 封装。
主要功能
- 01使用 PKCanvasView 捕获笔迹
- 02配置 PKToolPicker 与绘图工具
- 03序列化、合并和变换绘图
- 04将绘图导出为图像
- 05检查笔画并集成 SwiftUI
适用场景
- 01为应用添加 Apple Pencil 和手指绘图
- 02保存并重新载入用户绘图
- 03导出绘图或指定区域为图像
- 04在 SwiftUI 界面中嵌入画布
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill pencilkit-drawing安装后在 Claude Code 中输入/pencilkit-drawing确认已加载。
附带文件references/paperkit-integration.mdreferences/pencilkit-patterns.md
详细介绍
PencilKit 绘图
使用 PKCanvasView 捕捉 Apple Pencil 和手指输入,使用 PKToolPicker
管理绘图工具,使用 PKDrawing 序列化绘图,并在 SwiftUI 中封装
PencilKit。面向 Swift 6.2 / iOS 26+。
目录
设置
PencilKit 无需任何权利或 Info.plist 条目。导入 PencilKit
并创建一个 PKCanvasView。
import PencilKit
平台可用性: iOS 13+、iPadOS 13+、Mac Catalyst 13.1+、visionOS 1.0+。
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 | 使用 Apple Pencil 绘图;使用手指滚动 |
.anyInput | 使用 Apple Pencil 和手指绘图 |
.pencilOnly | 仅使用 Apple Pencil 绘图;手指始终用于滚动 |
canvasView.drawingPolicy = .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.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
}
自定义工具选择器项目
创建包含指定工具的工具选择器。
let toolPicker = PKToolPicker(toolItems: [
PKToolPickerInkingItem(type: .pen, color: .black),
PKToolPickerInkingItem(type: .pencil, color: .gray),
PKToolPickerInkingItem(type: .marker, color: .yellow),
PKToolPickerEraserItem(type: .vector),
PKToolPickerLassoItem(),
PKToolPickerRulerItem()
])
墨水类型
| 类型 | 说明 |
|---|---|
.pen | 平滑、对压力敏感的钢笔 |
.pencil | 带纹理且可倾斜着色的铅笔 |
.marker | 半透明荧光笔 |
.monoline | 宽度均匀的钢笔 |
.fountainPen | 宽度可变的书法笔 |
.watercolor | 可混色的水彩笔刷 |
.crayon | 带纹理的蜡笔 |
PKDrawing 序列化
PKDrawing 是一种值类型(结构体),用于保存所有笔画数据。将其序列化为
Data 以便持久化。
// Save
func saveDrawing(_ drawing: PKDrawing) throws {
let data = drawing.dataRepresentation()
try data.write(to: fileURL)
}
// Load
func loadDrawing() throws -> PKDrawing {
let data = try Data(contentsOf: fileURL)
return try PKDrawing(data: data)
}
合并绘图
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))
导出为图像
从绘图生成 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)")
}
}
以编程方式构建笔画
let points = [
PKStrokePoint(location: CGPoint(x: 0, y: 0), timeOffset: 0,
size: CGSize(width: 5, height: 5), opacity: 1,
force: 0.5, azimuth: 0, altitude: .pi / 2),
PKStrokePoint(location: CGPoint(x: 100, y: 100), timeOffset: 0.1,
size: CGSize(width: 5, height: 5), opacity: 1,
force: 0.5, azimuth: 0, altitude: .pi / 2)
]
let path = PKStrokePath(controlPoints: points, creationDate: Date())
let stroke = PKStroke(ink: PKInk(.pen, color: .black), path: path,
transform: .identity, mask: nil)
let drawing = PKDrawing(strokes: [stroke])
SwiftUI 集成
在 UIViewRepresentable 中封装 PKCanvasView,以便在 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
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 中使用
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 |
|---|---|---|
| 自由绘图 | 是 | 是 |
| 形状和线条 | 否 | 是 |
| 文本框 | 否 | 是 |
| 图像和贴纸 | 否 | 是 |
| 标注工具栏 | 否 | 是 |
| 数据模型 | PKDrawing | PaperMarkup |
PaperKit 在底层使用 PencilKit——PaperMarkupViewController 的
drawingTool 属性接受 PKTool,而 PaperMarkup 可以追加
PKDrawing。PaperKit 的使用模式请参阅 references/paperkit-integration.md。
常见错误
不要:忘记对工具选择器调用 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()
不要:忽略向后兼容性所需的内容版本
较新的墨水类型会在较旧的操作系统版本上导致崩溃。如果需要保持绘图的向后兼容性,请设置 maximumSupportedContentVersion。
// WRONG: Saves a drawing with .watercolor, crashes on iOS 16
canvasView.tool = PKInkingTool(.watercolor, color: .blue)
// CORRECT: Limit content version for compatibility
canvasView.maximumSupportedContentVersion = .version2
不要:通过数据表示形式比较绘图
PKDrawing 数据并不确定;相同的视觉绘图可能生成不同的字节。请改用相等运算符。
// WRONG
if drawing1.dataRepresentation() == drawing2.dataRepresentation() { }
// CORRECT
if drawing1 == drawing2 { }
审查清单
-
PKCanvasView.drawingPolicy设置正确(以 Apple Pencil 为主的应用使用.default) - 将
PKToolPicker存储为属性,而不是每次显示时重新创建 - 调用
canvasView.becomeFirstResponder()以显示工具选取器 - 使用
dataRepresentation()序列化绘图,并通过PKDrawing(data:)加载 - 使用
canvasViewDrawingDidChange代理方法跟踪更改 - 如需向后兼容,设置
maximumSupportedContentVersion - 导出的图像使用适合设备的缩放因子
- SwiftUI 封装通过检查
drawing != binding避免无限更新循环 - 在成为第一响应者之前添加工具选取器观察者
- 导出图像前检查绘图边界(空绘图的边界为
.zero)
参考资料
- 扩展的 PencilKit 模式(高级笔迹、随手写、代理):
references/pencilkit-patterns.md - PaperKit 集成模式:
references/paperkit-integration.md - PencilKit 框架
- PKCanvasView
- PKDrawing
- PKToolPicker
- PKInkingTool
- PKStroke
- 使用 PencilKit 绘图
- 配置 PencilKit 工具选取器