mcpbeat

Pencilkit

dpearson2699/pencilkit

Add Apple Pencil drawing with PKCanvasView, PKToolPicker, PKDrawing serialization/export, stroke inspection, and PencilKit/PaperKit handoffs. Use when building drawing apps, annotation features, handwriting capture, signature fields, content-version-safe ink workflows, or Apple Pencil-powered experiences on iOS/iPadOS/visionOS.

7k tokens
context cost
the whole folder, loaded on every use
3
files
instructions only
0
copies elsewhere
how many repositories repackaged it
960
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill pencilkit

The instruction itself

29 sections, as written by the author

PencilKit

Capture Apple Pencil and finger input using PKCanvasView, manage drawing

tools with PKToolPicker, serialize drawings with PKDrawing, and wrap PencilKit in 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 requires no entitlements or Info.plist entries. Import PencilKit

and create a PKCanvasView.

import PencilKit

Platform availability: iOS 13+, iPadOS 13+, Mac Catalyst 13.1+, visionOS 1.0+.

Capture-to-Export Workflow

  • Capture: Read canvasView.drawing from

canvasViewDrawingDidChange(_:); keep the previous persisted revision until

the new revision completes the remaining checkpoints.

  • Serialize: Create dataRepresentation(), write atomically, and run the

decode validate/fix/retry loop. Do not mark

bytes valid when PKDrawing(data:) still throws.

  • Version-gate: Apply Content Version Compatibility

before editable sync. If the recipient cannot load the drawing, preserve the

full-fidelity source and use an existing compatible fallback or read-only

preview.

  • Sync: Send only validated, compatible data and mark the revision synced

after acknowledgement. On transport or conflict failure, retain the pending

revision, resolve the cause, and retry without discarding the last good copy.

  • Export: Validate a nonempty drawing region and intended scale before

calling image(from:scale:); skip export on invalid bounds without altering

the serialized drawing.

PKCanvasView Basics

PKCanvasView is a UIScrollView subclass that captures Apple Pencil and

finger input and renders strokes.

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
    }
}

Drawing Policies

| Policy | Behavior |

|---|---|

| .default | Respects UIPencilInteraction.prefersPencilOnlyDrawing when the tool picker is visible; otherwise Pencil-only |

| .anyInput | Both pencil and finger draw |

| .pencilOnly | Only Apple Pencil touches draw on the canvas |

canvasView.drawingPolicy = .pencilOnly

Use .default for system-standard Pencil-primary canvases when the tool

picker's drawing-policy control should follow the user's Pencil preference. Use

.anyInput for signature pads, whiteboards, or explicit finger-drawing modes.

Use .pencilOnly when finger input should never create strokes.

Configuring the Canvas

// 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 displays a floating palette of drawing tools. The canvas

automatically adopts the selected tool.

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

Create a tool picker with specific tools. PKToolPicker(toolItems:) and

custom tool picker item classes require iOS/iPadOS 18+, Mac Catalyst 18+, and

visionOS 2+; those item classes are available on macOS starting in 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 | 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 |

| .reed | Reed pen (iOS/iPadOS/macOS/visionOS 26+) |

Content Versions

Use Content Version Compatibility as the

single version map and compatibility gate for both the canvas and tool picker.

PKDrawing Serialization

PKDrawing is a value type (struct) that holds all stroke data. Serialize

it to Data for persistence.

// 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)
}

Decode Validate/Fix/Retry Loop

For synced or user-provided data: validate with PKDrawing(data:); on

failure preserve the original bytes and fix the cause by refetching an

intact revision or selecting a previously generated compatible copy; then

retry the decode. Assign the drawing only after a successful retry. If

recovery still fails, keep the source unchanged and show an error or available

read-only preview instead of suppressing the failure with try?.

do {
    canvasView.drawing = try PKDrawing(data: correctedData) // retry
} 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

For sync, migration, downgrade, or cross-device editing tasks, use

requiredContentVersion as the compatibility gate and choose an explicit

maximumSupportedContentVersion when old clients must keep editing.

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)
}

If a drawing requires a newer version than a recipient can load, preserve the

full-fidelity PKDrawing for capable clients and provide a read-only preview or

separate fallback instead of silently overwriting it. See

references/pencilkit-patterns.md for the

deeper compatibility table.

Exporting to Image

Generate a UIImage from a drawing.

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)

Stroke Inspection

Access individual strokes, their ink, and control points.

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)")
    }
}

Constructing Strokes Programmatically

Load Constructing Strokes Programmatically

only for generated ink paths; ordinary drawing and inspection do not need the

advanced constructors.

SwiftUI Integration

Wrap PKCanvasView in a UIViewRepresentable for 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
        }
    }
}

For SwiftUI wrappers, set the input policy using the canonical

Drawing Policies table.

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+) extends PencilKit with a complete markup experience

including shapes, text boxes, images, stickers, and loupes. Use the sibling

paperkit skill when you need structured markup rather than only freeform

drawing.

| Capability | PencilKit | PaperKit |

|---|---|---|

| Freeform drawing | Yes | Yes |

| Shapes & lines | No | Yes |

| Text boxes | No | Yes |

| Images & stickers | No | Yes |

| Loupes | No | Yes |

| Markup toolbar | No | Yes |

| Markup insertion UI | No | MarkupEditViewController, MarkupToolbarViewController |

| Data model | PKDrawing | PaperMarkup |

PaperKit uses PencilKit under the hood: PaperMarkupViewController accepts

PKTool for its drawingTool property, and PaperMarkup can append a

PKDrawing.

Common Mistakes

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

The tool picker only appears when its associated responder is first responder.

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

// CORRECT: Also become first responder
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.

// 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()

DON'T: Ignore content versions for backward compatibility

Apply the Content Version Compatibility gate

to both the canvas and tool picker before syncing editable drawings.

DON'T: Compare drawings by data representation

dataRepresentation() is for persistence and interchange, not comparison.

Use PKDrawing equality for exact value checks, and inspect strokes or rendered

images for visual/approximate comparisons.

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

// CORRECT
if drawing1 == drawing2 { }

Review Checklist

  • [ ] PKCanvasView.drawingPolicy follows the canonical policy table
  • [ ] PKToolPicker stored as a property, not recreated each appearance
  • [ ] canvasView.becomeFirstResponder() called to show the tool picker
  • [ ] Canvas added as a PKToolPicker observer before showing the picker
  • [ ] Drawing serialized via dataRepresentation() and loaded via PKDrawing(data:)
  • [ ] canvasViewDrawingDidChange delegate method used to track changes
  • [ ] maximumSupportedContentVersion set on both canvas and tool picker if backward compatibility is needed
  • [ ] Custom tool picker item code guarded for iOS/iPadOS 18+ and visionOS 2+
  • [ ] Exported images use appropriate scale factor for the device
  • [ ] SwiftUI wrapper avoids infinite update loops by checking drawing != binding
  • [ ] Drawing bounds checked before image export (empty drawings have .zero bounds)

References

How to use it

Copy the folder

Take dpearson2699/pencilkit from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.