Drawing with PencilKit: Getting Started

In this PencilKit tutorial, you’ll learn to use PencilKit and an application where users can draw on a canvas using provided tools. By Christine Abernathy.

4.7 (17) · 1 Review

Download materials
Save for later
Share

Apple introduced PencilKit in iOS 13, making it easy to use Apple Pencil to add drawing features to iOS apps. Before iOS 13, integrating Apple Pencil was more complicated. PencilKit allows you to easily build apps that create drawings or annotate documents.

Low latency and efficient drawing are two highlights of PencilKit. Drawing with it feels fluid and responsive. PencilKit provides a rich palette to help users pick the right tools for drawing. These include different brush types, an eraser and a lasso selection tool. These combine to make PencilKit the de facto choice for creating drawing apps.

In this tutorial, you’ll learn the basics of PencilKit by building out MasterThis. The sample app lets you flex your artistic side as you practice drawing masterpieces. Once satisfied, you can share your rendition with future admirers.

Completed app flow: Empty canvas, masterpiece selection, drawing, share sheet

Getting Started

Click the Download Materials button at the top or bottom of this tutorial to download the starter project. Now, open MasterThis.xcodeproj in the starter folder. Build and run the project. You should see a blank screen waiting for you to unleash your creative brilliance:

Starter app: empty canvas

Tap + in the top left to show the list of masterpieces available for you to practice drawing with. Double-tap one to select it:

Starter app: masterpiece selection

Tap anywhere outside the pop-up to dismiss it and get ready to draw. However, the app’s not quite ready for you. Fortunately, you’re an iOS developer and a super hero so you should be ready to go.

PencilKit super hero reporting for duty

You’ll be adding functionality to turn that empty black hole into a canvas fit for Picasso.

Take a look at the key files in MasterThis to familiarize yourself with the project:

  • DrawingView: Where the drawing (magic) happens.
  • Rendition: Your rendition’s data model, which will host your drawing data.

Get ready to work!

Add a Canvas View

Any good artist knows that you typically draw on a canvas. The PencilKit equivalent is PKCanvasView. It captures Apple Pencil input and presents it on the screen. PKCanvasView is a subclass of UIScrollView, so it allows bounds that are larger than the visible area.

PKCanvasView can present a tool picker to the user. The tool picker contains the artist’s brushes, pens, ink, paint and other required drawing tools. Users can pick which tool they want to use.

PencilKit tool picker

You may choose not to present a picker and instead pre-select the tools available for drawing. This may make users very sad, but hey, whatever makes sense for your app. For example, you can pre-select PKInkingTool to draw lines on a canvas view.

Start by adding PKCanvasView to the starter app.

Right-click Views in Project navigator, then select New File…. Choose the iOS ▸ User Interface ▸ SwiftUI View template, and click Next. Name the file CanvasView.swift and click Create.

PKCanvasView is a UIKit view. To use it in SwiftUI, you need to wrap it in a SwiftUI view that conforms to UIViewRepresentable.

Importing the Framework

Add the following after the import SwiftUI:

import PencilKit

Here, you import the PencilKit framework.

Delete the CanvasView_Previews definition and replace CanvasView implementation with the following:

struct CanvasView {
  @Binding var canvasView: PKCanvasView
}

This creates a single property representing a canvas view. The canvasView property is a @Binding, which indicates another object owns it.

Add the following delegate extension to the end of the file:

extension CanvasView: UIViewRepresentable {
  func makeUIView(context: Context) -> PKCanvasView {
    canvasView.tool = PKInkingTool(.pen, color: .gray, width: 10)
    #if targetEnvironment(simulator)
      canvasView.drawingPolicy = .anyInput
    #endif
    return canvasView
  }

  func updateUIView(_ uiView: PKCanvasView, context: Context) {}
}

In this code, you conform to UIViewRepresentable and implement the two required methods. SwiftUI uses these to create and update the canvas view.

The drawingPolicy property on PKCanvasView controls what kinds of input the canvas view accepts. By default, it listens only for pencil input. This doesn’t work for devices that don’t support an Apple Pencil, such as an iPhone or the simulator. You set the policy to anyInput in the simulator environment to facilitate testing. If you want to support finger drawing in general, remove the conditional compilation instructions.

makeUIView(context:) adds a gray pen inking tool to allow the user to draw. The pen has a base width of 10. What’s a base width?

PencilKit base width guess

The final width applied to a drawing depends on the ink type selected and the Apple Pencil input. A marker’s final width will be thicker than a pen. If a user applies more force on the screen, the final width will be wider.

You don’t need your UIKit to react to any changes in the SwiftUI view, so keep updateUIView(_:context:) empty.

Now that you have the SwiftUI representation of your canvas view, you can integrate it into your main view.

Go to DrawingView.swift and add the following after the SwiftUI import:

import PencilKit

This imports the PencilKit framework.

Initializing the Canvas View

Now, add the following at the end of the state properties list:

@State private var canvasView = PKCanvasView()

Here, you initialize the property that represents the PencilKit canvas view. Later on, you’ll want to control this view from DrawingView so you can make updates to it.

Next, in body, replace:

Rectangle()

With:

CanvasView(canvasView: $canvasView)

You set up your CanvasView instance here and pass in a binding to the canvasView property.

Select iPad in the Scheme simulators list if it isn’t selected. Then, open the SwiftUI canvas if it isn’t already open. Use Adjust Editor Options ▸ Canvas at the top right of the editor or press Option-Command-Return. Click Resume in the preview pane. You should see a white canvas:

Preview with canvas added

Click Live Preview to run the app:

Activate live preview

Using your mouse, draw on the canvas. You should see something like this:

Drawing on canvas

Ah, so satisfying. You’re making progress.

PencilKit crown me now

If you have access to an iPad and Apple Pencil, build and run the app on your device. Apply different forces when you draw and see how the thickness of the drawing changes:

Apple Pencil drawings with various pressure

You’ve still got a ways to go. You’ve probably noticed by now that there’s no way to erase drawing mistakes.

PencilKit know-it-all

How about adding the ability to start afresh?

Clear the Canvas View

PKCanvasView drawing input is captured in a PKDrawing object. You can save this object for later reuse or even generate an image representation of the object.

Let’s see how to use PKDrawing to clear the canvas.

Still in DrawingView.swift, fill out the deleteDrawing() implementation with the following:

canvasView.drawing = PKDrawing()

Here, you set the drawing property of your canvas view to an empty PKDrawing. This effectively clears the canvas.

Now, make sure you’re in Live Preview mode. In the preview pane, draw on the canvas and click the trash icon to clear the drawing:

Creating drawing then deleting it

Just like that, you’re well on your way to making the fickle, perfectionist artist happier.

Saving the Drawing

The next logical step is saving a masterpiece rendition. To do this, you build on your knowledge of PKDrawing. If you have iOS experience, you probably suspect that delegates are about to get involved. You’re right!

PencilKit delegate detective

By adopting PKCanvasViewDelegate, you can track drawing changes in PKCanvasView. Then, you use a Coordinator to communicate back from the delegate to the SwiftUI view.

Go to CanvasView.swift and add the following property to the struct:

let onSaved: () -> Void

This defines a closure that the SwiftUI view can pass to the UIKit view to receive notifications about drawing changes.

Next, add the following to the end of the file:

class Coordinator: NSObject {
  var canvasView: Binding<PKCanvasView>
  let onSaved: () -> Void

  init(canvasView: Binding<PKCanvasView>, onSaved: @escaping () -> Void) {
    self.canvasView = canvasView
    self.onSaved = onSaved
  }
}

This creates a coordinator to communicate between the SwiftUI view and the canvas view. The custom initializer sets up a binding to the canvas view and the closure to call on drawing updates.

Then, add the following method at the end of the UIViewRepresentable extension:

func makeCoordinator() -> Coordinator {
  Coordinator(canvasView: $canvasView, onSaved: onSaved)
}

This code creates the coordinator and returns it to the SwiftUI view. SwiftUI calls this method before makeUIView(context:) sets up your canvas view to ensure that the coordinator is available when you create and configure CanvasView.

Setting up the Delegate

You can now set up your delegate to respond to drawing updates. Add the PKCanvasViewDelegate extension to the end of the file:

extension Coordinator: PKCanvasViewDelegate {
  func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
    if !canvasView.drawing.bounds.isEmpty {
      onSaved()
    }
  }
}

canvasViewDrawingDidChange(_:) is called whenever the contents of the canvas view change. Your implementation checks if there is content available before calling the save closure.

Finally, add the following before the return statement in makeUIView(context:):

canvasView.delegate = context.coordinator

Here, you assign the coordinator you defined as the canvas view delegate.

Go ahead and save the file to make sure Xcode takes note of the updates to CanvasView.

Back in DrawingView.swift, replace CanvasView(canvasView: $canvasView) with the following:

CanvasView(canvasView: $canvasView, onSaved: saveDrawing)

This updates the initializer and passes in the method to call for drawing updates.

Next, implement saveDrawing() by adding the following:

print("Drawing saved...")

Here, you log an entry to check if the save method is called.

Build and run the app. Draw on the canvas and verify that the following is logged in the console whenever you finish drawing a stroke:

Drawing saved...

Now that your coordinator is working, it’s time to save the drawing. The data model representing your masterpiece rendition is where you make the changes.

Changing the Data Model

Open Rendition.swift and replace the import UIKit with the following:

import PencilKit

This imports the PencilKit framework so your data model knows about drawings.

Next, add the following right after the title property definition:

let drawing: PKDrawing

With those changes, Rendition is ready to store PKDrawing objects.

Save the file.

Go to DrawingView.swift and replace the print statement in saveDrawing() with:

// 1
let image = canvasView.drawing.image(
  from: canvasView.bounds, scale: UIScreen.main.scale)
// 2
let rendition = Rendition(
  title: "Best Drawing",
  drawing: canvasView.drawing,
  image: image)
// 3
self.rendition = rendition

Here’s what the code is doing:

  1. You create an image representation of the canvas view’s drawing. The image is based on the visible bounds of the canvas and the scale factor defined for the device. This means that the saved image will look sharper for Retina display screens.
  2. You create a rendition instance with PKDrawing and UIImage versions of the drawing. Use these representations to restore and share your drawings.
  3. You update your rendition state variable so SwiftUI updates the view.

Then, replace the restoreDrawing() implementation with the following:

if let rendition = rendition {
  canvasView.drawing = rendition.drawing
}

This checks whether there’s a saved drawing. If one is found, it sets the canvas view drawing to the value. This shows how to update the canvas view with a pre-existing PKDrawing.

Click Resume in the preview pane, then click Live Preview to run the app. Draw on the canvas, then click on the trash icon to delete the drawing. Next, click the undo icon found next to the trash icon. Verify that your previous drawing is restored:

Deleting a drawing then restoring it

Adding the Tool Picker

You’ve endured the gray palette thus far, but I know you’re ready for more. Wouldn’t it be great if you could give users the ability to pick their own colors? What about the ability to select different brushes or change the widths? PencilKit has you covered.

You can easily add a tool palette to your app and instantly give users a selection of tools and colors. PKToolPicker manages this tool palette. You simply add it to your view hierarchy and control when it’s displayed.

PKToolPicker offers the following drawing tools:

  • PKInkingTool: Tools for drawing lines. You’ve already seen this in action
  • PKEraserTool: Brush for deleting portions of your drawing
  • PKLassoTool: Tool for selecting portions of your drawing so you can reposition it
  • Ruler: Helps you draw straight lines
  • Color picker: Use it to select a color for your inking tool
  • Undo/Redo: Controls you use to undo and redo drawing changes

The various tools and options in the tool picker

Setting up the Tool Picker

To set up a tool picker, you first associate it with a window in your app’s interface. Then, you set a view as the first responder for that window. When the view is visible, the tool picker is shown. When the view is not visible, the tool picker is hidden.

Once a tool picker is visible, the user can move the palette around. This is convenient if the palette is hiding a space where the user wants to draw.

Recall that you hard-coded the type of inking tool, color, and baseline width to apply to your canvas view. PKCanvasView implements an observer protocol for detecting tool picker changes. It gets notified and updates the current drawing tools.

Go to CanvasView.swift and add the following property after the declaration of onSaved:

@State var toolPicker = PKToolPicker()

You need to keep a reference to your PKToolPicker instance so that it displays. As a result, you declare it as a @State property.

Next, add the following extension after the struct declaration:

private extension CanvasView {
  func showToolPicker() {
    // 1
    toolPicker.setVisible(true, forFirstResponder: canvasView)
    // 2
    toolPicker.addObserver(canvasView)
    // 3
    canvasView.becomeFirstResponder()
  }
}

Here’s a step-by-step breakdown of what’s going on in the defined method:

  1. Tie the tool picker’s visibility to whether the canvas view is active.
  2. Ensure the canvas view is notified of any changes to the tool picked.
  3. Ask the canvas view to become the first responder. This makes the tool picker visible.

Add the following to makeUIView(context:) right after the canvasView delegate is assigned:

showToolPicker()

This calls your new private method to activate the tool picker.

Build and run the app. You should see the tool picker presented:

Tool picker added to the app

Try moving the tool picker around the window. Also, try out the various picker tools at your disposal.

Note: The tool picker has some limitations on the iPhone. The tool picker is docked to the bottom of the window. Also, undo and redo functionality isn’t available. You must implement these features yourself.

The tool picker opens up what you can do, but it can’t do everything. If you are looking for more customizations, consider building your own tool picker.

Alright, junior Picasso, you have everything you need to practice your masterpiece skills. Go!

An example drawing experience with the completed app

Courtesy of 10-year-old budding artist and developer

Once you’re happy with your work, go ahead and share it. The sample app contains code to trigger Apple’s share sheet. Click the share icon to see this in action:

Sharing the drawing, using the share sheet

Advanced PencilKit

You’ve seen how to use PKDrawing to clear the canvas and save a drawing.
You can change PKDrawing to suit your needs. For example, you might want to create a drawing on one device and display on another with a different canvas size. You can perform a CGAffineTransform on the drawing to do this. For example:

let transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
let drawingTransformed = pkDrawing.transformed(using: transform)

This code scales down a drawing by 50 percent.

You can even append one drawing to another by doing something like this:

let drawingCombined = pkDrawing1.appending(pkDrawing2)

Double-tapping the lower part of the Apple Pencil changes the tool selected. Try it out if you can. By default, it switches between the eraser and the currently selected tool. Users can go to Settings and change how they want double-tap to work. If your app has custom double-tap support, make sure you don’t confuse users if they’re used to a certain behavior. Give them the ability to opt in to your custom behavior.

Apple Pencil can sense tilt, force and orientation. By default, this changes the stroke. For example, applying more force leads to thicker lines. If you customize the tool picker, apply these data when drawing strokes. This Apple Pencil Tutorial shows an example of how to do this.

One other thing to be mindful of is that users can turn on dark mode. By default, colors in the PencilKit canvas adjust based on dark mode settings. Think about preventing automatic color changes for apps where users draw on top of a PDS or photo. This makes sure the markup remains visible.

Where to Go From Here?

Congratulations on completing the PencilKit tutorial! As you’ve seen, it’s very easy to build an app with drawing capabilities that gives users a lot of room to get creative.

Download the completed version of the project using the Download Materials button. You can find this at the top and bottom of this tutorial.

Check out the Apple Pencil Tutorial for a deeper look into what you can do with Apple Pencil. Introducing PencilKit from WWDC 2019 gives a good overview of the various features. For new updates to PencilKit framework in iOS 14, make sure you check out What’s new in PencilKit session from WWDC 2020.

I hope you enjoyed this tutorial! If you have any questions or comments, please join the discussion below.