Apple Pencil Tutorial: Getting Started
In this Apple Pencil tutorial, you’ll learn about force, touch coalescing, altitude, and azimuth, to add realistic lines and shading to a drawing app. By Caroline Begbie.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Apple Pencil Tutorial: Getting Started
30 mins
- Prerequisites
- Getting Started
- Your First Drawing with Pencil
- Smoother Drawing
- Tilting the Pencil
- Altitude, Azimuth and Unit Vectors
- Draw With Shading
- Working With Texture
- Using Azimuth to Adjust Width
- Playing with Opacity
- Finger vs. Pencil
- Faking Force For a Finger
- Reducing Latency
- Housekeeping: Deleting Drawing Predictions
- Where To Go From Here?
Using Azimuth to Adjust Width
One more thing to do: When you draw at 90 degrees with a real pencil, the line gets narrower as you change the pencil's tilt angle. But, if you try that with your Apple Pencil, the line width stays the same.
In addition to the azimuth angle, you also need to take into Pencil's altitude into account when calculating the width of the line.
Add this constant to the top of the CanvasView class, just below the others:
private let minLineWidth: CGFloat = 5
This will be the narrowest that a shading line can be -- you can change it to suit your own personal shading tastes. :]
At the bottom of lineWidthForShading(_:touch:)
, just before the return
statement, add the following:
// 1
let minAltitudeAngle: CGFloat = 0.25
let maxAltitudeAngle = tiltThreshold
// 2
let altitudeAngle = touch.altitudeAngle < minAltitudeAngle ? minAltitudeAngle : touch.altitudeAngle
// 3
let normalizedAltitude = 1 - ((altitudeAngle - minAltitudeAngle) / (maxAltitudeAngle - minAltitudeAngle))
// 4
lineWidth = lineWidth * normalizedAltitude + minLineWidth
Note: Make sure you add this code to lineWidthForShading(_:touch:)
, and not lineWidthForDrawing(_:touch:)
by mistake.
Note: Make sure you add this code to lineWidthForShading(_:touch:)
, and not lineWidthForDrawing(_:touch:)
by mistake.
There's a lot to digest here, so let's take this bit by bit.
- Theoretically, the minimum altitude of Pencil is 0 degrees, meaning it's lying flat on the iPad and the tip isn't touching the screen, hence, altitude can't be recorded. The actual minimum altitude is somewhere around
0.2
, but I've made the minimum to be0.25
. - If the altitude is less than the minimum, you use the minimum instead.
- Just like you did earlier, you normalize this altitude value to be between
0
and1
. - Finally, you multiply the line width you calculated with the azimuth by this normalized value, and add that to the minimum line width.
Build and run. As you shade, change the Pencil's altitude and see how the strokes get wider and narrower. Increasing the Pencil's altitude gradually should let you segue smoothly into the drawing line:
Playing with Opacity
The last task in this section is to make the shading look a bit more realistic by turning down the texture's opacity, which you'll calculate with force.
Just before the return
statement in lineWidthForShading(_:touch:)
, add the following:
let minForce: CGFloat = 0.0
let maxForce: CGFloat = 5
let normalizedAlpha = (touch.force - minForce) / (maxForce - minForce)
CGContextSetAlpha(context, normalizedAlpha)
After working through the previous blocks of code, this one should be self-explanatory. You're simply taking the force and normalizing it to a value between 0
and 1
, and then setting the alpha used by the drawing context to that value.
Build and run. Try shading with varying pressure:
Finger vs. Pencil
If you're anything like me, you've probably made a few sketching errors here and there and wish you could erase those errant lines.
In this section, you're going to look at how you can distinguish between using the Apple Pencil and your finger. More specifically, you'll configure the app so that your finger can play the role of a faithful eraser.
It turns out that checking whether a finger or the Apple Pencil is being used is pretty easy -- you just use the type
property on UITouch
.
At the top of CanvasView
, add a property for the eraser color. You're going to paint in the background color of the canvas view, and it will give the illusion of acting as an eraser. Clever, eh? :]
private var eraserColor: UIColor {
return backgroundColor ?? UIColor.whiteColor()
}
Here you set eraserColor
to the view's background color, unless it's nil
, in which case you just set it to white.
Next, find the following code in drawStroke(_:touch:)
:
if touch.altitudeAngle < tiltThreshold {
lineWidth = lineWidthForShading(context, touch: touch)
} else {
lineWidth = lineWidthForDrawing(context, touch: touch)
}
pencilTexture.setStroke()
And replace it with the following:
if touch.type == .Stylus {
if touch.altitudeAngle < tiltThreshold {
lineWidth = lineWidthForShading(context, touch: touch)
} else {
lineWidth = lineWidthForDrawing(context, touch: touch)
}
pencilTexture.setStroke()
} else {
lineWidth = 20
eraserColor.setStroke()
}
Here you've added a check to see whether it's Pencil or a finger, and if it's the latter you change the line width and use the eraser color for drawing.
Build and run. Now you can clean up any untidy edges or erase everything with your finger!
Faking Force For a Finger
Just as an aside, did you know that since iOS 8 you've been able to fake force with your finger? There's a property declared on UITouch
called majorRadius
, which, as its name implies, holds the size of the touch.
Find this line that you just added in the previous code block:
lineWidth = 20
And replace it with this one:
lineWidth = touch.majorRadius / 2
Build and run. Shade a dark area, and then erase with both the tip of your finger and the flat of your finger to see the varying thicknesses:
Finger painting feels really clumsy and the drawings are painful after you've played around with the elegant Apple Pencil. :].
Reducing Latency
You might think that your Pencil zooms over the surface of the iPad with the drawn line following closer than ever. Not so much -- it's an illusion because there is latency between the touch and the time the line renders. Apple has a trick up its sleeve to deal with it: Touch Prediction.
Incredible as it may seem, all-seeing Apple knows where your Pencil, or finger, is about to draw. Those predictions are saved into an array on UIEvent
so that you can draw that predicted touch ahead of time. How cool is that!? :]
Before you can begin working with predicted touches, there's one small technical obstacle to overcome. At the moment, you're drawing strokes in the graphics context, which are then displayed immediately in the canvas view.
You'll need to draw the predicted touches onto the canvas but discard them when the actual touches catch up with the predicted ones.
For example, when you draw an S-shape it predicts the curves, but when you change direction, those predictions will be wrong and need to be discarded. This picture illustrates the problem. The "S" is drawn in red and the predicted touches show in blue.
Here's what your code will need to do to avoid this problem:
- You'll create a new
UIImage
property nameddrawingImage
to capture the true -- not predicted -- touches from the graphics context. - On each touch move event, you'll draw
drawingImage
into the graphics context. - The real touches will be drawn into the graphics context, and you'll save it to the new
drawingImage
instead of using theimage
property on the canvas view. - The predicted touches will be drawn into the graphics context.
- The graphics context, complete with predicted touches, will be pushed into
canvasView.image
, which is what the user will see.
In this way, no predicted touches will draw into drawingImage
and each time a touch move event occurs, the predictions will be deleted.