pencilkit
dpearson2699/swift-ios-skills
Add Apple Pencil drawing, tool picker, and serialization to iOS/iPadOS apps.
What is pencilkit?
PencilKit provides PKCanvasView for capturing Apple Pencil and finger input, PKToolPicker for tool selection, and PKDrawing for serializing and exporting drawings. Use it when building drawing apps, annotation features, handwriting capture, signature fields, or any Apple Pencil-powered experience on iOS 13+, iPadOS 13+, or visionOS 1.0+.
- Capture Apple Pencil and finger strokes with PKCanvasView and configurable drawing policies
- Manage drawing tools (pen, pencil, marker, eraser, lasso, ruler) via PKToolPicker
- Serialize drawings to Data and deserialize with validation/fix/retry loops
- Export drawings to images with specified scale and bounds validation
- Inspect individual strokes and transform drawings with affine transforms
- Handle content version compatibility across iOS/iPadOS/visionOS versions
How to install pencilkit
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill pencilkit- iOS 13+, iPadOS 13+, Mac Catalyst 13.1+, or visionOS 1.0+
- No special entitlements or Info.plist entries required
- Import PencilKit framework
How to use pencilkit
- 1.Import PencilKit and create a PKCanvasView in your view controller
- 2.Set the canvas delegate and configure drawing policy (.default, .anyInput, or .pencilOnly)
- 3.Create and configure a PKToolPicker, add the canvas as observer, and make it visible
- 4.Read drawing changes via canvasViewDrawingDidChange(_:) delegate method
- 5.Serialize drawings with dataRepresentation() and deserialize with PKDrawing(data:) inside a validate/fix/retry loop
- 6.For synced data, check requiredContentVersion and set maximumSupportedContentVersion for compatibility
- 7.Export to image using image(from:scale:) after validating bounds
Use cases
- Building a note-taking or sketching app with Apple Pencil support
- Adding annotation or markup features to documents or images
- Creating signature capture fields for forms or contracts
- Implementing a whiteboard or collaborative drawing canvas
- Syncing drawings across devices while maintaining version compatibility
- iOS/iPadOS app developers building drawing or annotation features
- Developers creating productivity or creative apps with Pencil support
- Teams building cross-device drawing experiences with version management needs
pencilkit FAQ
.default respects the user's Pencil preference when the tool picker is visible; .anyInput allows both pencil and finger; .pencilOnly restricts to Apple Pencil only.
Use a validate/fix/retry loop: catch the error, preserve the original bytes, refetch an intact revision or select a compatible fallback, then retry deserialization. Show a read-only preview if recovery fails rather than silently suppressing the error.
Use requiredContentVersion to check which iOS/iPadOS version a drawing requires, and set maximumSupportedContentVersion on the canvas and tool picker to gate editable sync. This ensures older clients can still edit drawings without losing fidelity.
Yes. Use append(_:) to mutate a drawing or appending(_:) for a non-mutating version. Use transformed(using:) with CGAffineTransform to scale, translate, or rotate drawings.
pen, pencil, marker, monoline, fountainPen, watercolor, crayon, and reed (iOS 26+). Each has different visual characteristics and pressure sensitivity.
Full instructions (SKILL.md)
Source of truth, from dpearson2699/swift-ios-skills.
name: pencilkit description: "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."
PencilKit
Capture Apple Pencil and finger input using PKCanvasView, manage drawing
tools with PKToolPicker, serialize drawings with PKDrawing, and wrap PencilKit in SwiftUI.
Contents
- Setup
- Capture-to-Export Workflow
- PKCanvasView Basics
- PKToolPicker
- PKDrawing Serialization
- Content Version Compatibility
- 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+.
Capture-to-Export Workflow
- Capture: Read
canvasView.drawingfromcanvasViewDrawingDidChange(_:); keep the previous persisted revision until the new revision completes the remaining checkpoints. - Serialize: Create
dataRepresentation(), write atomically, and run the decode validate/fix/retry loop. Do not mark bytes valid whenPKDrawing(data:)still throws. - Version-gate: Apply Content Version Compatibility before editable sync. If the recipient cannot load the drawing, preserve the full-fidelity source and use an existing compatible fallback or read-only preview.
- Sync: Send only validated, compatible data and mark the revision synced after acknowledgement. On transport or conflict failure, retain the pending revision, resolve the cause, and retry without discarding the last good copy.
- Export: Validate a nonempty drawing region and intended scale before
calling
image(from:scale:); skip export on invalid bounds without altering the serialized drawing.
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 |
|---|---|
.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.
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.addObserver(canvasView)
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()
}
}
Custom Tool Picker Items
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()
])
Ink Types
| 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+) |
Content Versions
Use Content Version Compatibility as the single version map and compatibility gate for both the canvas and tool picker.
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, options: .atomic)
}
// Load
func loadDrawing() throws -> PKDrawing {
let data = try Data(contentsOf: fileURL)
return try PKDrawing(data: data)
}
Decode Validate/Fix/Retry Loop
For synced or user-provided data: validate with PKDrawing(data:); on
failure preserve the original bytes and fix the cause by refetching an
intact revision or selecting a previously generated compatible copy; then
retry the decode. Assign the drawing only after a successful retry. If
recovery still fails, keep the source unchanged and show an error or available
read-only preview instead of suppressing the failure with try?.
do {
canvasView.drawing = try PKDrawing(data: correctedData) // retry
} catch {
showReadOnlyPreview(for: document, loadError: error)
}
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))
Content Version Compatibility
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.
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
Load Constructing Strokes Programmatically only for generated ink paths; ordinary drawing and inspection do not need the advanced constructors.
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
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, set the input policy using the canonical Drawing Policies table.
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 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.
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 versions for backward compatibility
Apply the Content Version Compatibility gate to both the canvas and tool picker before syncing editable drawings.
DON'T: Compare drawings by data representation
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 { }
Review Checklist
-
PKCanvasView.drawingPolicyfollows the canonical policy table -
PKToolPickerstored as a property, not recreated each appearance -
canvasView.becomeFirstResponder()called to show the tool picker - Canvas added as a
PKToolPickerobserver before showing the picker - Drawing serialized via
dataRepresentation()and loaded viaPKDrawing(data:) -
canvasViewDrawingDidChangedelegate method used to track changes -
maximumSupportedContentVersionset on both canvas and tool picker if backward compatibility is needed - Custom tool picker item code guarded for iOS/iPadOS 18+ and visionOS 2+
- Exported images use appropriate scale factor for the device
- SwiftUI wrapper avoids infinite update loops by checking
drawing != binding - Drawing bounds checked before image export (empty drawings have
.zerobounds)
References
- Extended PencilKit patterns (advanced strokes, content versions, delegates): references/pencilkit-patterns.md
- PencilKit framework
- PKCanvasView
- PKDrawing
- PKToolPicker
- PKInkingTool
- PKStroke
- Drawing with PencilKit
- Configuring the PencilKit tool picker
Related skills
More from dpearson2699/swift-ios-skills and the wider catalog.

permissionkit
Request parental permission for child communication in iOS apps via iMessage.

photokit
Photo picking, camera capture, and media handling for iOS apps with PhotoKit and AVFoundation.

push-notifications
Implement and debug local and remote push notifications in iOS/macOS apps with APNs, rich content, and actions.

realitykit
Build iOS AR and 3D experiences with RealityKit and ARKit.

relevancekit
Increase Apple Watch widget visibility using contextual relevance signals for time, location, fitness, and hardware.

scenekit
Maintain existing SceneKit 3D scenes and visualizations in Swift iOS apps.