pencilkit-drawing について
Apple プラットフォームの開発者向け PencilKit スキルです。入力の取得、描画ツールの管理、描画の保存・書き出し、ストローク確認、SwiftUI でのラップ方法を解説します。
主な機能
- 01PKCanvasView でストロークを取得
- 02PKToolPicker と描画ツールを設定
- 03描画のシリアライズ・結合・変換
- 04描画を画像として書き出し
- 05ストローク確認と SwiftUI 連携
活用シーン
- 01アプリに Apple Pencil と指での描画を追加
- 02ユーザーの描画を保存・再読み込み
- 03描画や指定領域を画像として書き出し
- 04SwiftUI 画面にキャンバスを埋め込み
インストール
コマンドをコピーしてターミナルで実行します。
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 で描画をシリアライズして、PencilKit を SwiftUI でラップします。対象は Swift 6.2/iOS 26 以降です。
内容
- セットアップ
- PKCanvasView の基本
- PKToolPicker
- PKDrawing のシリアライズ
- 画像へのエクスポート
- ストロークの調査
- SwiftUI との統合
- PaperKit との関係
- よくある間違い
- レビュー用チェックリスト
- 参考資料
セットアップ
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 との統合
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
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 を使用してください。
| 機能 | PencilKit | PaperKit |
|---|---|---|
| フリーフォームの描画 | あり | あり |
| 図形と線 | なし | あり |
| テキストボックス | なし | あり |
| 画像とステッカー | なし | あり |
| マークアップツールバー | なし | あり |
| データモデル | PKDrawing | PaperMarkup |
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()
やってはいけないこと:同じキャンバスに複数のツールピッカーを作成する
1 つのキャンバスにつき PKToolPicker は 1 つにしてください。余分に作成すると、表示上の競合が発生します。
// 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()
やってはいけないこと:後方互換性のためのコンテンツバージョンを無視する
新しいインクの種類は、古い OS バージョンではクラッシュの原因になります。後方互換性のある描画が必要な場合は、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)
参考資料
- PencilKit の拡張パターン(高度なストローク、Scribble、デリゲート):
references/pencilkit-patterns.md - PaperKit の統合パターン:
references/paperkit-integration.md - PencilKit フレームワーク
- PKCanvasView
- PKDrawing
- PKToolPicker
- PKInkingTool
- PKStroke
- PencilKit を使った描画
- PencilKit ツールピッカーの設定