UIKit Drawing Tutorial: How to Make a Simple Drawing App
Learn how to create a simple drawing app in Swift using UIKit drawing APIs. By Ron Kliffer.
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
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
UIKit Drawing Tutorial: How to Make a Simple Drawing App
25 mins
At some stage in our lives, we all enjoyed drawing pictures, cartoons and other stuff.
For me, it was using a pen and paper but, these days, the old pen and paper have been replaced by computers and mobile devices! Drawing can be especially fun on touch-based devices, as you can see by the abundance of drawing apps on the App Store.
Want to learn how to make a drawing app of your own? The good news is that it’s pretty easy, thanks to some great drawing APIs available through UIKit in iOS.
In this tutorial, you will create an app very much like Color Pad for iPhone. In the process you’ll learn how to:
- Draw lines and strokes using Quartz2D.
- Use multiple colors.
- Set the brush stroke width and opacity.
- Create an eraser.
- Create a custom RGB color selector.
- Share your drawing!
Grab your pencils and get started; no need to make this introduction too drawn out! :]
Getting Started
Use the Download Materials button at the top or bottom of this tutorial to download the starter project.
Start Xcode, open the starter project and have a look at the files inside. As you can see, not much work has been done for you. You have all the needed images in the asset catalog and the main view of the app with all needed constraints set. The whole project is based on the Single View Application template.
Open Main.storyboard and look at the interface. The initial ViewController scene has three buttons on the top. As the titles suggest, you will use these to reset or share a drawing and to bring up a Settings screen. On the bottom, you can see more buttons with pencil images and an eraser. You’ll use these to select predefined colors.
Finally, there are two image views called MainImageView and TempImageView — you’ll see later why you need two image views when you use them to allow users to draw with different brush opacity levels.
The View Controller window has the actions and outlets set as you’d expect: Each button at the top has an action, the pencil colors all link to the same action — they have different tags set to tell them apart — and there are outlets for the two image views.
In order to let your inner artist shine, you’ll need to add some code!
Being Quick on the Draw
You’ll begin with a simple drawing feature whereby you can swipe your finger on the screen to draw simple black lines. Hey, even Picasso started with the basics!
Open ViewController.swift and add the following properties to the class:
var lastPoint = CGPoint.zero
var color = UIColor.black
var brushWidth: CGFloat = 10.0
var opacity: CGFloat = 1.0
var swiped = false
Here’s a quick explanation of these variables:
-
lastPoint
stores the last drawn point on the canvas. You’ll need this when your user draws a continuous brush stroke on the canvas. -
color
stores the current selected color. It defaults to black. -
brushWidth
stores the brush stroke width. It defaults to 10.0. -
opacity
stores the brush opacity. It defaults to 1.0. -
swiped
indicates if the brush stroke is continuous.
Now for the drawing part! All touch-notifying methods come from the parent class UIResponder
; they fire in response to a touch beginning, moving or ending. You’ll use these three methods to implement your drawing logic.
Start by adding the following method:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
swiped = false
lastPoint = touch.location(in: view)
}
The system calls touchesBegan(_:with:)
when the user puts a finger down on the screen. This is the start of a drawing event, so you first make sure you indeed received a touch. You then reset swiped
to false
since the touch hasn’t moved yet. You also save the touch location in lastPoint
so when the user starts drawing, you can keep track of where the stroke started. This is, so to speak, where the brush hits the paper. :]
Add the following two methods next:
func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
// 1
UIGraphicsBeginImageContext(view.frame.size)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
tempImageView.image?.draw(in: view.bounds)
// 2
context.move(to: fromPoint)
context.addLine(to: toPoint)
// 3
context.setLineCap(.round)
context.setBlendMode(.normal)
context.setLineWidth(brushWidth)
context.setStrokeColor(color.cgColor)
// 4
context.strokePath()
// 5
tempImageView.image = UIGraphicsGetImageFromCurrentImageContext()
tempImageView.alpha = opacity
UIGraphicsEndImageContext()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
// 6
swiped = true
let currentPoint = touch.location(in: view)
drawLine(from: lastPoint, to: currentPoint)
// 7
lastPoint = currentPoint
}
Here’s what’s going on in these methods:
- The first method is responsible for drawing a line between two points. Remember that this app has two image views:
mainImageView
, which holds the “drawing so far”, andtempImageView
, which holds the “line you’re currently drawing”. Here, you want to draw intotempImageView
, so you need to set up a drawing context with the image currently in thetempImageView
, which should be empty the first time. - Next, you get the current touch point and then draw a line from
lastPoint
tocurrentPoint
. You might think that this approach will produce a series of straight lines and the result will look like a set of jagged lines. This will produce straight lines, but the touch events fire so quickly that the lines are short enough and the result will look like a nice smooth curve. - Here, you set some drawing parameters for brush size and stroke color. If you’re interested in learning more about these, check out Apple’s documentation for CGContext.
- This is where the magic happens and where you actually draw the path!
- Next, you need to wrap up the drawing context to render the new line into the temporary image view. You get the image representation of the context and set it to the
image
property oftempImageView
. - The system calls
touchesMoved(_:with:)
when the user drags a finger along the screen. Here, you setswiped
to true so you can keep track of whether there is a current swipe in progress. Since this istouchesMoved
, the answer is yes, there is a swipe in progress! You then call the helper method you just wrote to draw the line. - Finally, you update
lastPoint
so the next touch event will continue where you just left off.
Next, add the final touch handler:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if !swiped {
// draw a single point
drawLine(from: lastPoint, to: lastPoint)
}
// Merge tempImageView into mainImageView
UIGraphicsBeginImageContext(mainImageView.frame.size)
mainImageView.image?.draw(in: view.bounds, blendMode: .normal, alpha: 1.0)
tempImageView?.image?.draw(in: view.bounds, blendMode: .normal, alpha: opacity)
mainImageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
tempImageView.image = nil
}
iOS calls touchesEnded(_:with:)
when the user lifts his finger off the screen. Here, you first check if the user is in the middle of a swipe. If not, then it means that the user just tapped the screen to draw a single point. In that case, just draw a single point using the helper method you wrote earlier.
If the user was in the middle of a swipe, that means you can skip drawing that single point — since touchesMoved(_:with:)
was called before, you don’t need to draw any further.
The final step is to merge the tempImageView
with mainImageView
. You drew the brush stroke on tempImageView
rather than on mainImageView
. What’s the point of an extra UIImageView
; can’t you just draw directly to mainImageView
?
You could, but the dual image views are used to preserve opacity. When you’re drawing on tempImageView
, the opacity is set to 1.0 (fully opaque). However, when you merge tempImageView
with mainImageView
, you can set the tempImageView
opacity to the configured value, thus giving the brush stroke the opacity you want. If you were to draw directly on mainImageView
, it would be incredibly difficult to draw brush strokes with different opacity values.
OK, time to get drawing! Build and run your app. You will see that you can now draw pretty black lines on your canvas!
That’s a great start! With just those touch handling methods you have a huge amount of the functionality in place. Now it’s time to fill in some more options, starting with color.
Adding Colors
It’s time to add a splash of color to the scene — line art alone is kind of drab.
There are 10 color buttons on the screen at the moment, but if you tap any button right now, nothing happens. To get this working, first you’ll need to define all the colors.
Open Pencil.swift. Pencil
is an enum
that represents the various color options the user can choose from. It has a single initializer init?(tag:)
that accepts a button tag
and turns it into the desired pencil. Add the following computed property to Pencil
:
var color: UIColor {
switch self {
case .black:
return .black
case .grey:
return UIColor(white: 105/255.0, alpha: 1.0)
case .red:
return UIColor(red: 1, green: 0, blue: 0, alpha: 1.0)
case .darkblue:
return UIColor(red: 0, green: 0, blue: 1, alpha: 1.0)
case .lightBlue:
return UIColor(red: 51/255.0, green: 204/255.0, blue: 1, alpha: 1.0)
case .darkGreen:
return UIColor(red: 102/255.0, green: 204/255.0, blue: 0, alpha: 1.0)
case .lightGreen:
return UIColor(red: 102/255.0, green: 1, blue: 0, alpha: 1.0)
case .brown:
return UIColor(red: 160/255.0, green: 82/255.0, blue: 45/255.0, alpha: 1.0)
case .orange:
return UIColor(red: 1, green: 102/255.0, blue: 0, alpha: 1.0)
case .yellow:
return UIColor(red: 1, green: 1, blue: 0, alpha: 1.0)
case .eraser:
return .white
}
}
This returns the appropriate UIColor
for each selected pencil.
Next, open ViewController.swift, add the following to pencilPressed(_:)
:
// 1
guard let pencil = Pencil(tag: sender.tag) else {
return
}
// 2
color = pencil.color
// 3
if pencil == .eraser {
opacity = 1.0
}
Here’s a look at this, step by step:
- First, you need to know which color index the user selected. There are many places this could go wrong, such as using an incorrect tag or a tag not set, so you guard against there not being a matching
Pencil
. - Next, you set the drawing color to the color the user selected.
- The last color is the eraser, so it’s a bit special. The eraser button sets the color to white and opacity to 1.0. As your background color is also white, this will give you a very handy eraser effect!
What? Time for more drawing already? Yup — build and run, and get ready to let the colors fly. Now, tapping a color button changes the brush stroke to use that button’s color. No more drab line art!
Resetting to Tabula Rasa
All great artists have those moments in which they step back and shake their heads muttering, “No! No! This will never do!” You’ll want to provide a way to clear the drawing canvas and start over again. You already have a Reset button set up in your app for this.
In ViewController.swift, add the following to resetPressed(_:)
:
mainImageView.image = nil
That’s it, believe it or not! All the code above does is set the mainImageView
‘s image to nil
, and voila! Your canvas is cleared! Remember, you drew lines into the image view’s image context, so clearing that out to nil here will reset everything.
Build and run your code again. Draw something, and then tap the Reset button to clear your drawing. There! No need to go tearing up canvases in frustration.
Adding Finishing Touches — Settings
You now have a functional drawing app, but there’s still that second screen of Settings to deal with.
Open Main.storyboard and click on the Settings View Controller scene. The Settings scene has five UISlider
components to set the brush width, brush opacity and brush RGB color values. You can also see a UIImageView
that shows a preview generated from all the selected values.
Now, open SettingsViewController.swift and add the following properties to the class:
var brush: CGFloat = 10.0
var opacity: CGFloat = 1.0
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
This will let you keep track of the brush size, opacity and color the user selects.
Next, add the following method:
func drawPreview() {
UIGraphicsBeginImageContext(previewImageView.frame.size)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.setLineCap(.round)
context.setLineWidth(brush)
context.setStrokeColor(UIColor(red: red,
green: green,
blue: blue,
alpha: opacity).cgColor)
context.move(to: CGPoint(x: 45, y: 45))
context.addLine(to: CGPoint(x: 45, y: 45))
context.strokePath()
previewImageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
This method uses the same techniques to draw a preview of the settings that drawLine(from:to:)
used in ViewController
. However, in this case, the method draws a single point rather than a line with the appropriate line width and opacity from the slider values.
Next, add the following to brushChanged(_:)
:
brush = CGFloat(sender.value)
labelBrush.text = String(format: "%.1f", brush)
drawPreview()
and add the following to opacityChanged(_:)
:
opacity = CGFloat(sender.value)
labelOpacity.text = String(format: "%.1f", opacity)
drawPreview()
In the code above, as the user changes the slider values, you need to set the respective values accordingly and then update the preview image by calling drawPreview()
.
Build and run, open the Settings screen, and play around with the sliders. You’ll see that now the preview image and value labels change as you move them! The color sliders don’t work yet, but you’ll come back to them shortly.
Integrating Settings
There’s still one important piece missing here. Did you notice what it was?
The updated opacity and width values are still not being applied to the ViewController drawing canvas! That’s because you have not yet communicated the values specified in the Settings screen to the ViewController yet. This is a perfect job for a delegate protocol.
Open SettingsViewController.swift and add the following code just below the import:
protocol SettingsViewControllerDelegate: class {
func settingsViewControllerFinished(_ settingsViewController: SettingsViewController)
}
This defines a class protocol with one required method. The settings screen will use this communicate back with any party interested in what the updated settings.
Also add a property to SettingsViewController
:
weak var delegate: SettingsViewControllerDelegate?
This will hold the reference to the delegate. If there is a delegate, you’ll need to notify it when the user taps the Close button. Add the following to closePressed(_:)
:
delegate?.settingsViewControllerFinished(self)
This calls the delegate method so it can update the delegate with the new values.
Now, open ViewController.swift and add a new class extension for the protocol to the bottom of the file:
// MARK: - SettingsViewControllerDelegate
extension ViewController: SettingsViewControllerDelegate {
func settingsViewControllerFinished(_ settingsViewController: SettingsViewController) {
brushWidth = settingsViewController.brush
opacity = settingsViewController.opacity
dismiss(animated: true)
}
}
This declares the class as conforming to SettingsViewControllerDelegate
and implements its single method. In the implementation, all it needs to do is set the current brush width and opacity to the values from the settings view’s slider controls.
When the user moves from the Drawing to the Settings screen, you’ll want the sliders to show the currently selected values for brush size and opacity. That means you’ll need to pass them along when you open Settings.
Add the following method override to the ViewController
class:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard
let navController = segue.destination as? UINavigationController,
let settingsController = navController.topViewController as? SettingsViewController
else {
return
}
settingsController.delegate = self
settingsController.brush = brushWidth
settingsController.opacity = opacity
}
When the user triggers the segue by tapping the Settings button, this method configures the new SettingsViewController
by setting itself as the delegate and passing the current brush and opacity values through.
Build and run. At this stage, you’ll see the brush and opacity values are now updated after you change them in the settings screen. Now you can draw with many colors of different brush sizes and opacity levels!
Adding Finishing Touches — A Custom Color Selector
Currently, you have 10 color buttons on your Drawing Canvas screen. However, with the custom RGB color selector, the discriminating artists using your app will have the ability to pick any available color from the RGB range.
There are a set of RGB color sliders in the settings screen that you’ll implement next.
Since you’ve provided a preview of the brush size and opacity, you might as well integrate a preview of the new brush color!
In SettingsViewController.swift, add the following to colorChanged(_:)
:
red = CGFloat(sliderRed.value / 255.0)
labelRed.text = Int(sliderRed.value).description
green = CGFloat(sliderGreen.value / 255.0)
labelGreen.text = Int(sliderGreen.value).description
blue = CGFloat(sliderBlue.value / 255.0)
labelBlue.text = Int(sliderBlue.value).description
drawPreview()
iOS calls colorChanged(_:)
when you move any of the RGB sliders. In the above code, notice how all you’re doing is updating the property values and updating the labels.
Now that you have the brush and opacity samples drawing with all the correct settings, you’ll want to show them correctly when the Settings screen appears. Add the following to the bottom of viewDidLoad()
:
sliderBrush.value = Float(brush)
labelBrush.text = String(format: "%.1f", brush)
sliderOpacity.value = Float(opacity)
labelOpacity.text = String(format: "%.1f", opacity)
sliderRed.value = Float(red * 255.0)
labelRed.text = Int(sliderRed.value).description
sliderGreen.value = Float(green * 255.0)
labelGreen.text = Int(sliderGreen.value).description
sliderBlue.value = Float(blue * 255.0)
labelBlue.text = Int(sliderBlue.value).description
drawPreview()
As you can see, this just presets all labels and sliders with the correct values. The call to drawPreview()
ensures that the preview image view also shows the correct thing.
Finally, open ViewController.swift. As before, you’ll need to make sure the current color makes it across to the Settings screen, so add the following lines to the end of prepare(for:sender:)
:
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
color.getRed(&red, green: &green, blue: &blue, alpha: nil)
settingsController.red = red
settingsController.green = green
settingsController.blue = blue
This passes the current red, green and blue values so the RGB sliders are set correctly. This syntax might look strange but don’t worry — it is a left-over feature from the old Objective-C days. When you call getRed(_:green:blue:alpha:)
, it sets the variables you pass in with the component values from color
.
Finally, find settingsViewControllerFinished(_:)
in the class extension and add the following lines, just before you call dismiss(animated: true)
:
color = UIColor(red: settingsViewController.red,
green: settingsViewController.green,
blue: settingsViewController.blue,
alpha: opacity)
This updates the color using the new RGB values.
Build and run again and put the color sliders through their paces. Also see how the specified RGB color, which is displayed in the preview, is now the brush stroke color on the drawing canvas!
But what good are all of these wonderful works of art if you can’t share them with the world? Since you can’t stick the pictures up on your refrigerator, you’ll share them with the world in the final step of this tutorial!
Adding Sharing
In this final step, you’ll use the iOS share sheet to send your works of art out into the world!
There are two parts to this: First, you need to get the drawing as a UIImage
object; then, you just pass it on to UIActivityViewController
to decide which sharing options will work best, depending on what accounts and services are available.
In ViewController.swift, add the following to sharePressed(_:)
:
guard let image = mainImageView.image else {
return
}
let activity = UIActivityViewController(activityItems: [image],
applicationActivities: nil)
present(activity, animated: true)
This method is pretty simple. First it verifies that mainImageView
indeed has an image. Then, UIActivityViewController
does most of the heavy lifting! All you need to do is pass it an array of things to share; in this case, that’s just the single image.
The second parameter to the initializer applicationActivities
lets you limit the activities, so passing nil
means iOS will provide as many share options as possible. Your drawing deserves no less!
Build and run the app, and create your masterpiece. When you tap Share, you’ll now be able to let the world know of your talents.
Where to Go From Here?
You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.
You can play around a bit more with the brush strokes and also investigate the drawing of arcs and rectangles using Quartz2D. A good start is the Quartz 2D Programming Guide. There are number of beginner and advanced-level concepts there with which you can experiment to create awesome shapes and patterns.
If you want to learn how to draw even smoother lines, you should also check out this smooth line drawing article by Krzysztof Zablocki. It’s based on Cocos2D but you can use the same technique in UIKit.
As a final challenge, try adding undo-redo capabilities. Check out UndoManager Tutorial: How to Implement With Swift Value Types published on this website by RW team member Lyndsey Scott.
Feel free to post your questions and comments on the forum — and feel free to share your masterpieces you created with your new app! :]