PluginBench
Skill
Pass
Audit score 90

pencilkit

dpearson2699/swift-ios-skills

Add Apple Pencil drawing, tool picker, and stroke serialization to iOS/iPadOS apps.

What is pencilkit?

PencilKit provides PKCanvasView for capturing Apple Pencil and finger input, PKToolPicker for tool selection, and PKDrawing for serialization and export. Use it when building drawing apps, annotation features, handwriting capture, signature fields, or any Apple Pencil-powered experience on iOS/iPadOS/visionOS.

  • Capture Apple Pencil and finger strokes with PKCanvasView and configurable drawing policies
  • Manage drawing tools (pen, pencil, marker, eraser, lasso, ruler) via PKToolPicker
  • Serialize drawings to Data and load from persistent storage with PKDrawing
  • Export drawings to UIImage with custom scale and region selection
  • Inspect stroke data and transform drawings with affine transforms
  • Handle content version compatibility across OS versions and sync scenarios

How to install pencilkit

npx skills add https://github.com/dpearson2699/swift-ios-skills --skill pencilkit
Prerequisites
  • Swift 6.3 or later
  • iOS 13+, iPadOS 13+, Mac Catalyst 13.1+, or visionOS 1.0+
  • No special entitlements or Info.plist entries required
Claude Code
Cursor
Windsurf
Cline

How to use pencilkit

  1. 1.Import PencilKit and create a PKCanvasView instance
  2. 2.Set the canvas delegate and configure drawing policy (.default, .anyInput, or .pencilOnly)
  3. 3.Create and configure a PKToolPicker, add it as an observer to the canvas, and make the canvas first responder
  4. 4.Set the current tool programmatically or let the tool picker control it
  5. 5.Listen to canvasViewDrawingDidChange(_:) delegate method to detect drawing changes
  6. 6.Serialize drawings with drawing.dataRepresentation() and load with PKDrawing(data:)
  7. 7.Export to image using drawing.image(from:scale:) or handle content version compatibility before sync

Use cases

Good for
  • Building drawing or sketching apps with tool selection and canvas management
  • Adding annotation or markup features to documents or images
  • Creating signature capture fields with finger or Pencil input
  • Implementing handwriting recognition workflows with stroke inspection
  • Syncing drawings across devices while maintaining compatibility with older OS versions
Who it's for
  • iOS/iPadOS app developers building creative or annotation tools
  • Teams implementing cross-device drawing sync with version compatibility requirements
  • Developers adding signature or handwriting capture to existing apps
  • Engineers building accessibility-aware drawing experiences

pencilkit FAQ

What drawing policies should I use?

Use .default for system-standard Pencil-primary canvases when the tool picker should follow the user's Pencil preference. Use .anyInput for signature pads or whiteboards where both Pencil and finger should draw. Use .pencilOnly when finger input should never create strokes.

How do I handle drawings that require newer iOS versions?

Check drawing.requiredContentVersion before syncing. If it exceeds the target OS version, preserve the full-fidelity PKDrawing for capable clients and provide a read-only preview or fallback instead of silently overwriting it.

Can I create a custom tool picker with specific tools?

Yes, use PKToolPicker(toolItems:) with custom tool picker item classes (iOS/iPadOS 18+, visionOS 2+). You can specify ink types, colors, widths, and include eraser, lasso, and ruler items.

How do I combine or transform drawings?

Use drawing.appending(_:) to combine drawings non-mutatively, or drawing.transformed(using:) to apply affine transforms like scaling or translation.

What happens if drawing deserialization fails?

Handle decode failures explicitly with do-catch instead of try?. Show a read-only preview or fallback content rather than silently failing or suppressing the error.

Full instructions (SKILL.md)

Source of truth, from dpearson2699/swift-ios-skills.


name: pencilkit description: "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."

PencilKit

Capture Apple Pencil and finger input using PKCanvasView, manage drawing tools with PKToolPicker, serialize drawings with PKDrawing, and wrap PencilKit in SwiftUI. Targets Swift 6.3 / iOS 26+.

Contents

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+.

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

PolicyBehavior
.defaultRespects UIPencilInteraction.prefersPencilOnlyDrawing when the tool picker is visible; otherwise Pencil-only
.anyInputBoth pencil and finger draw
.pencilOnlyOnly 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

TypeDescription
.penSmooth, pressure-sensitive pen
.pencilTextured pencil with tilt shading
.markerSemi-transparent highlighter
.monolineUniform-width pen
.fountainPenVariable-width calligraphy pen
.watercolorBlendable watercolor brush
.crayonTextured crayon
.reedReed pen (iOS/iPadOS/macOS/visionOS 26+)

Content Versions

When drawings sync to older OS versions, check requiredContentVersion before uploading or cap new content by setting maximumSupportedContentVersion on both the PKCanvasView and PKToolPicker.

VersionContent
.version1iPadOS 14-era inks: marker, pen, pencil
.version2iPadOS 17 inks: monoline, fountain pen, watercolor, crayon
.version3Barrel-roll angle data
.version4Reed pen

In compatibility reviews, state the complete version map before recommending a cap. If the plan exposes a curated picker or specific ink choices, also mention the availability of PKToolPicker(toolItems:) and custom picker item APIs. When existing content exceeds the target OS version, sync a verified fallback PKDrawing or restrict editing up front; do not rely only on a warning.

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

// Load
func loadDrawing() throws -> PKDrawing {
    let data = try Data(contentsOf: fileURL)
    return try PKDrawing(data: data)
}

When loading synced or user-provided drawings, handle decode failures explicitly instead of suppressing them with try?:

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

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 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, call out the input-policy choice in the wrapper guidance. Use .anyInput when finger drawing is part of the product. Use .pencilOnly when touch should stay reserved for scrolling or selection. Use .default when you want PencilKit's system behavior: with the tool picker visible, it follows the user's Pencil-only drawing setting; otherwise only Apple Pencil draws.

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.

CapabilityPencilKitPaperKit
Freeform drawingYesYes
Shapes & linesNoYes
Text boxesNoYes
Images & stickersNoYes
LoupesNoYes
Markup toolbarNoYes
Markup insertion UINoMarkupEditViewController, MarkupToolbarViewController
Data modelPKDrawingPaperMarkup

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: Oversimplify .default drawing policy

When explaining input behavior, .default is system-setting aware. If the tool picker is visible, it respects the user's Pencil-only drawing preference; otherwise only Apple Pencil draws.

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

Earlier OS versions throw when loading PKDrawing data that uses unsupported inks. Check requiredContentVersion before syncing, or set maximumSupportedContentVersion on both the canvas and tool picker to restrict new content.

// WRONG: only limits the canvas; picker can still expose newer inks
canvasView.tool = PKInkingTool(.watercolor, color: .blue)
canvasView.maximumSupportedContentVersion = .version1

// CORRECT: limit both surfaces for iPadOS 14-era ink compatibility
if #available(iOS 17.0, *) {
    canvasView.maximumSupportedContentVersion = .version1
    toolPicker.maximumSupportedContentVersion = .version1
}

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 set appropriately and .default explained as system-setting aware
  • 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