Skill v1.0.1
currentAutomated scan100/100+3 new
version: "1.0.1" 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. Targets Swift 6.3 / iOS 26+.
Contents
- Setup
- 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+.
PKCanvasView Basics
PKCanvasView is a UIScrollView subclass that captures Apple Pencil and finger input and renders strokes.
import PencilKitimport UIKitclass DrawingViewController: UIViewController, PKCanvasViewDelegate {let canvasView = PKCanvasView()override func viewDidLoad() {super.viewDidLoad()canvasView.delegate = selfcanvasView.drawingPolicy = .anyInputcanvasView.tool = PKInkingTool(.pen, color: .black, width: 5)canvasView.frame = view.boundscanvasView.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 rulercanvasView.isRulerActive = true// Set the current tool programmaticallycanvasView.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
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 Serialization
PKDrawing is a value type (struct) that holds all stroke data. Serialize it to Data for persistence.
// Savefunc saveDrawing(_ drawing: PKDrawing) throws {let data = drawing.dataRepresentation()try data.write(to: fileURL)}// Loadfunc 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)}
Combining Drawings
var drawing1 = PKDrawing()let drawing2 = PKDrawing()drawing1.append(drawing2)// Non-mutatinglet 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 = .version1canvasView.maximumSupportedContentVersion = targetVersiontoolPicker.maximumSupportedContentVersion = targetVersionswitch drawing.requiredContentVersion {case .version1:// Older marker, pen, and pencil ink setsyncEditable(drawing)case .version2:// iPadOS 17-era inks: monoline, fountain pen, watercolor, crayonsyncIfRecipientsSupportVersion2(drawing)case .version3, .version4:// Later features such as barrel-roll data and Reed PensyncEditableOnlyToCurrentClients(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 regionlet region = CGRect(x: 0, y: 0, width: 500, height: 500)let scale = UITraitCollection.current.displayScalelet 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.inkprint("Ink type: \(ink.inkType), color: \(ink.color)")print("Bounds: \(stroke.renderBounds)")// Access path pointslet path = stroke.pathprint("Points: \(path.count), created: \(path.creationDate)")// Interpolate along the pathfor 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 SwiftUIimport PencilKitstruct CanvasView: UIViewRepresentable {@Binding var drawing: PKDrawing@Binding var toolPickerVisible: Boolfunc makeUIView(context: Context) -> PKCanvasView {let canvas = PKCanvasView()canvas.delegate = context.coordinatorcanvas.drawingPolicy = .anyInputcanvas.drawing = drawingcontext.coordinator.toolPicker.addObserver(canvas)return canvas}func updateUIView(_ canvas: PKCanvasView, context: Context) {if canvas.drawing != drawing {canvas.drawing = drawing}let toolPicker = context.coordinator.toolPickertoolPicker.setVisible(toolPickerVisible, forFirstResponder: canvas)if toolPickerVisible { canvas.becomeFirstResponder() }}func makeCoordinator() -> Coordinator { Coordinator(self) }class Coordinator: NSObject, PKCanvasViewDelegate {let parent: CanvasViewlet toolPicker = PKToolPicker()init(_ parent: CanvasView) {self.parent = parentsuper.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.
Usage in SwiftUI
struct DrawingScreen: View {@State private var drawing = PKDrawing()@State private var showToolPicker = truevar 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 showstoolPicker.setVisible(true, forFirstResponder: canvasView)// CORRECT: Also become first respondertoolPicker.setVisible(true, forFirstResponder: canvasView)canvasView.becomeFirstResponder()
DON'T: Oversimplify .default drawing policy
When 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.
DON'T: Create multiple tool pickers for the same canvas
One PKToolPicker per canvas. Creating extras causes visual conflicts.
// WRONGfunc viewDidAppear(_ animated: Bool) {let picker = PKToolPicker() // New picker every appearancepicker.setVisible(true, forFirstResponder: canvasView)}// CORRECT: Store picker as a propertylet toolPicker = PKToolPicker()
DON'T: Ignore content versions for backward compatibility
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 inkscanvasView.tool = PKInkingTool(.watercolor, color: .blue)canvasView.maximumSupportedContentVersion = .version1// CORRECT: limit both surfaces for iPadOS 14-era ink compatibilityif #available(iOS 17.0, *) {canvasView.maximumSupportedContentVersion = .version1toolPicker.maximumSupportedContentVersion = .version1}
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.
// WRONGif drawing1.dataRepresentation() == drawing2.dataRepresentation() { }// CORRECTif drawing1 == drawing2 { }
Review Checklist
- [ ]
PKCanvasView.drawingPolicyset appropriately and.defaultexplained as system-setting aware - [ ]
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