Swift-ios-skills pencilkit
Add Apple Pencil drawing, canvas views, tool pickers, and ink serialization using PencilKit. Use when building drawing apps, annotation features, handwriting capture, signature fields, or any Apple Pencil-powered experience on iOS/iPadOS/visionOS.
git clone https://github.com/dpearson2699/swift-ios-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/dpearson2699/swift-ios-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/pencilkit" ~/.claude/skills/dpearson2699-swift-ios-skills-pencilkit && rm -rf "$T"
skills/pencilkit/SKILL.mdPencilKit
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
- PKCanvasView Basics
- PKToolPicker
- PKDrawing Serialization
- Exporting to Image
- Stroke Inspection
- SwiftUI Integration
- PaperKit Relationship
- Common Mistakes
- Review Checklist
- References
Setup
PencilKit requires no entitlements or Info.plist entries. Import
PencilKit
and create a PKCanvasView.
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.
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 |
|---|---|
| Apple Pencil draws; finger scrolls |
| Both pencil and finger draw |
| Only Apple Pencil draws; finger always scrolls |
canvasView.drawingPolicy = .pencilOnly
Configuring the Canvas
// 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.
class DrawingViewController: UIViewController { let canvasView = PKCanvasView() let toolPicker = PKToolPicker() override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) toolPicker.setVisible(true, forFirstResponder: canvasView) toolPicker.addObserver(canvasView) canvasView.becomeFirstResponder() } }
Custom Tool Picker Items
Create a tool picker with specific tools.
let toolPicker = PKToolPicker(toolItems: [ PKToolPickerInkingItem(type: .pen, color: .black), PKToolPickerInkingItem(type: .pencil, color: .gray), PKToolPickerInkingItem(type: .marker, color: .yellow), PKToolPickerEraserItem(type: .vector), PKToolPickerLassoItem(), PKToolPickerRulerItem() ])
Ink Types
| Type | Description |
|---|---|
| Smooth, pressure-sensitive pen |
| Textured pencil with tilt shading |
| Semi-transparent highlighter |
| Uniform-width pen |
| Variable-width calligraphy pen |
| Blendable watercolor brush |
| Textured crayon |
PKDrawing Serialization
PKDrawing is a value type (struct) that holds all stroke data. Serialize
it to Data for persistence.
// 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) }
Combining Drawings
var drawing1 = PKDrawing() let drawing2 = PKDrawing() drawing1.append(drawing2) // Non-mutating let combined = drawing1.appending(drawing2)
Transforming Drawings
let scaled = drawing.transformed(using: CGAffineTransform(scaleX: 2, y: 2)) let translated = drawing.transformed(using: CGAffineTransform(translationX: 100, y: 0))
Exporting to Image
Generate a
UIImage from a drawing.
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.
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
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.
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 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 } } }
Usage in SwiftUI
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 PaperKit when you need more than freeform drawing.
| Capability | PencilKit | PaperKit |
|---|---|---|
| Freeform drawing | Yes | Yes |
| Shapes & lines | No | Yes |
| Text boxes | No | Yes |
| Images & stickers | No | Yes |
| Markup toolbar | No | Yes |
| Data model | | |
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.
// WRONG: Tool picker never shows toolPicker.setVisible(true, forFirstResponder: canvasView) // CORRECT: Also become first responder toolPicker.setVisible(true, forFirstResponder: canvasView) canvasView.becomeFirstResponder()
DON'T: Create multiple tool pickers for the same canvas
One
PKToolPicker per canvas. Creating extras causes visual conflicts.
// 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 version for backward compatibility
Newer ink types crash on older OS versions. Set
maximumSupportedContentVersion
if you need backward-compatible drawings.
// WRONG: Saves a drawing with .watercolor, crashes on iOS 16 canvasView.tool = PKInkingTool(.watercolor, color: .blue) // CORRECT: Limit content version for compatibility canvasView.maximumSupportedContentVersion = .version2
DON'T: Compare drawings by data representation
PKDrawing data is not deterministic; the same visual drawing can produce
different bytes. Use equality operators instead.
// WRONG if drawing1.dataRepresentation() == drawing2.dataRepresentation() { } // CORRECT if drawing1 == drawing2 { }
Review Checklist
-
set appropriately (PKCanvasView.drawingPolicy
for Pencil-primary apps).default -
stored as a property, not recreated each appearancePKToolPicker -
called to show the tool pickercanvasView.becomeFirstResponder() - Drawing serialized via
and loaded viadataRepresentation()PKDrawing(data:) -
delegate method used to track changescanvasViewDrawingDidChange -
set if backward compatibility neededmaximumSupportedContentVersion - Exported images use appropriate scale factor for the device
- SwiftUI wrapper avoids infinite update loops by checking
drawing != binding - Tool picker observer added before becoming first responder
- Drawing bounds checked before image export (empty drawings have
bounds).zero
References
- Extended PencilKit patterns (advanced strokes, Scribble, delegate): references/pencilkit-patterns.md
- PencilKit framework
- PKCanvasView
- PKDrawing
- PKToolPicker
- PKInkingTool
- PKStroke
- Drawing with PencilKit
- Configuring the PencilKit tool picker