CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

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

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

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

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

skills

.mcp.json

README.md

tile.json