mcpField
P
pencilkit-drawing

pencilkit-drawing

github.com/dpearson2699開発1.2k403 回インストール13 週間前に更新
リポジトリ

pencilkit-drawing について

Apple プラットフォームの開発者向け PencilKit スキルです。入力の取得、描画ツールの管理、描画の保存・書き出し、ストローク確認、SwiftUI でのラップ方法を解説します。

主な機能

  1. 01PKCanvasView でストロークを取得
  2. 02PKToolPicker と描画ツールを設定
  3. 03描画のシリアライズ・結合・変換
  4. 04描画を画像として書き出し
  5. 05ストローク確認と SwiftUI 連携

活用シーン

  1. 01アプリに Apple Pencil と指での描画を追加
  2. 02ユーザーの描画を保存・再読み込み
  3. 03描画や指定領域を画像として書き出し
  4. 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 以降です。

内容

セットアップ

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
    }
}
描画ポリシー
ポリシー動作
.defaultApple Pencil で描画し、指でスクロールします
.anyInputApple Pencil と指の両方で描画します
.pencilOnlyApple 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 を使用してください。

機能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()
やってはいけないこと:同じキャンバスに複数のツールピッカーを作成する

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)

参考資料

コメント