Getting started with PencilKit on iOS 13

Introduction

Recently, I’ve released my new drawing app, called Drawland. It’s using Apple’s new drawing framework PencilKit, so in this post I will share some insights and interesting details about this new framework.

Getting started with PencilKit

PencilKit was introduced on WWDC 2019. Although the name might imply differently, PencilKit works great both on iPads with Apple Pencil and iPhones (using the finger). It’s available only on iOS 13 and above.

Why PencilKit?

In a nutshell, PencilKit provides a canvas view, a tool picker (with several useful tools for drawing), as well as PKDrawing object, that you can use for saving / restoring drawings. Of course, all this functionality can be implemented by yourself. I actually had a go with it, in a recent blog post. While that’s a fun thing to try, when you go in production, there are a lot of details that you need to consider to have a fully-fledged drawing app. And with PencilKit, Apple does that for you. It works seamlessly with Apple Pencil and it has high precision and low latency. With PencilKit, instead of re-inventing the wheel, you can focus on your app’s unique features.

pencilkit.PNG

PKCanvasView

The PKCanvasView represents the drawing canvas. It’s scrollable by default. However, in my case, this was not needed, because two canvases were displayed simultaneously (one for the original image to draw and another one for the drawing area).

The PKCanvasView provides several delegate methods, which inform you about several useful events.

    optional func canvasViewDrawingDidChange(_ canvasView: PKCanvasView)
    optional func canvasViewDidFinishRendering(_ canvasView: PKCanvasView)
    optional func canvasViewDidBeginUsingTool(_ canvasView: PKCanvasView)
    optional func canvasViewDidEndUsingTool(_ canvasView: PKCanvasView)

The canvasViewDrawingDidChange method is called whenever there’s a change in the canvas. It’s useful if you want to mark the drawing as modified, and maybe save it. In my case, it was also the event that triggered the image comparison code.

The canvasViewDidFinishRendering is called when the canvas view is rendered. If you have a more complex drawing, it may take a second, but the drawing is displayed part by part, so this looks good.

The other two delegate methods are called when the user starts and finishes the tool for drawing.

Integrating PKCanvasView in your app is very easy. You can do it either via storyboard or in code.

let canvas = PKCanvasView(frame: bounds)
view.addSubview(canvas)
canvas.tool = PKInkingTool(.pen, color: .black, width: 30)

If you do it via storyboard, make sure to add a regular UIView and then change its class to be PKCanvasView.

PKDrawing

The PKDrawing object represents the actual drawing on the canvas. You can get the frame of the drawing in the canvas. You can also easily serialise / deserialise it, to store it locally or on a server (in my case that was Firebase). Since the original drawings were drawn on one iPad, but displayed on many different devices, we had to re-scale and translate the drawing in many different canvas sizes. For this, the possibility to use CGAffineTransforms on the drawings proved to be very useful. Apart from that, you can append other drawings to the drawing programatically.

let factor = min(scaleX, scaleY)
let transform = CGAffineTransform(scaleX: factor, y: factor)
let drawing = pkDrawing.transformed(using: transform)

Another useful feature from PKDrawing (which I’ve used extensively), is the possibility to get the current UIImage from the drawing.

private func image(from canvas: PKCanvasView) -> UIImage {
    let drawing = canvas.drawing
    let visibleRect = canvas.bounds
    let image = drawing.image(from: visibleRect, scale: UIScreen.main.scale)
    return image
}

PKToolPicker

The PKToolPicker is a UI component that contains the tools for your drawing. On iPads, it’s floating over all views, while on iPhones it’s a view on the bottom of the screen. On iPads, it contains undo/redo functionality, while on iPhones you would need to add them by yourself.

To connect the PKToolPicker with the canvas view, you would need to write the following lines of code:

if let window = parent?.view.window,
   let toolPicker = PKToolPicker.shared(for: window) {
     toolPicker.setVisible(true, forFirstResponder: canvasView)
     toolPicker.addObserver(canvasView)
     toolPicker.addObserver(self)
     canvasView.becomeFirstResponder()

The PKToolPicker consists of several useful tools for drawing.

  • PKInkingTool – Contains the tools for drawing. It consists of a pen, marker or pencil.
  • PKEraserTool – this is the brush that deletes parts or all of your drawing. It can be object based (an entire object is deleted) or pixel based (when you want to remove certain pixels).
  • PKLassoTool – you can select a drawing area and move it around.
  • Ruler – for drawing straight lines.
  • Color picker – for picking a colour for your inking tool.
  • Undo/Redo functionality (only on iPad)

Limitations

However, with so many things coming out of the box, you don’t have a lot of flexibility for doing custom things. Several things I’ve encountered are:

  • it’s hard to customise the tool picker
  • you can’t get a list of all strokes that the drawing is consisted of
  • you get only a data representation of the drawing

Conclusion

PencilKit is a great new framework, which provides a lot of drawing functionality out-of-the-box. It’s a crucial part of my new app, Drawland. It saved me a lot of time and energy to focus on the parts that are specific to my app. It’s easy to get started and to integrate it in an app.

If you want to use it with SwiftUI, you can write your own UIViewRepresentable wrapper from UIKit to SwiftUI.

What are your thoughts on PencilKit? Have you tried it already? Leave your comments in the area below.

Good starting point with PencilKit is Apple’s reference code. If you want to see PencilKit in action, try out Drawland, by downloading it on the store.

6 Comments

  1. Hi Martin, amazing article!!, it has been really helpful. I have a doubt related with undo/redo and different drawingsizes,.. I have created an iPad app and manage the device rotation and multitask sizes using transforms on the drawing, everything worked great but I noticed a few issues with the default undo/redo actions after apply the transformations,… simply its not possible to return an state before the rotation/size change ( because of the transform operation, I checked that ). I tried with and undoManager like apple’s example but not worked as expected, other issues appeared. Did you have noticed about those issues or know about some approach to handle this effectively?

    Like

    1. Hi Gabe, thanks, I’m glad you liked it! I also have the same issue, after applying transforms, the undoManager doesn’t work anymore. It’s probably a bug in PencilKit. I haven’t invested a lot of time to try to fix that, for now I live with the bug. If you have come up with a solution, please share 🙂 Best, Martin

      Like

  2. Hi Martin,Thanks for the great article. Can you please give some insight on how to rescale the drawings?,I am not able to understand how to do it exactly.
    let factor = min(scaleX, scaleY)
    let transform = CGAffineTransform(scaleX: factor, y: factor)
    let drawing = pkDrawing.transformed(using: transform)
    how to assign factor and scaleX and scaleY.Thanks

    Like

    1. Hi Ritika,

      In my implementation, I’ve computed the scaling of x and y, based on the ratio of the current screen size and the drawing size.

      let scaleY = frame.size.height / (pkDrawing.bounds.size.height + pkDrawing.bounds.origin.y) * 1.0
      let scaleX = frame.size.width / (pkDrawing.bounds.size.width + pkDrawing.bounds.origin.x) * 1.0
      let factor = min(scaleX, scaleY)

      Here, frame is the frame of the view and pkDrawing.bounds is the frame of the drawing. This is needed if for example the drawing was drawn on iPad, and needs to be displayed on iPhone. In those cases, we compute the ratio between the current available space (frame) and the drawing size. We take the minimum, because we want to minimum size to fit in the available space.

      Hope this clarifies things.

      Martin

      Like

  3. Thanks a ton Martin for the response.I am using it for a scenario where I need to transfer the drawing back and forth across views of different size(first is of smaller dimension and second is bigger dimension).I am able to use your method and send across one view to another by scaling up but while sending back I need to scale down and doing this multiple times lead to making my drawing appear very tiny on the first view.I was searching if there was something like aspectFit which is used in case of images.
    Thanks

    Like

    1. I see. Probably you would have to make a copy of the PKDrawing when you are passing it to other views and scaling it up or down. There’s no method like aspectFit, you would need to do it by yourself with transformations. If you pass a copy, you will always have the original size untouched and you would be able to do scale back to the original size, without the view getting smaller. I had similar issue with Drawland and that’s how I’ve solved it.

      Like

Leave a comment