skills/pencilkit/SKILL.md
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.
npx skillsauth add dpearson2699/swift-ios-skills pencilkitInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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+.
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 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
}
}
| Policy | Behavior |
|---|---|
| .default | Respects UIPencilInteraction.prefersPencilOnlyDrawing when the tool picker is visible; otherwise Pencil-only |
| .anyInput | Both pencil and finger draw |
| .pencilOnly | Only Apple Pencil touches draw on the canvas |
canvasView.drawingPolicy = .pencilOnly
Use .default for system-standard Pencil-primary canvases when the tool
picker's drawing-policy control should follow the user's Pencil preference. Use
.anyInput for signature pads, whiteboards, or explicit finger-drawing modes.
Use .pencilOnly when finger input should never create strokes.
// 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 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.addObserver(canvasView)
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()
}
}
Create a tool picker with specific tools. PKToolPicker(toolItems:) and
custom tool picker item classes require iOS/iPadOS 18+, Mac Catalyst 18+, and
visionOS 2+; those item classes are available on macOS starting in macOS 26.
let toolPicker = PKToolPicker(toolItems: [
PKToolPickerInkingItem(type: .pen, color: .black, width: 5),
PKToolPickerInkingItem(type: .pencil, color: .gray, width: 5),
PKToolPickerInkingItem(type: .marker, color: .yellow, width: 12),
PKToolPickerEraserItem(type: .vector),
PKToolPickerLassoItem(),
PKToolPickerRulerItem()
])
| Type | Description |
|---|---|
| .pen | Smooth, pressure-sensitive pen |
| .pencil | Textured pencil with tilt shading |
| .marker | Semi-transparent highlighter |
| .monoline | Uniform-width pen |
| .fountainPen | Variable-width calligraphy pen |
| .watercolor | Blendable watercolor brush |
| .crayon | Textured crayon |
| .reed | Reed pen (iOS/iPadOS/macOS/visionOS 26+) |
When drawings sync to older OS versions, check requiredContentVersion before
uploading or cap new content by setting maximumSupportedContentVersion on
both the PKCanvasView and PKToolPicker.
| Version | Content |
|---|---|
| .version1 | iPadOS 14-era inks: marker, pen, pencil |
| .version2 | iPadOS 17 inks: monoline, fountain pen, watercolor, crayon |
| .version3 | Barrel-roll angle data |
| .version4 | Reed pen |
In compatibility reviews, state the complete version map before recommending a
cap. If the plan exposes a curated picker or specific ink choices, also mention
the availability of PKToolPicker(toolItems:) and custom picker item APIs.
When existing content exceeds the target OS version, sync a verified fallback
PKDrawing or restrict editing up front; do not rely only on a warning.
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)
}
When loading synced or user-provided drawings, handle decode failures explicitly
instead of suppressing them with try?:
do {
canvasView.drawing = try PKDrawing(data: data)
} catch {
showReadOnlyPreview(for: document, loadError: error)
}
var drawing1 = PKDrawing()
let drawing2 = PKDrawing()
drawing1.append(drawing2)
// Non-mutating
let combined = drawing1.appending(drawing2)
let scaled = drawing.transformed(using: CGAffineTransform(scaleX: 2, y: 2))
let translated = drawing.transformed(using: CGAffineTransform(translationX: 100, y: 0))
For sync, migration, downgrade, or cross-device editing tasks, use
requiredContentVersion as the compatibility gate and choose an explicit
maximumSupportedContentVersion when old clients must keep editing.
let targetVersion: PKContentVersion = .version1
canvasView.maximumSupportedContentVersion = targetVersion
toolPicker.maximumSupportedContentVersion = targetVersion
switch drawing.requiredContentVersion {
case .version1:
// Older marker, pen, and pencil ink set
syncEditable(drawing)
case .version2:
// iPadOS 17-era inks: monoline, fountain pen, watercolor, crayon
syncIfRecipientsSupportVersion2(drawing)
case .version3, .version4:
// Later features such as barrel-roll data and Reed Pen
syncEditableOnlyToCurrentClients(drawing)
@unknown default:
showReadOnlyPreview(for: drawing)
}
If a drawing requires a newer version than a recipient can load, preserve the
full-fidelity PKDrawing for capable clients and provide a read-only preview or
separate fallback instead of silently overwriting it. See
references/pencilkit-patterns.md for the
deeper compatibility table.
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)
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)")
}
}
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])
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
context.coordinator.toolPicker.addObserver(canvas)
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
}
}
}
For SwiftUI wrappers, call out the input-policy choice in the wrapper guidance.
Use .anyInput when finger drawing is part of the product. Use .pencilOnly
when touch should stay reserved for scrolling or selection. Use .default when
you want PencilKit's system behavior: with the tool picker visible, it follows
the user's Pencil-only drawing setting; otherwise only Apple Pencil draws.
struct DrawingScreen: View {
@State private var drawing = PKDrawing()
@State private var showToolPicker = true
var body: some View {
CanvasView(drawing: $drawing, toolPickerVisible: $showToolPicker)
.ignoresSafeArea()
}
}
PaperKit (iOS 26+) extends PencilKit with a complete markup experience
including shapes, text boxes, images, stickers, and loupes. Use the sibling
paperkit skill when you need structured markup rather than only freeform
drawing.
| Capability | PencilKit | PaperKit |
|---|---|---|
| Freeform drawing | Yes | Yes |
| Shapes & lines | No | Yes |
| Text boxes | No | Yes |
| Images & stickers | No | Yes |
| Loupes | No | Yes |
| Markup toolbar | No | Yes |
| Markup insertion UI | No | MarkupEditViewController, MarkupToolbarViewController |
| Data model | PKDrawing | PaperMarkup |
PaperKit uses PencilKit under the hood: PaperMarkupViewController accepts
PKTool for its drawingTool property, and PaperMarkup can append a
PKDrawing.
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()
.default drawing policyWhen explaining input behavior, .default is system-setting aware. If the tool
picker is visible, it respects the user's Pencil-only drawing preference;
otherwise only Apple Pencil draws.
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()
Earlier OS versions throw when loading PKDrawing data that uses unsupported
inks. Check requiredContentVersion before syncing, or set
maximumSupportedContentVersion on both the canvas and tool picker to restrict
new content.
// WRONG: only limits the canvas; picker can still expose newer inks
canvasView.tool = PKInkingTool(.watercolor, color: .blue)
canvasView.maximumSupportedContentVersion = .version1
// CORRECT: limit both surfaces for iPadOS 14-era ink compatibility
if #available(iOS 17.0, *) {
canvasView.maximumSupportedContentVersion = .version1
toolPicker.maximumSupportedContentVersion = .version1
}
dataRepresentation() is for persistence and interchange, not comparison.
Use PKDrawing equality for exact value checks, and inspect strokes or rendered
images for visual/approximate comparisons.
// WRONG
if drawing1.dataRepresentation() == drawing2.dataRepresentation() { }
// CORRECT
if drawing1 == drawing2 { }
PKCanvasView.drawingPolicy set appropriately and .default explained as system-setting awarePKToolPicker stored as a property, not recreated each appearancecanvasView.becomeFirstResponder() called to show the tool pickerPKToolPicker observer before showing the pickerdataRepresentation() and loaded via PKDrawing(data:)canvasViewDrawingDidChange delegate method used to track changesmaximumSupportedContentVersion set on both canvas and tool picker if backward compatibility is neededdrawing != binding.zero bounds)development
Implement, review, or improve data visualizations using Swift Charts. Use when building bar, line, area, point, pie, donut, or iOS 26 3D charts; when adding chart selection, scrolling, annotations, axes, scales, legends, or foregroundStyle grouping; when plotting functions with BarPlot, LinePlot, AreaPlot, PointPlot, Chart3D, or SurfacePlot; or when creating heat maps, Gantt charts, grouped bars, sparklines, threshold lines, or spatial visualizations.
data-ai
Select, implement, or migrate between app architecture patterns for Apple platform apps. Use when choosing between MV (Model-View with @Observable), MVVM, MVI, TCA (The Composable Architecture), Clean Architecture, VIPER, or Coordinator patterns; when evaluating architecture fit for a feature's complexity; when migrating from one pattern to another; or when reviewing whether an app's current architecture is appropriate. Scoped to Apple-platform patterns using Swift 6.3, SwiftUI, and UIKit.
development
Apply Swift API Design Guidelines to name, label, and document Swift APIs. Covers argument label rules (prepositional phrase rule, grammatical phrase rule, first-label omission), mutating/nonmutating pair naming (-ed/-ing participle pattern, form- prefix, sort/sorted, formUnion/union), side-effect naming (noun for pure, verb for mutating), documentation comment structure (summary by declaration kind, O(1) complexity rule), clarity at call site, role-based naming, protocol naming (-able/-ible/-ing), default arguments over method families, casing conventions, and terminology. Use when designing new Swift APIs, reviewing naming and argument labels, writing documentation comments, or refactoring for call site clarity.
development
Implement, review, or improve in-app purchases and subscriptions using StoreKit 2. Use when building paywalls with SubscriptionStoreView or ProductView, processing transactions with Product and Transaction APIs, verifying entitlements, handling purchase flows (consumable, non-consumable, auto-renewable), implementing offer codes or promotional/win-back/introductory offers, managing subscription status and renewal state, setting up StoreKit testing with configuration files, or integrating Family Sharing, Ask to Buy, refund handling, and billing retry logic.