加入 Apple Pencil 手繪與繪圖功能,支援 PKCanvasView、PKToolPicker、PKDrawing 序列化與匯出、筆觸檢視,以及 PencilKit 與 PaperKit 之間的接續處理(handoff)。適用於在 iOS/iPadOS/visionOS 上開發繪圖應用程式、標註功能、手寫擷取、簽名欄位、相容內容版本的墨水工作流程,或打造基於 Apple Pencil 的互動體驗。
PencilKit
使用 PKCanvasView 擷取 Apple Pencil 與手指輸入,透過 PKToolPicker 管理繪圖工具,使用 PKDrawing 序列化繪圖內容,並將 PencilKit 封裝至 SwiftUI 中。
Contents
- Setup
- Capture-to-Export Workflow
- PKCanvasView Basics
- PKToolPicker
- PKDrawing Serialization
- Content Version Compatibility
- Exporting to Image
- Stroke Inspection
- SwiftUI Integration
- PaperKit Relationship
- Common Mistakes
- Review Checklist
- References
Setup
PencilKit 不需要任何權限設定(Entitlements)或 Info.plist 設定項目。只需匯入 PencilKit 並建立 PKCanvasView 即可。
import PencilKit
支援平台與版本: iOS 13+、iPadOS 13+、Mac Catalyst 13.1+、visionOS 1.0+。
Capture-to-Export Workflow
- 擷取: 從
canvasViewDrawingDidChange(_:)讀取canvasView.drawing;在新的修訂版本完成剩餘的檢查點前,請保留上一次已持久化的修訂版本。 - 序列化: 呼叫
dataRepresentation()建立資料,進行原子化寫入(atomically),並執行解碼驗證/修復/重試迴圈。若PKDrawing(data:)仍會拋出異常,切勿將該 Data 標示為有效。 - 版本控管: 在進行可編輯同步前,先套用內容版本相容性。若接收端無法載入該繪圖,請保留高傳真(full-fidelity)的原始資料,並使用現有的相容降級方案或唯讀預覽。
- 同步: 僅傳送經過驗證且相容的資料,並在收到確認後將該修訂版本標示為已同步。發生傳輸或衝突失敗時,保留待處理的修訂版本,解決原因後重試,切勿丟棄最後一份完好的副本。
- 匯出: 在呼叫
image(from:scale:)之前,先確認繪圖區域非空且縮放比例符合預期;若範圍無效則跳過匯出,且不改變已序列化的繪圖內容。
PKCanvasView Basics
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 Policies
| Policy | Behavior |
|---|---|
.default |
當工具選取器可見時,遵循 UIPencilInteraction.prefersPencilOnlyDrawing;否則僅限 Pencil 繪製 |
.anyInput |
Apple Pencil 與手指皆可繪製 |
.pencilOnly |
僅允許 Apple Pencil 的觸控在畫布上繪製 |
canvasView.drawingPolicy = .pencilOnly
當工具選取器的繪圖策略控制項應遵循使用者的 Pencil 偏好時,系統標準且以 Pencil 為主的畫布請使用 .default。如果是簽名板、白板或明確的手指繪圖模式,請使用 .anyInput。若希望手指觸控絕不產生筆觸,請使用 .pencilOnly。
Configuring the Canvas
// 設定大型繪圖區域(可滾動)
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()
}
}
Custom Tool Picker Items
建立包含特定工具的工具選取器。使用 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()
])
Ink Types
| Type | Description |
|---|---|
.pen |
平滑且具感壓效果的原子筆 |
.pencil |
帶有紋理與傾斜斜刷效果的鉛筆 |
.marker |
半透明螢光筆 |
.monoline |
等寬線條筆 |
.fountainPen |
變寬書法鋼筆 |
.watercolor |
可混色的水彩筆 |
.crayon |
具質感質地的蠟筆 |
.reed |
蘆葦筆(iOS/iPadOS/macOS/visionOS 26+) |
Content Versions
使用內容版本相容性作為畫布與工具選取器的單一版本對照與相容性閘口。
PKDrawing Serialization
PKDrawing 是一個包含所有筆觸資料的值類型(struct)。可將其序列化為 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)
}
Decode Validate/Fix/Retry Loop
對於同步或使用者提供的資料:先用 PKDrawing(data:) 進行驗證;若解碼失敗,保留原始位元組資料,並透過重新擷取完整修訂版本或選擇先前產生的相容副本來修復原因;接著重試解碼。只有在重試成功後才能將繪圖指派給畫布。若修復後仍無法復原,請保持原始資料不變,並顯示錯誤訊息或可用的唯讀預覽,切勿使用 try? 壓制錯誤。
do {
canvasView.drawing = try PKDrawing(data: correctedData) // 重試
} catch {
showReadOnlyPreview(for: document, loadError: error)
}
Combining Drawings
var drawing1 = PKDrawing()
let drawing2 = PKDrawing()
drawing1.append(drawing2)
// 不變更原物件(Non-mutating)
let combined = drawing1.appending(drawing2)
Transforming Drawings
let scaled = drawing.transformed(using: CGAffineTransform(scaleX: 2, y: 2))
let translated = drawing.transformed(using: CGAffineTransform(translationX: 100, y: 0))
Content Version Compatibility
在進行同步、版本遷移、降級或跨裝置編輯任務時,請將 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)
}
若繪圖所需的版本高於接收端所能載入的版本,請為具備相應能力的用戶端保留完整高傳真度(full-fidelity)的 PKDrawing,並提供唯讀預覽或獨立的降級備用方案,而不是靜默地覆蓋原始資料。更深入的相容性對照表請參閱 references/pencilkit-patterns.md。
Exporting to Image
從繪圖產生 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)
Stroke Inspection
存取單一筆觸、其墨水種類與控制點。
for stroke in drawing.strokes {
let ink = stroke.ink
print("Ink type: \(ink.inkType), color: \(ink.color)")
print("Bounds: \(stroke.renderBounds)")
// 存取路徑點
let path = stroke.path
print("Points: \(path.count), created: \(path.creationDate)")
// 沿著路徑插值採樣
for point in path.interpolatedPoints(by: .distance(10)) {
print("Location: \(point.location), force: \(point.force)")
}
}
Constructing Strokes Programmatically
僅在需要透過程式產生墨水路徑時載入以程式碼建立筆觸;一般繪圖與筆觸檢視不需要使用這些進階建構子。
SwiftUI Integration
將 PKCanvasView 封裝在 UIViewRepresentable 中以供 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
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 的封裝元件中,請參照標準的繪圖策略表來設定輸入策略。
Usage in 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 Relationship
PaperKit(iOS 26+)擴充了 PencilKit,提供包含形狀、文字方塊、圖片、貼圖與放大鏡在內的完整標註體驗。當你需要結構化標註而非僅是自由手繪時,請搭配使用兄弟技能 paperkit。
| Capability | PencilKit | PaperKit |
|---|---|---|
| 自由繪圖 | 是 | 是 |
| 形狀與線條 | 否 | 是 |
| 文字方塊 | 否 | 是 |
| 圖片與貼圖 | 否 | 是 |
<!-- truncated for translation batch; full body continues in source -->






