pencilkit-drawing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PencilKit Drawing

PencilKit 绘图

Capture Apple Pencil and finger input using
PKCanvasView
, manage drawing tools with
PKToolPicker
, serialize drawings with
PKDrawing
, and wrap PencilKit in SwiftUI. Targets Swift 6.2 / iOS 26+.
使用
PKCanvasView
捕捉Apple Pencil和手指输入,借助
PKToolPicker
管理绘图工具,通过
PKDrawing
实现绘图内容序列化,并在SwiftUI中封装PencilKit。适配Swift 6.2 / iOS 26+。

Contents

目录

Setup

设置

PencilKit requires no entitlements or Info.plist entries. Import
PencilKit
and create a
PKCanvasView
.
swift
import PencilKit
Platform availability: iOS 13+, iPadOS 13+, Mac Catalyst 13.1+, visionOS 1.0+.
PencilKit不需要权限或Info.plist配置项。导入
PencilKit
并创建
PKCanvasView
实例。
swift
import PencilKit
平台兼容性: iOS 13+, iPadOS 13+, Mac Catalyst 13.1+, visionOS 1.0+。

PKCanvasView Basics

PKCanvasView 基础

PKCanvasView
is a
UIScrollView
subclass that captures Apple Pencil and finger input and renders strokes.
swift
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
    }
}
PKCanvasView
UIScrollView
的子类,可捕捉Apple Pencil和手指输入并渲染笔触。
swift
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 Policies

绘图策略

PolicyBehavior
.default
Apple Pencil draws; finger scrolls
.anyInput
Both pencil and finger draw
.pencilOnly
Only Apple Pencil draws; finger always scrolls
swift
canvasView.drawingPolicy = .pencilOnly
策略行为
.default
Apple Pencil绘图;手指滚动
.anyInput
Pencil和手指均可绘图
.pencilOnly
仅Apple Pencil可绘图;手指始终用于滚动
swift
canvasView.drawingPolicy = .pencilOnly

Configuring the Canvas

配置画布

swift
// 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)
swift
// 设置大尺寸绘图区域(支持滚动)
canvasView.contentSize = CGSize(width: 2000, height: 3000)

// 启用/禁用标尺
canvasView.isRulerActive = true

// 以编程方式设置当前工具
canvasView.tool = PKInkingTool(.pencil, color: .blue, width: 3)
canvasView.tool = PKEraserTool(.vector)

PKToolPicker

PKToolPicker 工具选择器

PKToolPicker
displays a floating palette of drawing tools. The canvas automatically adopts the selected tool.
swift
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()
    }
}
PKToolPicker
显示浮动的绘图工具面板,画布会自动应用选中的工具。
swift
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()
    }
}

Custom Tool Picker Items

自定义工具选择器项

Create a tool picker with specific tools.
swift
let toolPicker = PKToolPicker(toolItems: [
    PKToolPickerInkingItem(type: .pen, color: .black),
    PKToolPickerInkingItem(type: .pencil, color: .gray),
    PKToolPickerInkingItem(type: .marker, color: .yellow),
    PKToolPickerEraserItem(type: .vector),
    PKToolPickerLassoItem(),
    PKToolPickerRulerItem()
])
创建包含特定工具的工具选择器。
swift
let toolPicker = PKToolPicker(toolItems: [
    PKToolPickerInkingItem(type: .pen, color: .black),
    PKToolPickerInkingItem(type: .pencil, color: .gray),
    PKToolPickerInkingItem(type: .marker, color: .yellow),
    PKToolPickerEraserItem(type: .vector),
    PKToolPickerLassoItem(),
    PKToolPickerRulerItem()
])

Ink Types

墨迹类型

TypeDescription
.pen
Smooth, pressure-sensitive pen
.pencil
Textured pencil with tilt shading
.marker
Semi-transparent highlighter
.monoline
Uniform-width pen
.fountainPen
Variable-width calligraphy pen
.watercolor
Blendable watercolor brush
.crayon
Textured crayon
类型描述
.pen
平滑的压感钢笔
.pencil
带倾斜阴影的纹理铅笔
.marker
半透明荧光笔
.monoline
等宽钢笔
.fountainPen
可变宽度的书法钢笔
.watercolor
可混合的水彩画笔
.crayon
纹理蜡笔

PKDrawing Serialization

PKDrawing 序列化

PKDrawing
is a value type (struct) that holds all stroke data. Serialize it to
Data
for persistence.
swift
// 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)
}
PKDrawing
是值类型(结构体),存储所有笔触数据。可序列化为
Data
以持久化存储。
swift
// 保存
func saveDrawing(_ drawing: PKDrawing) throws {
    let data = drawing.dataRepresentation()
    try data.write(to: fileURL)
}

// 加载
func loadDrawing() throws -> PKDrawing {
    let data = try Data(contentsOf: fileURL)
    return try PKDrawing(data: data)
}

Combining Drawings

合并绘图内容

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

// Non-mutating
let combined = drawing1.appending(drawing2)
swift
var drawing1 = PKDrawing()
let drawing2 = PKDrawing()
drawing1.append(drawing2)

// 非可变方式
let combined = drawing1.appending(drawing2)

Transforming Drawings

变换绘图内容

swift
let scaled = drawing.transformed(using: CGAffineTransform(scaleX: 2, y: 2))
let translated = drawing.transformed(using: CGAffineTransform(translationX: 100, y: 0))
swift
let scaled = drawing.transformed(using: CGAffineTransform(scaleX: 2, y: 2))
let translated = drawing.transformed(using: CGAffineTransform(translationX: 100, y: 0))

Exporting to Image

导出为图片

Generate a
UIImage
from a drawing.
swift
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)
将绘图内容生成为
UIImage
swift
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)

Stroke Inspection

笔触检查

Access individual strokes, their ink, and control points.
swift
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)")
    }
}
访问单个笔触、其墨迹属性和控制点。
swift
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)")
    }
}

Constructing Strokes Programmatically

以编程方式构造笔触

swift
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])
swift
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 Integration

SwiftUI 集成

Wrap
PKCanvasView
in a
UIViewRepresentable
for SwiftUI.
swift
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
        }
    }
}
PKCanvasView
封装为
UIViewRepresentable
以在SwiftUI中使用。
swift
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
        }
    }
}

Usage in SwiftUI

在SwiftUI中使用

swift
struct DrawingScreen: View {
    @State private var drawing = PKDrawing()
    @State private var showToolPicker = true

    var body: some View {
        CanvasView(drawing: $drawing, toolPickerVisible: $showToolPicker)
            .ignoresSafeArea()
    }
}
swift
struct DrawingScreen: View {
    @State private var drawing = PKDrawing()
    @State private var showToolPicker = true

    var body: some View {
        CanvasView(drawing: $drawing, toolPickerVisible: $showToolPicker)
            .ignoresSafeArea()
    }
}

PaperKit Relationship

与PaperKit的关系

PaperKit (iOS 26+) extends PencilKit with a complete markup experience including shapes, text boxes, images, stickers, and loupes. Use PaperKit when you need more than freeform drawing.
CapabilityPencilKitPaperKit
Freeform drawingYesYes
Shapes & linesNoYes
Text boxesNoYes
Images & stickersNoYes
Markup toolbarNoYes
Data model
PKDrawing
PaperMarkup
PaperKit uses PencilKit under the hood --
PaperMarkupViewController
accepts
PKTool
for its
drawingTool
property and
PaperMarkup
can append a
PKDrawing
. See
references/paperkit-integration.md
for PaperKit patterns.
PaperKit(iOS 26+)是PencilKit的扩展,提供完整的标注体验,包括形状、文本框、图片、贴纸和放大镜。如果需要的功能超出自由绘图,可使用PaperKit。
功能PencilKitPaperKit
自由绘图
形状与线条
文本框
图片与贴纸
标注工具栏
数据模型
PKDrawing
PaperMarkup
PaperKit底层基于PencilKit实现 --
PaperMarkupViewController
drawingTool
属性支持
PKTool
PaperMarkup
也可追加
PKDrawing
内容。如需了解PaperKit的使用模式,请查看
references/paperkit-integration.md

Common Mistakes

常见错误

DON'T: Forget to call becomeFirstResponder for the tool picker

错误做法:忘记为工具选择器调用becomeFirstResponder

The tool picker only appears when its associated responder is first responder.
swift
// WRONG: Tool picker never shows
toolPicker.setVisible(true, forFirstResponder: canvasView)

// CORRECT: Also become first responder
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()
只有当关联的响应者成为第一响应者时,工具选择器才会显示。
swift
// 错误:工具选择器永远不会显示
toolPicker.setVisible(true, forFirstResponder: canvasView)

// 正确:同时设置为第一响应者
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()

DON'T: Create multiple tool pickers for the same canvas

错误做法:为同一个画布创建多个工具选择器

One
PKToolPicker
per canvas. Creating extras causes visual conflicts.
swift
// 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()
一个画布对应一个
PKToolPicker
。创建多个会导致视觉冲突。
swift
// 错误
func viewDidAppear(_ animated: Bool) {
    let picker = PKToolPicker()  // 每次显示都创建新的选择器
    picker.setVisible(true, forFirstResponder: canvasView)
}

// 正确:将选择器作为属性存储
let toolPicker = PKToolPicker()

DON'T: Ignore content version for backward compatibility

错误做法:忽略内容版本以实现向后兼容

Newer ink types crash on older OS versions. Set
maximumSupportedContentVersion
if you need backward-compatible drawings.
swift
// 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
较新的墨迹类型在旧版系统上会崩溃。如果需要向后兼容的绘图内容,请设置
maximumSupportedContentVersion
swift
// 错误:保存包含.watercolor的绘图内容,在iOS 16上会崩溃
canvasView.tool = PKInkingTool(.watercolor, color: .blue)

// 正确:限制内容版本以确保兼容性
canvasView.maximumSupportedContentVersion = .version2

DON'T: Compare drawings by data representation

错误做法:通过数据表示比较绘图内容

PKDrawing
data is not deterministic; the same visual drawing can produce different bytes. Use equality operators instead.
swift
// WRONG
if drawing1.dataRepresentation() == drawing2.dataRepresentation() { }

// CORRECT
if drawing1 == drawing2 { }
PKDrawing
的数据表示并非确定性的,相同视觉效果的绘图可能生成不同的字节。应使用相等运算符进行比较。
swift
// 错误
if drawing1.dataRepresentation() == drawing2.dataRepresentation() { }

// 正确
if drawing1 == drawing2 { }

Review Checklist

检查清单

  • PKCanvasView.drawingPolicy
    set appropriately (
    .default
    for Pencil-primary apps)
  • PKToolPicker
    stored as a property, not recreated each appearance
  • canvasView.becomeFirstResponder()
    called to show the tool picker
  • Drawing serialized via
    dataRepresentation()
    and loaded via
    PKDrawing(data:)
  • canvasViewDrawingDidChange
    delegate method used to track changes
  • maximumSupportedContentVersion
    set if backward compatibility needed
  • Exported images use appropriate scale factor for the device
  • SwiftUI wrapper avoids infinite update loops by checking
    drawing != binding
  • Tool picker observer added before becoming first responder
  • Drawing bounds checked before image export (empty drawings have
    .zero
    bounds)
  • 已正确设置
    PKCanvasView.drawingPolicy
    (以Pencil为主的应用推荐使用
    .default
  • PKToolPicker
    已作为属性存储,而非每次显示时重新创建
  • 已调用
    canvasView.becomeFirstResponder()
    以显示工具选择器
  • 已通过
    dataRepresentation()
    序列化绘图内容,并通过
    PKDrawing(data:)
    加载
  • 已使用
    canvasViewDrawingDidChange
    代理方法跟踪内容变更
  • 若需要向后兼容,已设置
    maximumSupportedContentVersion
  • 导出图片时使用了适配设备的缩放因子
  • SwiftUI封装通过检查
    drawing != binding
    避免无限更新循环
  • 在设置为第一响应者前已添加工具选择器观察者
  • 导出图片前已检查绘图边界(空绘图的边界为
    .zero

References

参考资料