mcpField
P
pencilkit

pencilkit

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

pencilkit について

Appleプラットフォーム開発者向けのPencilKitスキルです。描画入力やツール選択、保存、画像出力に加え、同期時のデータ検証とコンテンツバージョン互換性を扱います。

主な機能

  1. 01Pencilと指の入力を取得
  2. 02キャンバスと描画ツールを設定
  3. 03PKDrawingのシリアライズと検証
  4. 04ストロークの確認・結合・変換
  5. 05描画を画像として出力
  6. 06SwiftUIキャンバスとの統合

活用シーン

  1. 01手書きキャンバスの構築
  2. 02描画データの保存と同期
  3. 03描画の画像出力
  4. 04SwiftUIへのキャンバス埋め込み

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill pencilkit

インストール後、Claude Code で/pencilkitを入力して読み込まれたか確認します。

同梱ファイルevals/evals.jsonreferences/pencilkit-patterns.md

詳細

PencilKit

PKCanvasView を使用して Apple Pencil と指の入力をキャプチャし、PKToolPicker で描画ツールを管理し、PKDrawing で描画をシリアライズし、PencilKit を SwiftUI でラップします。

目次

セットアップ

PencilKit にエンタイトルメントや Info.plist の項目は必要ありません。PencilKit をインポートし、PKCanvasView を作成します。

import PencilKit

プラットフォーム対応状況: iOS 13 以降、iPadOS 13 以降、Mac Catalyst 13.1 以降、visionOS 1.0 以降。

キャプチャからエクスポートまでのワークフロー

  1. キャプチャ: canvasViewDrawingDidChange(_:) から canvasView.drawing を読み取ります。新しいリビジョンが残りのチェックポイントを完了するまで、以前に永続化したリビジョンを保持します。
  2. シリアライズ: dataRepresentation() を作成してアトミックに書き込み、デコード・検証・修正・再試行ループを実行します。PKDrawing(data:) が引き続きエラーをスローする場合、そのバイト列を有効としてマークしないでください。
  3. バージョンの確認: 編集可能な同期を行う前に、コンテンツバージョンの互換性を適用します。受信側で描画を読み込めない場合は、完全な忠実度のソースを保持し、既存の互換性のある代替手段または読み取り専用プレビューを使用します。
  4. 同期: 検証済みで互換性のあるデータのみを送信し、確認応答の後にリビジョンを同期済みとしてマークします。転送または競合のエラーが発生した場合は、保留中のリビジョンを保持し、原因を解決してから、最後に正常だったコピーを破棄せずに再試行します。
  5. エクスポート: image(from:scale:) を呼び出す前に、描画領域が空でないことと意図したスケールを検証します。境界が無効な場合はエクスポートをスキップし、シリアライズ済みの描画は変更しません。

PKCanvasView の基本

PKCanvasView は、Apple Pencil と指の入力をキャプチャし、ストロークを描画する UIScrollView サブクラスです。

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ツールピッカーが表示されている場合は UIPencilInteraction.prefersPencilOnlyDrawing に従い、それ以外の場合は Pencil のみ
.anyInputPencil と指の両方で描画
.pencilOnlyキャンバス上では Apple Pencil のタッチのみで描画
canvasView.drawingPolicy = .pencilOnly

ツールピッカーの描画ポリシーコントロールでユーザーの Pencil の設定に従わせる、システム標準の Pencil 優先キャンバスには .default を使用します。署名パッド、ホワイトボード、または指での描画を明示的に許可するモードには .anyInput を使用します。指の入力でストロークを作成させない場合は .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.addObserver(canvasView)
        toolPicker.setVisible(true, forFirstResponder: canvasView)
        canvasView.becomeFirstResponder()
    }
}
カスタムツールピッカー項目

特定のツールを含むツールピッカーを作成します。PKToolPicker(toolItems:) とカスタムツールピッカー項目クラスには、iOS/iPadOS 18 以降、Mac Catalyst 18 以降、visionOS 2 以降が必要です。これらの項目クラスは 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()
])
インクの種類
種類説明
.pen滑らかで筆圧に反応するペン
.pencil傾きによる陰影に対応したテクスチャ付き鉛筆
.marker半透明の蛍光ペン
.monoline線幅が一定のペン
.fountainPen線幅が変化するカリグラフィーペン
.watercolor混色可能な水彩ブラシ
.crayonテクスチャ付きクレヨン
.reedリードペン(iOS/iPadOS/macOS/visionOS 26 以降)
コンテンツバージョン

キャンバスとツールピッカーの両方について、単一のバージョンマップおよび互換性ゲートとしてコンテンツバージョンの互換性を使用します。

PKDrawing のシリアル化

PKDrawing は、すべてのストロークデータを保持する値型(構造体)です。永続化するには、Data にシリアル化します。

// Save
func saveDrawing(_ drawing: PKDrawing) throws {
    let data = drawing.dataRepresentation()
    try data.write(to: fileURL, options: .atomic)
}

// Load
func loadDrawing() throws -> PKDrawing {
    let data = try Data(contentsOf: fileURL)
    return try PKDrawing(data: data)
}
デコードの検証/修正/再試行ループ

同期データまたはユーザー提供データの場合は、PKDrawing(data:) で検証します。失敗した場合は元のバイト列を保持し、完全なリビジョンを再取得するか、以前に生成した互換性のあるコピーを選択して原因を修正してから、デコードを再試行します。再試行に成功した後でのみ、描画を割り当てます。復旧できない場合は、try? で失敗を握りつぶさず、ソースを変更せずに保持し、エラーまたは利用可能な読み取り専用プレビューを表示します。

do {
    canvasView.drawing = try PKDrawing(data: correctedData) // retry
} catch {
    showReadOnlyPreview(for: document, loadError: error)
}
描画の結合
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))

コンテンツバージョンの互換性

同期、移行、ダウングレード、またはデバイス間での編集では、互換性の基準としてrequiredContentVersionを使用します。また、古いクライアントでも編集を続けられるようにする必要がある場合は、maximumSupportedContentVersionを明示的に選択します。

let targetVersion: PKContentVersion = .version1
canvasView.maximumSupportedContentVersion = targetVersion
toolPicker.maximumSupportedContentVersion = targetVersion

switch drawing.requiredContentVersion {
case .version1:
    // Older marker, pen, and pencil ink set
    syncEditable(drawing)
case .version2:
    // iPadOS 17-era inks: monoline, fountain pen, watercolor, crayon
    syncIfRecipientsSupportVersion2(drawing)
case .version3, .version4:
    // Later features such as barrel-roll data and Reed Pen
    syncEditableOnlyToCurrentClients(drawing)
@unknown default:
    showReadOnlyPreview(for: drawing)
}

描画が受信側で読み込めるバージョンより新しいバージョンを必要とする場合は、対応するクライアント向けに完全な忠実度のPKDrawingを保持し、黙って上書きするのではなく、読み取り専用プレビューまたは別のフォールバックを提供します。詳しい互換性表については、references/pencilkit-patterns.mdを参照してください。

画像への書き出し

描画から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)")
    }
}
プログラムによるストロークの構築

生成したインクパスでのみプログラムによるストロークの構築を読み込んでください。通常の描画や確認には、高度なコンストラクターは必要ありません。

SwiftUI との統合

SwiftUI では、UIViewRepresentable でPKCanvasViewをラップします。

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 のラッパーでは、標準の描画ポリシー表を使用して入力ポリシーを設定してください。

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
フリーフォームの描画はいはい
図形と線いいえはい
テキストボックスいいえはい
画像とステッカーいいえはい
ルーペいいえはい
マークアップツールバーいいえはい
マークアップ挿入 UIいいえMarkupEditViewController、MarkupToolbarViewController
データモデルPKDrawingPaperMarkup

PaperKit は内部で PencilKit を使用します。PaperMarkupViewControllerはdrawingToolプロパティにPKToolを受け取り、PaperMarkupはPKDrawingを追加できます。

よくある間違い

やってはいけないこと:ツールピッカーに対して becomeFirstResponder を呼び忘れる

ツールピッカーは、関連付けられたレスポンダーがファーストレスポンダーの場合にのみ表示されます。

// WRONG: Tool picker never shows
toolPicker.setVisible(true, forFirstResponder: canvasView)

// CORRECT: Also become first responder
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()
やってはいけないこと:同じキャンバスに複数のツールピッカーを作成する

キャンバスごとに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()
やってはいけないこと:後方互換性のためのコンテンツバージョンを無視する

編集可能な描画を同期する前に、キャンバスとツールピッカーの両方にコンテンツバージョンの互換性の基準を適用してください。

やってはいけないこと:データ表現で描画を比較する

dataRepresentation()は永続化とデータ交換のためのものであり、比較には使用しません。値の完全一致を確認するにはPKDrawingの等価性を使用し、視覚的または近似的な比較にはストロークやレンダリング画像を調べてください。

// WRONG
if drawing1.dataRepresentation() == drawing2.dataRepresentation() { }

// CORRECT
if drawing1 == drawing2 { }

レビューチェックリスト

  • PKCanvasView.drawingPolicy が標準のポリシーテーブルに従っている
  • PKToolPicker がプロパティとして保持され、表示のたびに再作成されていない
  • ツールピッカーを表示するために canvasView.becomeFirstResponder() が呼び出されている
  • ピッカーを表示する前に、キャンバスが PKToolPicker のオブザーバーとして追加されている
  • 描画が dataRepresentation() でシリアライズされ、PKDrawing(data:) で読み込まれている
  • 変更の追跡に canvasViewDrawingDidChange デリゲートメソッドが使用されている
  • 下位互換性が必要な場合、キャンバスとツールピッカーの両方で maximumSupportedContentVersion が設定されている
  • カスタムツールピッカー項目のコードが iOS/iPadOS 18+ および visionOS 2+ 用にガードされている
  • エクスポート画像で、デバイスに適したスケール係数が使用されている
  • SwiftUI ラッパーが drawing != binding を確認して無限更新ループを回避している
  • 画像をエクスポートする前に描画の境界が確認されている(空の描画の境界は .zero)

参考資料

コメント