What's new in PencilKit
Swift & UI 进阶 22m

PencilKit 新增功能

What's new in PencilKit

2020年6月24日

在 Apple 官方观看视频

一句话判断

PencilKit 在 iOS 14 中终于支持以编程方式创建和修改笔画(Stroke),这意味着你可以用代码生成绘图、实现撤销重做、以及在背景层添加内容,PencilKit 从一个”只能用系统画板”的框架变成了真正可定制的绘图引擎。

这场 Session 讲了什么

PencilKit 自 iOS 13 引入以来,提供了一套完整的 Apple Pencil 绘图体验,包括画笔、橡皮擦、尺子等工具。但它有一个致命的限制:你只能通过用户的手指/Pencil 输入来创建内容,不能用代码创建笔画。iOS 14 解除了这个限制。

Session 介绍了四项重要更新。第一,PKStrokePKStrokePath API 允许你用代码创建和修改笔画,包括设置颜色、宽度、工具类型等属性。第二,PKCanvasViewdrawing 属性现在支持直接修改,你可以添加、删除或替换 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:) 方法,支持任意分辨率。
WWDC 2020