Core Graphics Tutorial: Lines, Rectangles, and Gradients
Learn how to use Core Graphics to draw lines, rectangles, and gradients — starting by beautifying a table view! 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
Core Graphics Tutorial: Lines, Rectangles, and Gradients
30 mins
- Getting Started
- Analyzing the Table View Style
- Hello, Core Graphics!
- Drawing Rectangles
- Showing Your New Cell
- Creating New Colors
- Drawing Gradients
- The Graphics State Stack
- Completing the Gradient
- Fixing the Theme
- Stroking Paths
- Outside the Bounds
- Anti-Aliasing
- Adding a Border
- Building a Card Layout
- Drawing Lines
- Where to Go From Here?
Showing Your New Cell
To see your new view in action, open StarshipListCell.swift. This is a UITableViewCell
subclass for displaying starships in StarshipsViewController
. In awakeFromNib
, add the following code to the end of the method:
backgroundView = StarshipListCellBackground()
This code sets the cell’s background view to be that of your new view. Build and run the app, and you’ll see a lovely yellow background in every cell.
Amazing! You can now draw with Core Graphics. And believe it or not, you’ve already learned a bunch of important techniques: how to get a context to draw in, how to change the fill color and how to fill rectangles with a color. You can make some lovely UI with just that.
But you’re going to take it a step further and learn about one of the most valuable techniques to make excellent UIs: gradients!
Creating New Colors
You’re going to use the same colors again and again in this project, so create an extension for UIColor
to make these readily accessible. Go to File ▸ New ▸ File… and create a new Swift File called UIColor+Extensions. Replace the contents of the file with the following:
import UIKit
extension UIColor {
public static let starwarsYellow =
UIColor(red: 250 / 255, green: 202 / 255, blue: 56 / 255, alpha: 1.0)
public static let starwarsSpaceBlue =
UIColor(red: 5 / 255, green: 10 / 255, blue: 85 / 255, alpha: 1.0)
public static let starwarsStarshipGrey =
UIColor(red: 159 / 255, green: 150 / 255, blue: 135 / 255, alpha: 1.0)
}
This code defines three new colors, which you can access as static properties on UIColor
.
Drawing Gradients
Next, because you’re going to draw many gradients in this project, add a helper method for drawing them. This will simplify the task by keeping the gradient code in one place and avoiding the need to repeat yourself.
Select File ▸ New ▸ File… and create a new Swift File called CGContext+Extensions. Replace the contents of the file with the following:
import CoreGraphics
extension CGContext {
func drawLinearGradient(
in rect: CGRect,
startingWith startColor: CGColor,
finishingWith endColor: CGColor
) {
// 1
let colorSpace = CGColorSpaceCreateDeviceRGB()
// 2
let locations: [CGFloat] = [0.0, 1.0]
// 3
let colors = [startColor, endColor] as CFArray
// 4
guard let gradient = CGGradient(
colorsSpace: colorSpace,
colors: colors,
locations: locations
) else {
return
}
}
}
There’s a lot to this method:
- First, you set up the correct color space. You can do much with color spaces, but you almost always want to use a standard device-dependent RGB color space using
CGColorSpaceCreateDeviceRGB
. - Next, you set up an array that tracks the location of each color within the range of the gradient. A value of 0 means the start of the gradient, and 1 means the end of the gradient.
Note: You can have three or more colors in a gradient if you want, and you can set where each color begins in the gradient in an array like this one. This is useful for certain effects.
- After that, you create an array with the colors you passed into your method. Notice the use of
CFArray
, rather thanArray
, here as you work with the lower level C APIs. - Then, you create your gradient by initializing a
CGGradient
object, passing in the color space, array of colors and locations you previously made. If, for whatever reason, the optional initializer fails, you return early.
You now have a gradient reference, but it hasn’t actually drawn anything yet — it’s just a pointer to the information you’ll use when actually drawing later. It’s nearly time to draw the gradient, but before you do, it’s time for a bit more theory.
The Graphics State Stack
Remember: Core Graphics Contexts are state machines. You have to be careful when setting state on a context, especially in functions that you pass a context or, as in this case, methods on the context itself, because you can’t know the state of the context before you modify it. Consider the following code in a UIView
:
override func draw(_ rect: CGRect) {
// ... get context
context.setFillColor(UIColor.red.cgColor)
drawBlueCircle(in: context)
context.fill(someRect)
}
// ... many lines later
func drawBlueCircle(in context: CGContext) {
context.setFillColor(UIColor.blue.cgColor)
context.addEllipse(in: bounds)
context.drawPath(using: .fill)
}
Glancing at this code, you might think it would draw a red rectangle and a blue circle in the view, but you’d be wrong! Instead, this code draws a blue rectangle and a blue circle — but why?
Because drawBlueCircle(in:)
sets a blue fill color on the context and, because a context is a state machine, this overrides the red fill color set previously.
This is where saveGState()
and its partner method restoreGState()
) come in!
Each CGContext
maintains a stack of the graphics state containing most, although not all, aspects of the current drawing environment. saveGState()
pushes a copy of the current state onto the graphics state stack, and then you can use restoreGState()
to restore the context to that state at a later date and remove the state from the stack in the process.
In the example above, you should modify drawBlueLines(in:)
like this:
func drawBlueCircle(in context: CGContext) {
context.saveGState()
context.setFillColor(UIColor.blue.cgColor)
context.addEllipse(in: bounds)
context.drawPath(using: .fill)
context.restoreGState()
}
You can test this by opening RedBluePlayground.playground in the Download Materials button at the top or bottom of this tutorial.
Completing the Gradient
Armed with knowledge about the graphics state stack, it’s time to finish drawing the background gradient. Add the following to the end of drawLinearGradient(in:startingWith:finishingWith:)
:
// 5
let startPoint = CGPoint(x: rect.midX, y: rect.minY)
let endPoint = CGPoint(x: rect.midX, y: rect.maxY)
// 6
saveGState()
// 7
addRect(rect)
clip()
drawLinearGradient(
gradient,
start: startPoint,
end: endPoint,
options: CGGradientDrawingOptions())
restoreGState()
Here’s a breakdown of that code:
Clipping is an awesome feature in Core Graphics that lets you restrict drawing to an arbitrary shape. All you have to do is add the shape to the context, but instead of filling it like you usually would, you call clip()
on the context, which then restricts all future drawing to that region.
So, in this case, you set the provided rectangle on the context and clip before finally calling drawLinearGradient(_:start:end:options:)
to draw the gradient.
- You start by calculating the start and end points for the gradient. You set this as a line from the top-middle to the bottom-middle of the rectangle. Helpfully,
CGRect
contains some instance properties such asmidX
andmaxY
to make this quite simple. - Next, because you’re about to modify the state of the context, you save its graphics state and end the method by restoring it.
- Finally, you draw the gradient in the provided rectangle.
drawLinearGradient(_:start:end:options:)
is the method that actually draws the gradient but, unless told otherwise, it will fill the entire context, which is the entire view in your case, with the gradient. Here, you only want to fill the gradient in the supplied rectangle. To do this, you need to understand clipping.Clipping is an awesome feature in Core Graphics that lets you restrict drawing to an arbitrary shape. All you have to do is add the shape to the context, but instead of filling it like you usually would, you call
clip()
on the context, which then restricts all future drawing to that region.So, in this case, you set the provided rectangle on the context and clip before finally calling
drawLinearGradient(_:start:end:options:)
to draw the gradient.
It’s time to give this method a whirl! Open StarshipListCellBackground.swift and, after getting the current UIGraphicsContext
in the guard
statement, replace the code with the following:
context.drawLinearGradient(
in: bounds,
startingWith: UIColor.starwarsSpaceBlue.cgColor,
finishingWith: UIColor.black.cgColor)
Build and run the app.
You’ve now added a gradient background to your custom cell. Well done, young Padawan! However, it would be fair to say the finished product isn’t exactly looking great just now. It’s time to fix that with some standard UIKit theming.