CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

90

Quality

90%

Does it follow best practices?

Impact

Average score across 248 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

pencilkit-patterns.mdskills/pencilkit/references/

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
  • Custom Tool Picker Items
  • Canvas View Delegate Lifecycle
  • Undo/Redo Support
  • Thumbnail Generation
  • Drawing Comparison and Scoring
  • Content Version Management
  • Advanced SwiftUI Wrapper

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

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

skills

README.md

tile.json