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.

Skill file

Preview skill file
---
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](#setup)
- [PKCanvasView Basics](#pkcanvasview-basics)
- [PKToolPicker](#pktoolpicker)
- [PKDrawing Serialization](#pkdrawing-serialization)
- [Content Version Compatibility](#content-version-compatibility)
- [Exporting to Image](#exporting-to-image)
- [Stroke Inspection](#stroke-inspection)
- [SwiftUI Integration](#swiftui-integration)
- [PaperKit Relationship](#paperkit-relationship)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Setup

PencilKit requires no entitlements or Info.plist entries. Import `PencilKit`
and create a `PKCanvasView`.

```swift
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.

```swift
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 |

```swift
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

```swift
// 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.

```swift
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.

```swift
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

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

| Version | Content |
|---|---|
| `.version1` | iPadOS 14-era inks: marker, pen, pencil |
| `.version2` | iPadOS 17 inks: monoline, fountain pen, watercolor, crayon |
| `.version3` | Barrel-roll angle data |
| `.version4` | Reed 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.

```swift
// 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?`:

```swift
do {
    canvasView.drawing = try PKDrawing(data: data)
} catch {
    showReadOnlyPreview(for: document, loadError: error)
}
```

### Combining Drawings

```swift
var drawing1 = PKDrawing()
let drawing2 = PKDrawing()
drawing1.append(drawing2)

// Non-mutating
let combined = drawing1.appending(drawing2)
```

### Transforming Drawings

```swift
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.

```swift
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](references/pencilkit-patterns.md) for the
deeper compatibility table.

## Exporting to Image

Generate a `UIImage` from a drawing.

```swift
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.

```swift
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

```swift
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.

```swift
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

```swift
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.

```swift
// 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.

```swift
// 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.

```swift
// 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.

```swift
// 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

- Extended PencilKit patterns (advanced strokes, content versions, delegates): [references/pencilkit-patterns.md](references/pencilkit-patterns.md)
- [PencilKit framework](https://sosumi.ai/documentation/pencilkit)
- [PKCanvasView](https://sosumi.ai/documentation/pencilkit/pkcanvasview)
- [PKDrawing](https://sosumi.ai/documentation/pencilkit/pkdrawing-swift.struct)
- [PKToolPicker](https://sosumi.ai/documentation/pencilkit/pktoolpicker)
- [PKInkingTool](https://sosumi.ai/documentation/pencilkit/pkinkingtool-swift.struct)
- [PKStroke](https://sosumi.ai/documentation/pencilkit/pkstroke-swift.struct)
- [Drawing with PencilKit](https://sosumi.ai/documentation/pencilkit/drawing-with-pencilkit)
- [Configuring the PencilKit tool picker](https://sosumi.ai/documentation/pencilkit/configuring-the-pencilkit-tool-picker)

Source

Creator's repository · dpearson2699/swift-ios-skills

View on GitHub

Security

Security checks in progress
Results will appear here once audits complete
What this skill can do
Reads your filesConnects to the internetRuns code on your machine
Checked by 3 independent security firms
Does it try to trick the AI?Not yet checkedPending · Gen Agent Trust Hub
Does it sneak in hidden code?Not yet checkedPending · Socket
Does it have known bugs?Not yet checkedPending · Snyk