civil-and-structural-engineering
Building a Custom Drawing Canvas with Pencilkit for Ios
Table of Contents
Introduction to PencilKit
PencilKit, introduced by Apple in iOS 13, provides a high-level framework for integrating drawing, sketching, and handwriting features into iOS apps. It abstracts away the complexity of handling touch input, stroke rendering, and tool management, allowing developers to focus on crafting intuitive user experiences. Whether you’re building a note-taking app, a digital art tool, or an annotation system, PencilKit delivers a polished drawing canvas with minimal boilerplate. This article walks through creating a custom drawing canvas using PencilKit, covering setup, configuration, saving and loading drawings, undo support, and advanced customization.
Setting Up Your Project for PencilKit
Target Requirements and Framework Import
PencilKit is available from iOS 13.0 onward. Ensure your Xcode project targets at least iOS 13. Add the PencilKit framework via the “Frameworks, Libraries, and Embedded Content” section in your target settings, or by importing it in code:
import PencilKit
For apps supporting iPad and Apple Pencil, also enable the “Apple Pencil” entitlement in your Info.plist (key: UIRequiredDeviceCapabilities includes apple-pencil) to ensure proper behaviour and user expectations. If you plan to use PencilKit on iPhone, PencilKit works with touch input as well, but the full Apple Pencil experience is limited to compatible iPads.
Storyboard vs Programmatic Setup
You can add PencilKit either via Interface Builder (drag a PKCanvasView from the Object Library) or programmatically. This guide uses a programmatic approach for clarity and flexibility. In your view controller’s viewDidLoad, create a PKCanvasView instance that fills the view:
override func viewDidLoad() {
super.viewDidLoad()
let canvasView = PKCanvasView(frame: view.bounds)
canvasView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
canvasView.backgroundColor = .white
canvasView.isOpaque = true
view.addSubview(canvasView)
}
Creating the Core Drawing Canvas
Understanding PKCanvasView
PKCanvasView is the primary view that hosts the drawing. It holds a PKDrawing object (the model of all strokes) and manages rendering. Every stroke drawn by the user is automatically recorded and stored in the drawing property. You can subscribe to its delegate methods to track changes, such as canvasViewDrawingDidChange(_:).
Key properties include:
drawing: APKDrawingobject representing the current artwork.tool: The currently active drawing tool (PKInkingTool,PKEraserTool,PKLassoTool).isRulerActive: Boolean to toggle the built‑in ruler.drawingPolicy: Controls whether drawings are accepted, ignored, or only from Apple Pencil.
For a production app, be mindful of memory: very large drawings (many thousands of strokes) can cause performance issues. PencilKit is optimized for typical use cases like note-taking and simple illustrations.
Configuring the Drawing Policy
Decide whether the canvas accepts input from Apple Pencil only, fingers only, or both. Use drawingPolicy:
canvasView.drawingPolicy = .anyInput // accepts both pencil and finger
// or .pencilOnly, .fingerOnly
The default is .anyInput. For a sketching app where users often use fingers, this is fine. For handwriting recognition, you might restrict to pencil.
Integrating the Tool Picker
What is PKToolPicker?
PKToolPicker provides a standard UI for selecting tools, colors, and stroke widths. It automatically adapts to the current tool and shows controls for inking, erasing, lassoing, and line thickness. You display a single tool picker per window; it can be shared across multiple canvases.
Setting Up the Tool Picker
Obtain the shared tool picker for the window and attach it to the PKCanvasView:
guard let window = view.window else { return }
let toolPicker = PKToolPicker.shared(for: window)
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
canvasView.tool = PKInkingTool(.pen, color: .black, width: 5)
The tool picker becomes visible when the canvas is the first responder. It automatically handles showing/hiding based on the responder chain – for instance, it hides when the keyboard appears. You can also listen to tool changes via the observer pattern.
Important: Call canvasView.becomeFirstResponder() after the view appears. In viewWillAppear(_:) you might need to resend this if the view reappears.
Customizing the Initial Tool
Set any PKTool as the default. The PKInkingTool has six pen types: .pen, .pencil, .marker, .crayon, .watercolor, and .fountainPen. Each offers a different stroke feel. Example setting a blue marker with width 10:
canvasView.tool = PKInkingTool(.marker, color: .blue, width: 10)
If you want to force a specific color even when the user picks others from the tool picker, you can override the tool picker’s changes by implementing the PKToolPickerObserver protocol – but in most cases letting users control colors is preferred.
Managing the Drawing Experience
Supporting Undo and Redo
PencilKit does not include built‑in undo manager support for the drawing property. However, you can easily implement undo/redo by saving snapshots of the drawing data before each change. A common pattern is to observe tool changes and drawing completion, then store a copy of canvasView.drawing. Here’s a minimal implementation using UndoManager:
var lastDrawingData: Data?
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
// Undo grouping: based on your app logic, you might register an undo action only after the user lifts the pencil
}
// In your undo action:
@objc func undo() {
let current = canvasView.drawing
undoManager?.registerUndo(withTarget: self) { target in
target.canvasView.drawing = current
}
// Restore previous drawing
if let data = lastDrawingData, let drawing = try? PKDrawing(data: data) {
canvasView.drawing = drawing
}
lastDrawingData = canvasView.drawing.dataRepresentation()
}
For a more robust solution, consider the UndoManager documentation or third‑party libraries.
Detecting Drawing Completion
Use the PKCanvasViewDelegate method canvasViewDidEndUsingTool(_:) to know when a stroke finishes. This is the right moment to save an undo checkpoint.
Saving and Loading Drawings
Persisting Drawings as Data
PencilKit provides a simple serialization: PKDrawing conforms to Codable starting iOS 14, but you can always use dataRepresentation() to get a Data blob. To save to files, use FileManager or Core Data. Example:
func saveDrawing(to url: URL) throws {
let data = canvasView.drawing.dataRepresentation()
try data.write(to: url, options: .atomic)
}
func loadDrawing(from url: URL) throws {
let data = try Data(contentsOf: url)
if let drawing = try? PKDrawing(data: data) {
canvasView.drawing = drawing
} else {
throw NSError(domain: "com.example", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid drawing data"])
}
}
For iOS 14+, you can also use JSONEncoder/Decoder with the Codable conformance.
Thumbnail Previews
To display a small preview of a drawing, use the image(from:scale:) method of PKDrawing. This returns a UIImage rendered at the given rect and scale. Ideal for a gallery view.
Advanced Customization
Creating Custom Tool Palettes
If the default tool picker does not fit your UI, you can hide it and build your own tool selection. Use the tool property directly. You can programmatically change tools without showing the picker:
canvasView.tool = PKInkingTool(.pen, color: .red, width: 3)
canvasView.tool = PKEraserTool(.vector) // or .bitmap
You can also implement a slider for width and color picker. For color, use UIColor and the initializer.
Working with Multiple Canvases
Apps that require layered drawings (e.g., page‑based note app) can use several PKCanvasView instances. Each has its own PKDrawing. The tool picker can be shared by adding observers for each canvas. Be careful with responder management – only one canvas can be first responder at a time.
Adding a Ruler
PencilKit includes a straight edge (ruler) that you can programmatically toggle:
canvasView.isRulerActive = true
The user can also toggle it via a button in the default tool picker. When active, strokes snap to the ruler edge.
Performance Optimization
Drawing Policy for Touch Input
On older devices, processing finger strokes can be slower. Consider using .pencilOnly for high‑end features or .anyInput with a minimum stroke thickness to reduce touch artifacts.
Managing Large Drawings
PencilKit automatically tiles the canvas for efficient rendering; however, drawings with tens of thousands of vector strokes may cause memory warnings. If your app expects very large artworks, you can periodically split the drawing into multiple pages or downsample. Also, avoid keeping many PKDrawing objects in memory at once.
Throttling Save Calls
Save the drawing only when the user finishes a stroke (e.g., after canvasViewDidEndUsingTool) rather than on every canvasViewDrawingDidChange to reduce I/O overhead.
Handling Input from Apple Pencil and Touch
PencilKit intelligently differentiates between Apple Pencil and finger input. You can customize how each is handled using the drawingPolicy and by rejecting specific touches. For example, to disable finger drawing while allowing finger gestures for navigation:
canvasView.drawingPolicy = .pencilOnly
// then add your own gesture recognizers for pan, pinch, etc.
If you need to detect when the pencil is in use, observe touch events via UIResponder methods on the canvas view subclass, but be careful not to interfere with PencilKit’s internal handling.
Integrating with Other Frameworks
Vision + CoreML for Handwriting Recognition
You can pass the PKCanvasView’s drawing data to Vision’s handwriting recognition request. First, convert the drawing to an image using image(from:scale:), then run a VNRecognizeTextRequest. This enables converting pen strokes to digital text.
Sharing Drawings as Images or PDF
Render the canvas as a UIImage for sharing:
let image = canvasView.drawing.image(from: canvasView.bounds, scale: UIScreen.main.scale)
let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil)
present(activityVC, animated: true)
For PDF export, use a PDF renderer context and draw the canvas view.
Common Pitfalls and Troubleshooting
- Tool picker not showing: Ensure the canvas is first responder and the tool picker is added as an observer. Call
canvasView.becomeFirstResponder()after the view appears (not justviewDidLoad). - Drawing not appearing after load: Verify the data is valid
PKDrawing. Usetry? PKDrawing(data:)and check for nil. - Performance drops on large drawings: Consider splitting drawings into pages; use
.pencilOnlyto reduce input load. - Undo not working: Implement your own undo stack; PencilKit does not provide built‑in undo.
- iOS 13 compatibility: The
PKToolPicker.shared(for:)method requires iOS 13 or later; on earlier versions you cannot use PencilKit.
Example: Full Minimal Drawing View Controller
import UIKit
import PencilKit
class DrawingViewController: UIViewController, PKCanvasViewDelegate {
let canvasView = PKCanvasView()
var toolPicker: PKToolPicker!
override func viewDidLoad() {
super.viewDidLoad()
setupCanvas()
setupToolPicker()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
canvasView.becomeFirstResponder()
}
private func setupCanvas() {
canvasView.frame = view.bounds
canvasView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
canvasView.backgroundColor = .white
canvasView.delegate = self
view.addSubview(canvasView)
}
private func setupToolPicker() {
guard let window = view.window else { return }
toolPicker = PKToolPicker.shared(for: window)
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.tool = PKInkingTool(.pen, color: .black, width: 5)
}
// MARK: - Save & Load
func saveDrawing() -> Data? {
return canvasView.drawing.dataRepresentation()
}
func loadDrawing(_ data: Data) {
if let drawing = try? PKDrawing(data: data) {
canvasView.drawing = drawing
}
}
// MARK: - PKCanvasViewDelegate
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
// Optional: track changes for undo
}
}
Conclusion
PencilKit enables developers to add a full‑featured drawing canvas with minimal effort. By understanding its core components – PKCanvasView, PKToolPicker, and the various tools – you can build robust drawing experiences that feel native. The framework handles the heavy lifting of vector stroke management, pressure sensitivity, and input coalescing, while leaving you the freedom to customize the UI and integrate with other iOS technologies like Vision and Core Data.
For further exploration, consult the official PencilKit documentation and sample code from Handling Apple Pencil and Touch Input. Experiment with different tool types and drawing policies to find the best fit for your app’s use case.