mcpField
P
pencilkit-drawing

pencilkit-drawing

github.com/dpearson2699开发1.2k403 次安装13 周前更新
代码仓库

关于 pencilkit-drawing

面向 Apple 平台开发者的 PencilKit 技能,讲解用 Swift 捕获手写输入、管理画笔并保存或导出绘图,也介绍 SwiftUI 封装。

主要功能

  1. 01使用 PKCanvasView 捕获笔迹
  2. 02配置 PKToolPicker 与绘图工具
  3. 03序列化、合并和变换绘图
  4. 04将绘图导出为图像
  5. 05检查笔画并集成 SwiftUI

适用场景

  1. 01为应用添加 Apple Pencil 和手指绘图
  2. 02保存并重新载入用户绘图
  3. 03导出绘图或指定区域为图像
  4. 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。

功能PencilKitPaperKit
自由绘图是是
形状和线条否是
文本框否是
图像和贴纸否是
标注工具栏否是
数据模型PKDrawingPaperMarkup

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)

参考资料

评论

支持 Markdown