iOS Touch Drawing
In a recent project I’ve been working on, we needed to provide the user a way of drawing on top of a photo by touching the screen. The user would be able to draw freely over the image, and also they would be able to erase any of their previous drawing simply by touching the erase button and then touching the drawing.
As it turns out the hard part was not the drawing itself, since a quick internet search threw a fantastic drawing tutorial in the Ray Wenderlich site, the hardest for us was the erasing of a particular drawing.
UIGraphicsContext
The Ray Wenderlich tutorial has three main functions that comprise most of the drawing logic.
Where lastPoint
is being used to save the last known point of the line to draw (in this case the last known point is also the initial point).
Then touchesMoved
:
As you can see, drawing a line is actually quite simple, you just create a custom UIView and basically paste this code and that view is almost ready to serve as a drawing canvas for your user.
The problem we had with this approach is that UIGraphicsGetImageFromCurrentImageContext
returns a UIImage?
with size of view.frame.size
and all of the generated images would have the same size and position making it almost impossible to distinguish between one and another, further complicating the erasing of an individual drawing.
Which took us to option number 2:
CAShapeLayer
Using the same touchesBegan/Moved/Ended
functions as before we can create CGPaths
, which describe all of the points where the user has moved it's finger, then we add this path to a CAShapeLayer
and add this new layer as a sublayer of the "canvas" view this way we get images (layers) for each individual drawing.
Now let’s really see how using CGPath
compares to using UIGraphicContext
.
Show me the code!
To begin with we created a custom view called DrawingView
and replaced the touchesBegan
function with this:
The touchesMoved
function remains the same as previous example and the drawLine
gets changed to this:
Comparing previous implementation (CGContext) with current implementation (CALayer) you can see they are incredibly similar. They both make almost the same function calls, but in different contexts. Instead of
context.move(to: fromPoint)
, we usecurrentPath.move(to: fromPoint)
, instead ofcontext.setLineCap(.round)
we now usecurrentLayer.lineCap = .round
.
Finally the touchesEnded
function gets changed to this, since we need to render the added sublayers:
At this point the user can draw by moving it’s finger on the screen, each drawing is individual and it’s linked to a CAShapeLayer
.
Now how do we erase specific drawings? This is the issue we wanted to solve in the beggining, so let's get to it!
Erasing
First we will create a function that will help us find the layer that contains the touch point:
Now if you have some worked previously with CAShapeLayers
you may have used the hitTest
function to check if the layer contains a CGPoint
.
In our specific case this doesn’t work because for the hitTest
function requires that the layer
have a frame
and we are not assigning one to it, because if the user draws a long diagonal line through the screen the frame that contains the whole drawing would be a really big rectangle which would detect touches even if the finger is far away from the actual drawn line.
Where the red line is the line drawed by the user, and the blue rectangle is the frame.
Another thing that pops up in the previous code is that we are creating an outline of the shapeLayer.path
and then calling the contains(point)
function on it, why not call the contains function directly on the path, why do we need to create an outline?
Well it turns out that the contains(point)
function only works on closed paths, so if the user draws a straight line that funtion would always return false
.
The outline is something like this:
As you can see the outline is a closed shape, so it gives us the required precision we want when deleting shapes by tapping.
Great explanation of this here.
Ok let’s carry on, the findLayer
function returns the touched layer
, so now we need to delete it:
With these two new functions in place, we need a way to switch between drawing and erasing, so we add a control property and use it to limit drawing capabilities in touchesBegan
:
and touchesMoved
:
In touchesEnded
we call the findLayer
if isDrawing == false
The result is this:
It looks kind of nice! Well this are the basics for drawing in iOS, the code can be found here.
This is a very simple example of how drawing and erasing works, this code can be further exapanded and refactored to give the user more drawing options like drawing a square or a circle, or showing the angle between two lines. These options will be covered on a following article.
I hope you liked it! Thanks for reading!