All skills
dpearson2699 avatar

/pencilkit

@45c9085

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.

Use this Skill: https://skilld.dev/gh/dpearson2699/swift-ios-skills/pencilkit

This session only. Nothing lands on disk.

referencespencilkit-patterns.md

≈2.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

PencilKit Extended Patterns

Overflow reference for the pencilkit skill. Contains advanced patterns that exceed the main skill file's scope.

Contents

Tool Picker Observer Pattern

Observe tool picker changes to update custom UI or track tool usage.

import PencilKit

class DrawingController: UIViewController, PKToolPickerObserver {
    let canvasView = PKCanvasView()
    let toolPicker = PKToolPicker()

    override func viewDidLoad() {
        super.viewDidLoad()
        toolPicker.addObserver(self)
        toolPicker.addObserver(canvasView)
    }

    func toolPickerSelectedToolItemDidChange(_ toolPicker: PKToolPicker) {
        let item = toolPicker.selectedToolItem
        print("Selected tool: \(item.identifier)")
    }

    func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
        print("Picker visible: \(toolPicker.isVisible)")
    }

    func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) {
        let obscured = toolPicker.frameObscured(in: view)
        // Adjust content insets to avoid overlap
        canvasView.contentInset.bottom = obscured.height
    }
}

Custom Tool Picker Items

Create custom tools with unique behaviors and icons. Custom tool picker items require iOS/iPadOS 18+, Mac Catalyst 18+, or visionOS 2+.

var customConfig = PKToolPickerCustomItem.Configuration(
    identifier: "com.app.highlighter",
    name: "Highlighter"
)
customConfig.defaultColor = .yellow
customConfig.allowsColorSelection = true
customConfig.defaultWidth = 20
customConfig.widthVariants = [
    10: UIImage(systemName: "line.diagonal")!,
    20: UIImage(systemName: "line.3.horizontal")!,
    40: UIImage(systemName: "rectangle.fill")!
]
customConfig.imageProvider = { item in
    // Return a custom image based on current color/width
    let config = UIImage.SymbolConfiguration(pointSize: 24)
    return UIImage(systemName: "highlighter", withConfiguration: config)!
}

let customItem = PKToolPickerCustomItem(configuration: customConfig)

let toolPicker = PKToolPicker(toolItems: [
    PKToolPickerInkingItem(type: .pen, color: .black, width: 5),
    customItem,
    PKToolPickerEraserItem(type: .vector)
])

Canvas View Delegate Lifecycle

Track the complete drawing lifecycle.

class DrawingManager: NSObject, PKCanvasViewDelegate {
    var hasUnsavedChanges = false
    var isCurrentlyDrawing = false

    func canvasViewDidBeginUsingTool(_ canvasView: PKCanvasView) {
        isCurrentlyDrawing = true
    }

    func canvasViewDidEndUsingTool(_ canvasView: PKCanvasView) {
        isCurrentlyDrawing = false
    }

    func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
        hasUnsavedChanges = true
    }

    func canvasViewDidFinishRendering(_ canvasView: PKCanvasView) {
        // Safe to capture a snapshot for thumbnails
    }
}

Undo/Redo Support

PKCanvasView automatically integrates with UndoManager.

class DrawingViewController: UIViewController {
    let canvasView = PKCanvasView()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(canvasView)

        navigationItem.leftBarButtonItem = UIBarButtonItem(
            systemItem: .undo,
            primaryAction: UIAction { [weak self] _ in
                self?.canvasView.undoManager?.undo()
            }
        )
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            systemItem: .redo,
            primaryAction: UIAction { [weak self] _ in
                self?.canvasView.undoManager?.redo()
            }
        )
    }
}

Thumbnail Generation

Generate thumbnails for document browsers or galleries.

func generateThumbnail(
    for drawing: PKDrawing,
    size: CGSize,
    scale: CGFloat = 2.0
) -> UIImage? {
    let bounds = drawing.bounds
    guard !bounds.isEmpty else { return nil }

    let aspectRatio = bounds.width / bounds.height
    let targetAspect = size.width / size.height
    var renderRect = bounds

    if aspectRatio > targetAspect {
        let scaleFactor = size.width / bounds.width
        renderRect = CGRect(
            x: bounds.minX,
            y: bounds.midY - (size.height / scaleFactor) / 2,
            width: bounds.width,
            height: size.height / scaleFactor
        )
    } else {
        let scaleFactor = size.height / bounds.height
        renderRect = CGRect(
            x: bounds.midX - (size.width / scaleFactor) / 2,
            y: bounds.minY,
            width: size.width / scaleFactor,
            height: bounds.height
        )
    }

    return drawing.image(from: renderRect, scale: scale)
}

Drawing Comparison and Scoring

Compare two drawings by analyzing their strokes and points.

func strokeSimilarity(
    reference: PKDrawing,
    candidate: PKDrawing,
    tolerance: CGFloat = 20
) -> Double {
    let refPoints = reference.strokes.flatMap { stroke in
        stroke.path.interpolatedPoints(by: .distance(5)).map(\.location)
    }

    let candPoints = candidate.strokes.flatMap { stroke in
        stroke.path.interpolatedPoints(by: .distance(5)).map(\.location)
    }

    guard !refPoints.isEmpty else { return 0 }

    var matchCount = 0
    for refPoint in refPoints {
        let minDist = candPoints.map { point in
            hypot(refPoint.x - point.x, refPoint.y - point.y)
        }.min() ?? .infinity

        if minDist <= tolerance { matchCount += 1 }
    }

    return Double(matchCount) / Double(refPoints.count)
}

Constructing Strokes Programmatically

Use these constructors only when an app generates ink rather than receiving it from PKCanvasView:

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

Content Version Management

Handle backward compatibility when sharing drawings across OS versions.

// Check if a drawing uses features beyond a version
let drawing = canvasView.drawing
let version = drawing.requiredContentVersion

switch version {
case .version1:
    // iPadOS 14-era inks: marker, pen, pencil
    break
case .version2:
    // iPadOS 17 inks: monoline, fountain pen, watercolor, crayon
    break
case .version3:
    // Barrel-roll angle data
    break
case .version4:
    // Reed pen
    break
@unknown default:
    break
}

// Limit both canvas and picker to a specific version.
// Use .version1 when saved drawings must load on pre-iPadOS 17 systems.
if #available(iOS 17.0, *) {
    canvasView.maximumSupportedContentVersion = .version1
    toolPicker.maximumSupportedContentVersion = .version1
}

When you allow newer inks, branch before CloudKit or cross-device sync and upload either the original drawing or a verified fallback drawing.

func drawingForPreiPadOS17Sync(_ drawing: PKDrawing) -> PKDrawing? {
    switch drawing.requiredContentVersion {
    case .version1:
        return drawing
    case .version2, .version3, .version4:
        let fallback = version1Fallback(from: drawing)
        guard fallback.requiredContentVersion == .version1 else {
            // Reusing paths can preserve newer metadata, such as barrel-roll data.
            // Sync a thumbnail/message instead of incompatible drawing data.
            return nil
        }
        return fallback
    @unknown default:
        return nil
    }
}

func version1Fallback(from drawing: PKDrawing) -> PKDrawing {
    let strokes = drawing.strokes.map { stroke -> PKStroke in
        var fallback = stroke
        fallback.ink = PKInkingTool(.pen, color: .black, width: 2).ink
        return fallback
    }
    return PKDrawing(strokes: strokes)
}

Advanced SwiftUI Wrapper

A full-featured SwiftUI wrapper with tool picker, undo, and save support.

import SwiftUI
import PencilKit

struct DrawingCanvas: UIViewRepresentable {
    @Binding var drawing: PKDrawing
    var drawingPolicy: PKCanvasViewDrawingPolicy = .anyInput
    var showToolPicker: Bool = true

    func makeUIView(context: Context) -> PKCanvasView {
        let canvas = PKCanvasView()
        canvas.delegate = context.coordinator
        canvas.drawingPolicy = drawingPolicy
        canvas.drawing = drawing
        canvas.backgroundColor = .clear
        canvas.isOpaque = false

        let coordinator = context.coordinator
        coordinator.toolPicker.addObserver(canvas)

        return canvas
    }

    func updateUIView(_ canvas: PKCanvasView, context: Context) {
        let coordinator = context.coordinator

        if canvas.drawing != drawing {
            canvas.drawing = drawing
        }

        coordinator.toolPicker.setVisible(showToolPicker, forFirstResponder: canvas)
        if showToolPicker {
            canvas.becomeFirstResponder()
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    class Coordinator: NSObject, PKCanvasViewDelegate {
        let parent: DrawingCanvas
        let toolPicker = PKToolPicker()

        init(parent: DrawingCanvas) {
            self.parent = parent
        }

        func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
            parent.drawing = canvasView.drawing
        }
    }
}

Source: SKILL.md on GitHub

No alerts16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The PencilKit skill provides comprehensive documentation and Swift code examples for integrating Apple's PencilKit framework into iOS, iPadOS, and visionOS applications. It covers canvas setup, tool management, drawing serialization, and SwiftUI integration. No security risks were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 45c9085. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Steadyupdated 3 months ago

README badge

README badge for dpearson2699/swift-ios-skills/pencilkit