PencilKit 新增功能
What's new in PencilKit
2020年6月24日
一句话判断
PencilKit 在 iOS 14 中终于支持以编程方式创建和修改笔画(Stroke),这意味着你可以用代码生成绘图、实现撤销重做、以及在背景层添加内容,PencilKit 从一个”只能用系统画板”的框架变成了真正可定制的绘图引擎。
这场 Session 讲了什么
PencilKit 自 iOS 13 引入以来,提供了一套完整的 Apple Pencil 绘图体验,包括画笔、橡皮擦、尺子等工具。但它有一个致命的限制:你只能通过用户的手指/Pencil 输入来创建内容,不能用代码创建笔画。iOS 14 解除了这个限制。
Session 介绍了四项重要更新。第一,PKStroke 和 PKStrokePath API 允许你用代码创建和修改笔画,包括设置颜色、宽度、工具类型等属性。第二,PKCanvasView 的 drawing 属性现在支持直接修改,你可以添加、删除或替换 Drawing 中的笔画。第三,新增了 Ink(墨水)类型系统,可以指定不同的画笔效果。第四,PKToolPicker 可以在你的 App 中更灵活地使用,包括自定义工具栏布局。
值得深挖的点
程序化创建笔画
这是 PencilKit 开发者最期待的功能。通过 PKStroke,你可以创建一条由一系列点组成的路径。每个点有位置、大小、和透明度属性。你可以用这个功能来实现:预设图形(如模板线条、装饰图案)、自动补全(用户画了一条线后自动延伸)、数据可视化(用笔画风格绘制图表),以及导入外部绘图数据。
背景内容和分层
PencilKit 现在支持在一个 PKDrawing 下方放置背景内容。你可以在 PKCanvasView 的底层放置一个 UIImageView 或自定义 View,用户在上面绘画时背景保持不动。这对标注类 App 非常有用——用户可以在照片、PDF 或地图上画标记。
代码片段
用代码创建笔画
场景:在画布上绘制一条预设的红色直线。
import PencilKit
func createLineStroke(from start: CGPoint, to end: CGPoint) -> PKStroke {
// 创建笔画的路径点
let startPoint = PKStrokePoint(
location: start,
timeOffset: 0,
size: CGSize(width: 3, height: 3), // 笔画宽度
opacity: 1.0,
force: 1.0,
azimuth: Vector(x: 0, y: 1, z: 0),
secondaryScale: 1.0
)
let endPoint = PKStrokePoint(
location: end,
timeOffset: 0.1,
size: CGSize(width: 3, height: 3),
opacity: 1.0,
force: 1.0,
azimuth: Vector(x: 0, y: 1, z: 0),
secondaryScale: 1.0
)
// 创建笔画路径
let path = PKStrokePath(
controlPoints: [startPoint, endPoint],
creationDate: Date()
)
// 创建笔画,指定墨水类型和颜色
let ink = PKInk(.pen, color: .systemRed)
let stroke = PKStroke(
ink: ink,
path: path,
transform: .identity,
mask: nil
)
return stroke
}
// 将创建的笔画添加到画布
func addStrokeToCanvas(canvasView: PKCanvasView) {
var drawing = canvasView.drawing
let lineStroke = createLineStroke(
from: CGPoint(x: 50, y: 50),
to: CGPoint(x: 300, y: 200)
)
// 在现有绘画上追加笔画
drawing.strokes.append(lineStroke)
canvasView.drawing = drawing
}
实现撤销和重做
场景:利用 PKDrawing 的不可变性实现撤销栈。
class DrawingManager {
private var undoStack: [PKDrawing] = []
private var redoStack: [PKDrawing] = []
private(set) var currentDrawing: PKDrawing = PKDrawing()
private let canvasView: PKCanvasView
init(canvasView: PKCanvasView) {
self.canvasView = canvasView
}
// 保存当前状态到撤销栈
func saveState() {
undoStack.append(currentDrawing)
redoStack.removeAll() // 新操作清空重做栈
currentDrawing = canvasView.drawing
}
// 撤销
func undo() {
guard let previous = undoStack.popLast() else { return }
redoStack.append(currentDrawing)
currentDrawing = previous
canvasView.drawing = previous
}
// 重做
func redo() {
guard let next = redoStack.popLast() else { return }
undoStack.append(currentDrawing)
currentDrawing = next
canvasView.drawing = next
}
}
创建带背景的绘图视图
场景:在照片上方叠加绘图层。
import PencilKit
class PhotoAnnotationViewController: UIViewController {
private var canvasView: PKCanvasView!
private var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
setupAnnotationView()
}
private func setupAnnotationView() {
// 底层:照片
imageView = UIImageView(frame: view.bounds)
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "sample_photo")
view.addSubview(imageView)
// 上层:画布(透明背景)
canvasView = PKCanvasView(frame: view.bounds)
canvasView.backgroundColor = .clear
canvasView.isOpaque = false
canvasView.drawingPolicy = .pencilOnly // 只响应 Apple Pencil
view.addSubview(canvasView)
// 显示工具选择器
let toolPicker = PKToolPicker.shared(for: view.window!)
toolPicker?.setVisible(true, forFirstResponder: canvasView)
toolPicker?.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
// 导出合并后的图片
func exportAnnotatedImage() -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
return renderer.image { context in
// 先绘制照片
imageView?.image?.draw(in: view.bounds)
// 再绘制笔记
canvasView.drawing.image(from: view.bounds, scale: UIScreen.main.scale)
.draw(in: view.bounds)
}
}
}
修改现有笔画
场景:将所有笔画的颜色改为蓝色。
func changeAllStrokesToBlue(canvasView: PKCanvasView) {
var drawing = canvasView.drawing
// 遍历所有笔画并修改颜色
let blueInk = PKInk(.pen, color: .systemBlue)
drawing.strokes = drawing.strokes.map { stroke in
// 创建一个新的笔画,只替换 ink
return PKStroke(
ink: blueInk,
path: stroke.path,
transform: stroke.transform,
mask: stroke.mask
)
}
canvasView.drawing = drawing
}
最佳实践
已有项目:如果你在使用 PencilKit 但无法实现撤销重做,利用 PKDrawing 的不可变性(值类型)来构建撤销栈。每次 drawingDidChange 回调时保存一份副本。如果你之前因为”不能用代码创建笔画”而放弃了 PencilKit,现在是重新评估的好时机。
新项目:对于标注类 App(照片标注、PDF 批注、地图标注),PencilKit 是最省力的选择。它提供了专业级的绘图工具、压感支持和系统级的工具选择器。利用新的 PKStroke API 来实现预设图形和智能辅助功能。
还有什么值得关注
- PencilKit 的工具选择器现在可以在
PKCanvasView之外使用,支持浮动工具栏模式。 PKInk类型包括.pen、.pencil、.marker,每种有不同的渲染效果。- 导出绘图时使用
PKDrawing.image(from:scale:)方法,支持任意分辨率。