SwiftUI PencilKit in iOS 26: Apple Pencil, PKCanvasView, and Custom Drawing Tools
Embed a low-latency drawing canvas in SwiftUI using PencilKit in iOS 26. Wrap PKCanvasView, wire up PKToolPicker, persist PKDrawing to SwiftData, and handle Apple Pencil Pro squeeze and hover with production-ready Swift code.
SwiftUI PencilKit is Apple's built-in drawing framework that lets you embed a fully-featured, low-latency handwriting and sketching canvas in a SwiftUI app by wrapping PKCanvasView in a UIViewRepresentable. In iOS 26 it ships with native Apple Pencil Pro support (barrel roll, squeeze, and hover), plus a redesigned PKToolPicker that adopts the Liquid Glass palette. I've shipped three note-taking apps on top of PencilKit, and honestly? The API is more approachable than the Stack Overflow snippets make it look. So, let's walk through it end-to-end.
PencilKit ships two SwiftUI-adjacent primitives: PKCanvasView (the ink surface) and PKToolPicker (the floating palette). Both are UIKit and need a UIViewRepresentable bridge.
iOS 26 adds first-class Apple Pencil Pro support through the PKPencilInteraction squeeze action and the hoverPose callback for barrel-roll and hover previews.
A PKDrawing is a Codable value type. Persist it as Data via drawing.dataRepresentation(), not as an image.
Export happens through PKDrawing.image(from:scale:) for UIImage, or by pairing it with ImageRenderer for PDF and share-sheet flows.
Use PencilKit when users draw. Use SwiftUI's Canvas when your app draws. They solve different problems and shouldn't be swapped.
VoiceOver users can operate PencilKit if you call accessibilityLabel on the canvas and expose stroke count for the rotor.
What is PencilKit in SwiftUI?
PencilKit is the framework Apple introduced in iOS 13 to give third-party apps the same drawing engine that powers Notes, Freeform, and Markup. It renders vector strokes on a Metal-backed view with sub-frame latency on iPad, exposes a shared PKToolPicker, and stores drawings as a compact, resolution-independent PKDrawing. Because the whole framework predates SwiftUI, it lives in UIKit. Wrapping it is a two-file job, though, and it plays nicely with observation once you plumb the delegate through a coordinator.
In iOS 26, the framework picked up three big additions. The tool picker adopted the Liquid Glass material and can now dock to the leading or trailing edge on iPad. PKPencilInteraction exposes the Apple Pencil Pro squeeze gesture as a first-class event. And PKCanvasView gained a hoverPose callback that surfaces roll and azimuth without a private-API dance. If you last touched PencilKit under iOS 17, this stuff is worth a fresh look.
One clarification up front: PencilKit is not a general 2D drawing engine. It's an ink surface. Humans (or the Apple Pencil) draw on it, strokes get stored as bezier paths, and the framework handles smoothing, palm rejection, and the tool palette. If you want to programmatically render shapes and animations, that's SwiftUI Canvas and TimelineView, not PencilKit.
Wrapping PKCanvasView with UIViewRepresentable
SwiftUI can't host PKCanvasView directly, so we bridge it. It's the same pattern you'd use for any UIKit view (I covered the general mechanics in UIViewRepresentable and UIViewControllerRepresentable), but PencilKit has one wrinkle. The canvas expects a delegate for stroke-change callbacks, and you'll almost always want to bind the drawing back to SwiftUI state.
Here's the smallest useful wrapper. It gives the parent a @Binding<PKDrawing>, forwards edits, and exposes a knob for the tool picker's ruler.
import SwiftUI
import PencilKit
struct DrawingCanvas: UIViewRepresentable {
@Binding var drawing: PKDrawing
var showRuler: Bool = false
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.drawing = drawing
canvas.drawingPolicy = .anyInput // accept finger + pencil in the Simulator
canvas.isRulerActive = showRuler
canvas.delegate = context.coordinator
canvas.backgroundColor = .systemBackground
return canvas
}
func updateUIView(_ canvas: PKCanvasView, context: Context) {
// Only push external changes to avoid a delegate feedback loop.
if canvas.drawing != drawing {
canvas.drawing = drawing
}
canvas.isRulerActive = showRuler
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
final class Coordinator: NSObject, PKCanvasViewDelegate {
let parent: DrawingCanvas
init(_ parent: DrawingCanvas) { self.parent = parent }
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
parent.drawing = canvasView.drawing
}
}
}
Two things trip people up here. First, drawingPolicy defaults to .default, which on iPad only accepts Apple Pencil input. Great for shipping apps, terrible in the Simulator. Use .anyInput during development so you can draw with the mouse. Second, that equality check inside updateUIView isn't cosmetic. Without it, SwiftUI will reassign the drawing on every state change, which nukes the current stroke mid-gesture and produces the classic "my line disappears when I lift the pencil" bug. (Yes, I hit that one shipping v1 of my first sketch app. Not fun.)
Adding the PKToolPicker for a full drawing UI
An ink canvas without a tool picker is just a black-pen input field. PKToolPicker is the floating palette users know from Notes: pen, marker, pencil, highlighter, eraser, lasso, ruler, and a color grid. In iOS 26, Apple removed the old shared-per-window API and replaced it with a per-canvas instance you attach with toolPicker(_:isVisible:) on the canvas view.
Add the picker to our wrapper by keeping it in the coordinator and toggling visibility when the canvas becomes the first responder:
final class Coordinator: NSObject, PKCanvasViewDelegate {
let parent: DrawingCanvas
let toolPicker = PKToolPicker()
init(_ parent: DrawingCanvas) { self.parent = parent }
func canvasViewDidBeginUsingTool(_ canvasView: PKCanvasView) {
// Optional: log analytics, dim other UI, etc.
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
parent.drawing = canvasView.drawing
}
func attachToolPicker(to canvas: PKCanvasView) {
toolPicker.setVisible(true, forFirstResponder: canvas)
toolPicker.addObserver(canvas)
canvas.becomeFirstResponder()
}
}
Then call context.coordinator.attachToolPicker(to: canvas) at the end of makeUIView. On iPad the picker floats and can be repositioned; on iPhone it docks to the bottom. The Liquid Glass palette rendering is automatic (no opt-in required), but I'd recommend testing your canvas background against it, because a fully white canvas plus a translucent picker eats into the drawing area more than you'd expect.
How do you save and reload a PKDrawing?
Don't save the drawing as an image. A PKDrawing is a value type backed by an efficient binary format, and round-tripping through PNG throws away pressure, tilt, and the resolution-independent vector data. The correct pattern is drawing.dataRepresentation() for encoding and PKDrawing(data:) for decoding, storing the raw Data in SwiftData, Core Data, or a file.
Here's a SwiftData model plus the canvas usage side. If SwiftData is new to you, I walked through the migration story in a previous post on SwiftData from zero to production.
import SwiftData
import PencilKit
@Model
final class Sketch {
var title: String
var createdAt: Date
var drawingData: Data
init(title: String, drawing: PKDrawing = PKDrawing()) {
self.title = title
self.createdAt = .now
self.drawingData = drawing.dataRepresentation()
}
var drawing: PKDrawing {
get { (try? PKDrawing(data: drawingData)) ?? PKDrawing() }
set { drawingData = newValue.dataRepresentation() }
}
}
struct SketchEditor: View {
@Bindable var sketch: Sketch
var body: some View {
DrawingCanvas(drawing: Binding(
get: { sketch.drawing },
set: { sketch.drawing = $0 }
))
.navigationTitle(sketch.title)
}
}
A blank drawing weighs about 40 bytes; a full page of dense notes is typically 30–80 KB. That's small enough to store inline in SwiftData without the external storage attribute. If you'd rather write to disk, drop the data into the app's documents directory and store only the URL. Handy when you want iCloud Documents sync without CloudKit.
Apple Pencil Pro features in iOS 26: squeeze, roll, and hover
Apple Pencil Pro shipped with three interactions the earlier Pencils don't have: a barrel squeeze, gyroscope-based roll (rotate the pencil to change the brush angle), and a haptic engine that can respond to system events. Under iOS 26, PencilKit surfaces all three natively, so you no longer have to reach into UIHoverGestureRecognizer or private APIs.
Squeeze is the interesting one. It's effectively a right-click for the pencil, and users expect it to open a tool palette by default. Register for it through the modern PKPencilInteraction:
import UIKit
import PencilKit
final class SketchViewController: UIViewController, PKPencilInteractionDelegate {
let interaction = PKPencilInteraction()
override func viewDidLoad() {
super.viewDidLoad()
interaction.delegate = self
view.addInteraction(interaction)
}
func pencilInteractionDidTap(_ interaction: PKPencilInteraction) {
// Double-tap on Pencil 2 and squeeze on Pencil Pro
// both call this delegate on iOS 26.
toggleEraser()
}
func toggleEraser() { /* swap the active tool */ }
}
For barrel roll, keep an eye on the canvas's hoverPose publisher. It emits a PKHoverPose whenever the pencil is within roughly 12 mm of the screen, exposing azimuth, altitude, and roll. A flat-tip brush that rotates as the user turns the pencil is a five-line change once you subscribe. The PencilKit documentation has the full list of pose properties, and Apple's WWDC session on Apple Pencil Pro is still the clearest walk-through of the hardware capabilities.
How to export a PKDrawing to UIImage or PDF
Exporting is where PencilKit's separation between "drawing" and "rendering" pays off. The framework hands you a rasterizer that respects the canvas transform, and it uses Metal internally, so it's fast enough to run on the main thread for reasonable sizes.
For PDF export you have two paths. If you only need the drawing itself, wrap UIGraphicsPDFRenderer around the same image(from:scale:) call. If you need the drawing composited with SwiftUI chrome (a title bar, a signature line, a header logo), reach for SwiftUI ImageRenderer, which renders any SwiftUI view (including one that hosts your DrawingCanvas) into a CGContext.
import PDFKit
func exportPDF(from drawing: PKDrawing, pageSize: CGSize) -> Data {
let renderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: pageSize))
return renderer.pdfData { context in
context.beginPage()
let image = drawing.image(from: CGRect(origin: .zero, size: pageSize), scale: 2)
image.draw(in: context.pdfContextBounds)
}
}
Two gotchas. An empty PKDrawing has an empty bounds, which turns into a zero-size image, so always guard against it. And PencilKit renders on a transparent background by default. If you're printing on white paper, fill the rect first or your exported PDF will look ghostly.
PencilKit vs SwiftUI Canvas: which should you use?
This trips up people about once a week in the Swift forums, so let's settle it with a comparison. Both draw pixels. That's where the similarity ends.
Dimension
PencilKit
SwiftUI Canvas
Input source
User (finger, Apple Pencil)
Your code
Storage format
Vector strokes (PKDrawing)
Nothing; recomputed every frame
Built-in palette
Yes, via PKToolPicker
No; bring your own UI
Palm rejection
Yes, tuned for Apple Pencil
None
Animatable
No; static ink
Yes, via TimelineView
Accessibility rotor
Yes, per-stroke
Manual
Best for
Notes, signatures, sketches, markup
Charts, custom controls, games
The shortcut heuristic: if the pixels come from a human's hand, use PencilKit. If they come from a data structure, use Canvas. Apps like Freeform and Procreate combine both: a Canvas layer for programmatic backgrounds and a PKCanvasView on top for freehand ink. There's no conflict, just be careful with hit-testing when you stack them. If you're leaning heavily on the SwiftUI side, my earlier piece on mastering Liquid Glass in SwiftUI covers how the two layers should feel together visually.
Making PencilKit accessible with VoiceOver
PencilKit gets a lot of accessibility for free. The tool picker is a proper VoiceOver group, buttons announce their tool names, and the color grid works with the rotor. What you have to add is context around the canvas itself. A screen without a description reads as "canvas view" and that's not enough.
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.accessibilityLabel = "Sketch canvas"
canvas.accessibilityHint = "Draw with your finger or Apple Pencil."
canvas.accessibilityValue = "\(canvas.drawing.strokes.count) strokes"
canvas.accessibilityTraits.insert(.allowsDirectInteraction)
return canvas
}
The allowsDirectInteraction trait tells VoiceOver to pass touch events through instead of intercepting them, which lets a VoiceOver user actually draw. Without it, every gesture is captured as swipe navigation, which is the single biggest complaint I see on drawing-app App Store reviews. Update accessibilityValue from the delegate whenever the stroke count changes, so users hear "5 strokes" when they lift the pencil. For deeper coverage I'd point you at my SwiftUI accessibility guide. The Dynamic Type section applies to your surrounding chrome even though the canvas itself scales visually.
Common PencilKit errors and how to fix them
A short field guide of the ones I hit while writing this piece.
"The tool picker doesn't appear"
Almost always because the canvas never became first responder. Call canvas.becomeFirstResponder() after the view is in the window hierarchy, not inside makeUIView. A dispatch to the main queue in updateUIView is the safest place if you're not sure.
"My drawing snaps back after every stroke"
The equality check in updateUIView is missing or wrong. SwiftUI re-invalidates the representable whenever any observed state changes, and if you unconditionally assign canvas.drawing = drawing, you'll clobber the in-flight stroke. Always compare first.
"Simulator won't let me draw"
Set drawingPolicy = .anyInput. The default is .default, which requires a real Apple Pencil, which the Simulator doesn't have.
"Squeeze doesn't fire on Apple Pencil Pro"
Check that the user hasn't disabled squeeze in Settings → Apple Pencil, and that you added the PKPencilInteraction to a view that's currently in the responder chain. A disabled squeeze returns UIPencilPreferredAction.ignore, which the delegate silently swallows.
"Export image is blurry"
You're passing scale: 1. Use UIScreen.main.scale or a fixed 2 or 3 for retina displays. The default scale on image(from:scale:) is the main screen's, but if you're rendering off-screen for share flows it defaults to 1.
Frequently Asked Questions
Is PencilKit free to use in commercial apps?
Yes. PencilKit is part of the iOS SDK and has no licensing fees or usage restrictions beyond the standard Apple Developer Program terms. You can ship it in free and paid apps, including subscription products, without royalties.
Does PencilKit work on iPhone or only iPad?
PencilKit works on iPhone, iPad, and Mac (through Mac Catalyst and native macOS). On iPhone, users draw with a finger since Apple Pencil is iPad-only; the tool picker docks to the bottom of the screen instead of floating.
Can I customize the PencilKit tool picker's brushes?
Not really. The built-in tool set (pen, marker, pencil, highlighter, monoline, fountain pen, watercolor, crayon, eraser, lasso, ruler) is fixed. You can hide the picker and build your own UI that sets canvas.tool to a custom PKInkingTool with any color and width you want.
How do I convert a PencilKit drawing to an SVG?
PencilKit doesn't ship an SVG exporter, but PKDrawing.strokes exposes each stroke's PKStrokePath as a collection of control points. Walk those points and emit an SVG <path> element per stroke; a working converter is typically about 60 lines of Swift.
What's the difference between PKCanvasView and Freeform's canvas?
Freeform is Apple's whiteboard app built on top of PencilKit plus a custom layout engine for shapes, sticky notes, and connectors. The ink layer is PKCanvasView; everything else (the infinite pan, the shape recognition, the object model) is Apple's proprietary code, not part of the public framework.
A practical guide to Apple's unified logging in Swift for iOS 26: how Logger replaces print, when to use each log level, how privacy annotations protect PII, and how OSLogStore and OSSignposter power in-app diagnostics and performance tracing.
SwiftUI's PhaseAnimator walks a view through ordered animation phases from a single trigger, replacing @State flags and asyncAfter chains. Learn per-phase curves, trigger patterns, and how to combine it with CustomAnimation, plus real-world code for like buttons and toasts.
Profile SwiftUI apps with Xcode Instruments in Xcode 26. A real-device workflow for finding hangs, frame drops, and memory leaks, plus custom signposts with OSSignposter.