pencilkit

pencilkit

热门

集成 Apple Pencil 手绘功能,支持 PKCanvasView、PKToolPicker、PKDrawing 序列化与导出、笔画检查以及 PencilKit/PaperKit 协作与切换。适用于开发绘画应用、批注功能、手写捕捉、电子签名板、内容版本兼容的墨迹工作流,或在 iOS/iPadOS/visionOS 上构建 Apple Pencil 交互体验。

967Star
49Fork
更新于 2026/7/31
SKILL.md
只读
名称
pencilkit
描述

集成 Apple Pencil 手绘功能,支持 PKCanvasView、PKToolPicker、PKDrawing 序列化与导出、笔画检查以及 PencilKit/PaperKit 协作与切换。适用于开发绘画应用、批注功能、手写捕捉、电子签名板、内容版本兼容的墨迹工作流,或在 iOS/iPadOS/visionOS 上构建 Apple Pencil 交互体验。

PencilKit

使用 PKCanvasView 捕捉 Apple Pencil 和手指输入,使用 PKToolPicker 管理绘画工具,使用 PKDrawing 序列化绘图数据,并在 SwiftUI 中封装 PencilKit。

目录

环境配置

PencilKit 无需额外的权限声明或 Info.plist 配置。直接导入 PencilKit 并创建 PKCanvasView 即可。

import PencilKit

支持平台: iOS 13+、iPadOS 13+、Mac Catalyst 13.1+、visionOS 1.0+。

从捕捉到导出的工作流

  1. 捕捉:canvasViewDrawingDidChange(_:) 回调中读取 canvasView.drawing;在新的修订版本完成后续所有检查点之前,请保留上一个已持久化的版本。
  2. 序列化: 调用 dataRepresentation() 生成数据并执行原子写入,随后运行 解码校验/修复/重试循环。只要 PKDrawing(data:) 仍抛出异常,切勿将数据标记为有效。
  3. 版本门控: 在进行可编辑同步前应用 内容版本兼容性。若接收方无法加载该绘图,请保留完整精度的源数据,并使用已有的兼容降级方案或只读预览。
  4. 同步: 仅发送经过校验且兼容的数据,并在收到确认后将该版本标记为已同步。若遇到传输或冲突失败,请保留待处理的版本,排除原因后再重试,切勿丢弃最后一个已知良好的副本。
  5. 导出: 在调用 image(from:scale:) 前,先校验绘图区域非空且缩放比例符合预期;若 Bounds 无效则跳过导出,且不要修改已序列化的绘图数据。

PKCanvasView 基础用法

PKCanvasViewUIScrollView 的子类,用于捕捉 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) {
        // 绘图已更改 -- 在此保存或处理
    }
}

绘制策略

策略 行为说明
.default 当工具选择器可见时遵循 UIPencilInteraction.prefersPencilOnlyDrawing 设置;否则仅允许 Pencil 绘制
.anyInput Apple Pencil 和手指均可绘制
.pencilOnly 仅允许 Apple Pencil 在画板上绘制
canvasView.drawingPolicy = .pencilOnly

在工具选择器的绘制策略控制需要遵循用户 Pencil 偏好时,为系统标准且 Pencil 优先的画板使用 .default;在电子签名板、白板或明确需要手指绘制的模式下使用 .anyInput;当手指输入绝不应产生笔画时使用 .pencilOnly

配置画板

// 设置超大绘制区域(可滚动)
canvasView.contentSize = CGSize(width: 2000, height: 3000)

// 启用/禁用直尺工具
canvasView.isRulerActive = true

// 通过代码设置当前工具
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 上自 macOS 26 起可用。

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 以进行持久化存储。

// 保存绘图
func saveDrawing(_ drawing: PKDrawing) throws {
    let data = drawing.dataRepresentation()
    try data.write(to: fileURL, options: .atomic)
}

// 加载绘图
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) // 重试
} catch {
    showReadOnlyPreview(for: document, loadError: error)
}

合并绘图

var drawing1 = PKDrawing()
let drawing2 = PKDrawing()
drawing1.append(drawing2)

// 非破坏性合并
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:
    // 早期马克笔、钢笔和铅笔墨迹集
    syncEditable(drawing)
case .version2:
    // iPadOS 17 时代的墨迹:等宽笔、书法钢笔、水彩笔、蜡笔
    syncIfRecipientsSupportVersion2(drawing)
case .version3, .version4:
    // 后续推出的新特性,如笔杆倾角/旋转数据(barrel-roll)和芦苇笔
    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)
}

// 导出指定区域
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.inkType), 颜色: \(ink.color)")
    print("渲染包围盒: \(stroke.renderBounds)")

    // 访问路径点
    let path = stroke.path
    print("点数: \(path.count), 创建时间: \(path.creationDate)")

    // 沿路径按距离插值采样
    for point in path.interpolatedPoints(by: .distance(10)) {
        print("坐标位置: \(point.location), 压力值: \(point.force)")
    }
}

通过代码构建笔画

仅在需要通过代码生成墨迹路径时,才需查阅 通过代码构建笔画;常规的绘制和笔画检查无需使用这些高级构造器。

SwiftUI 集成

在 SwiftUI 中,可将 PKCanvasView 包装在 UIViewRepresentable 中使用。

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 的基础上进行了扩展,提供了包含形状、文本框、图片、贴纸和放大镜在内的完整标记(markup)体验。如果除了自由手绘外还需要结构化标记功能,请配合使用同级的 paperkit skill。

功能特性 PencilKit PaperKit
自由手绘
形状与线条
文本框
图片与贴纸

<!-- truncated for translation batch; full body continues in source -->