CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

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

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

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.

let 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),
    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
        stride(from: 0, to: CGFloat(stroke.path.count), by: 1).map { i in
            stroke.path.interpolatedLocation(at: i)
        }
    }

    let candPoints = candidate.strokes.flatMap { stroke in
        stride(from: 0, to: CGFloat(stroke.path.count), by: 1).map { i in
            stroke.path.interpolatedLocation(at: i)
        }
    }

    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: PKDrawing = // loaded drawing
let version = drawing.requiredContentVersion

switch version {
case .version1:
    // Basic inks: pen, pencil, marker
    break
case .version2:
    // Adds monoline and fountain pen
    break
case .version3:
    // Adds watercolor and crayon
    break
case .version4:
    // Latest inks including reed
    break
@unknown default:
    break
}

// Limit canvas to a specific version
canvasView.maximumSupportedContentVersion = .version2
toolPicker.maximumSupportedContentVersion = .version2

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

CHANGELOG.md

README.md

tile.json