Home iOS & Swift Tutorials

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!

5/5 1 Rating

Version

  • Swift 5.5, iOS 15, Xcode 13
Update note: Ron Kliffer updated this tutorial for iOS 15, Xcode 13 and Swift 5.5. Ray Wenderlich wrote the original.

Core Graphics is a cool API on iOS. As a developer, you can use it to customize your UI with some neat effects, often without involving an artist. Anything related to 2-D drawing — such as drawing shapes, filling them in and giving them gradients — is a good candidate for using Core Graphics.

With a history dating back to the early days of OS X, Core Graphics is one of the oldest APIs still used today. Perhaps this is why, for many iOS developers, Core Graphics can be somewhat intimidating at first: It’s a large API and has plenty of snags to get caught on along the way. However, since Swift 3, the C-style APIs have been updated to look and feel like the modern Swift APIs you know and love!

In this tutorial, you’ll build a Star Wars Top Trumps card app, which consists of a list of starships:

Finished starship list

… as well as a detail view for each starship.

Finished starship detail

In creating this app, you’ll learn how to get started with Core Graphics, fill and stroke rectangles and draw lines and gradients to make custom table view cells and backgrounds.

You might want to buckle up — it’s time to have some fun with Core Graphics!

Getting Started

Click the Download Materials button at the top or bottom of this tutorial to download the starter and finished projects. Open the starter project in the Sample Projects folder and look around. Specifically, check out:

  • Starship.swift: Here, you’ll find the main model for this app — Starship. It’s a simple struct with common properties of Starships. In an extension below, you’ll find the static property all that returns all the starships from the Starships.json file located in the Resources folder.
  • StarshipsViewController.swift: This is the main view controller of the app. It holds an array of all starships and displays them in a table.
  • StarshipDetailViewController.swift: This view controller displays a table of all the fields of the starship the user selected.

Build and run the app.

Note: If you don’t see a simulator available to run the project, you’re probably using an older version of Xcode. You can download Xcode 13 from Apple’s Developer portal.

Starting starship list

The landing page is the StarshipsViewController showing a list of starships from the Star Wars universe. Tap to select the Y-wing, and the app will navigate to the detail view for that ship, which shows an image of a Y-wing followed by various properties such as its cost and speed.

Starting starship detail

This is a fully functional, if pretty boring, app. It’s time to add some bling!

Analyzing the Table View Style

In this tutorial, you’ll add a different style to two table views. Look more closely at what those changes look like.

In StarshipsViewController, each cell:

  • Has a gradient from dark blue to black.
  • Is outlined in yellow, drawn inset from the cell bounds.

Starship list gradient detail

And in StarshipDetailViewController:

  • The table itself has a gradient from dark blue to black.
  • Each cell has a yellow splitter separating it from adjacent cells.

Starship detail gradient detail

To draw both of these designs, you simply need to know how to draw rectangles, gradients and lines with Core Graphics, which is what you’re about to learn. :]

Hello, Core Graphics!

Although this tutorial covers using Core Graphics on iOS, it’s important to know Core Graphics is available for all major Apple platforms, including: macOS via AppKit; iOS and tvOS via UIKit; and on the Apple Watch via WatchKit.

You can think of using Core Graphics like painting on a physical canvas: The order of drawing operations matters. For example, if you draw overlapping shapes, then the last one you add will be on top and overlap the ones below.

Apple designed Core Graphics so you, as a developer, provide instructions on what to draw separate from where to draw it.

Core Graphics Context, represented by the CGContext class, defines the where. You tell the context what drawing operations to do. There are CGContexts for drawing to bit-mapped images, drawing to PDF files and, most commonly, drawing directly into a UIView.

In this painting analogy, the Core Graphics Context represents the painter’s canvas.

Core Graphics Contexts are State Machines. When you set, say, a fill color, you set it for the entire canvas. And any shape you draw will have the same fill color until you change it.

Each UIView has its own Core Graphics Context. To draw the contents of a UIView using Core Graphics, you must write your drawing code within draw(_:) of the view. This is because iOS sets up the correct CGContext for drawing into a view immediately before calling draw(_:).

Now that you understand the basics of how to use Core Graphics within UIKit, it’s time to update your app!

Drawing Rectangles

To get started, create a view file by selecting New ▸ File… from the File menu. Select Cocoa Touch Class, click Next and then set the class name to StarshipListCellBackground. Make it a subclass of UIView, then create the class file. Add the following code to your new class:

override func draw(_ rect: CGRect) {
  // 1
  guard let context = UIGraphicsGetCurrentContext() else {
    return
  }
  // 2  
  context.setFillColor(UIColor.yellow.cgColor)
  // 3
  context.fill(bounds)
}

Breaking this down line by line:

  1. First, you get the current CGContext for this UIView instance, using UIGraphicsGetCurrentContext(). Remember, iOS set this up for you automatically before calling draw(_:). If you can’t get the context for any reason, you return early from the method.
  2. Then, you set the fill color on the context itself.
  3. Finally, you tell it to fill the bounds of the view.

As you can see, the Core Graphics API doesn’t contain a method for directly drawing a shape filled with a color. Instead, a bit like adding paint to a particular brush, you set a color as a state of CGContext and then tell the context what to paint with that color separately.

You also might have also noticed that when you called setFillColor(_:) on the context, you didn’t provide a standard UIColor. Instead, you must use a CGColor, which is the fundamental data type Core Graphics uses internally to represent colors. It’s super easy to convert a UIColor to a CGColor by just accessing the cgColor property of any UIColor.

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.

Yellow cells

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:

  1. 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.
  2. 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.
  3. After that, you create an array with the colors you passed into your method. Notice the use of CFArray, rather than Array, here as you work with the lower level C APIs.
  4. 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?

Leaking blue fill

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()
}

Using SaveGState to stop the blue leaking

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:

  1. 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 as midX and maxY to make this quite simple.
  2. Next, because you’re about to modify the state of the context, you save its graphics state and end the method by restoring it.
  3. 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.

Ugly cell gradient

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.

Fixing the Theme

Open StarshipsViewController.swift. At the end of viewDidLoad(), add the following:

tableView.separatorStyle = .none
tableView.backgroundColor = .starwarsSpaceBlue

Then, in tableView(_:cellForRowAt:), just before returning the cell, set the color of the text:

cell.textLabel?.textColor = .starwarsStarshipGrey

This removes the cell separators and gives the table some nice starship colors.

Next, open AppDelegate.swift and, in application(_:didFinishLaunchingWithOptions:), add the following just before returning:

// Theming
let barAppearance = UINavigationBarAppearance()
barAppearance.configureWithOpaqueBackground()
barAppearance.backgroundColor = .starwarsSpaceBlue
barAppearance.titleTextAttributes = [
  .foregroundColor: UIColor.starwarsStarshipGrey
]

UINavigationBar.appearance().tintColor = .starwarsYellow
UINavigationBar.appearance().barStyle = .black
UINavigationBar.appearance().standardAppearance = barAppearance
UINavigationBar.appearance().scrollEdgeAppearance = barAppearance

This sets the navigation bar’s appearance to match the table’s, using the UINavigationBarAppearance class introduced in iOS 13.

Build and run the app.

Less ugly cell gradient

That’s better! Your starship’s table view is starting to look space age. :]

Stroking Paths

Stroking in Core Graphics means drawing a line along a path, rather than filling it, as you did before.

When Core Graphics strokes a path, it draws the stroke line on the middle of the exact edge of the path. This can cause a couple of common problems.

Outside the Bounds

First, if you’re drawing around the edge of a rectangle — a border, for example — Core Graphics won’t draw half the stroke path by default.

Why? Because the context set up for a UIView extends only to the bounds of the view. Imagine stroking with a one-point border around the edge of a view. Because Core Graphics strokes down the middle of the path, the line will be half a point outside the bounds of the view and half a point inside the bounds of the view.

A common solution is to inset the path for the stroke rect half the width of the line in each direction, so it sits inside the view.

The diagram below shows a yellow rectangle with a red stroke one point wide on a gray background, striped at one-point intervals. In the left diagram, the stroke path follows the bounds of the view and has been cropped. You can see this because the red line is half the width of the gray squares. On the right diagram, the stroke path has been inset half a point and now has the correct line width.

Stroking on the bounds and inset 1/2 point

Anti-Aliasing

Second, you need to be aware of anti-aliasing effects that can affect the appearance of your border. Anti-aliasing is a technique rendering engines use to avoid the appearance of “jagged” edges and lines when displayed graphics don’t map perfectly to physical pixels on a device.

Take the example of a one-point border around a view from the previous paragraph. If the border follows the bounds of the view, then Core Graphics will attempt to draw a line half a point wide on either side of the rectangle.

On a non-Retina display, one point is equal to one pixel on the device. It’s not possible to light up just half of a pixel, so Core Graphics will use anti-aliasing to draw in both pixels, but in a lighter shade to give the appearance of only a single pixel.

In the following sets of screenshots, the left image is a non-Retina display, the middle image is a Retina display with a scale of two and the third image is a Retina display with a scale of three.

For the first diagram, notice how the 2x image doesn’t show any anti-aliasing, as the half point on either side of the yellow rectangle falls on a pixel boundary. However, in the 1x and 3x images, anti-aliasing occurs.

Stroking with different screen scales

In this next set of screenshots, the stroke rect has been inset half a point. Thus, the stroke line aligns exactly with point, and thus pixel, boundaries. Notice how there are no aliasing artifacts.

Stroking with different screen scales after aligning on a pixel boundary

Adding a Border

Back to your app! The cells are starting to look good, but you’re going to add another touch to make them stand out. This time, you’ll draw a bright yellow frame around the edges of the cell.

You already know how to easily fill rectangles. Well, stroking around them is just as easy.

Open StarshipListCellBackground.swift and add the following to the bottom of draw(_:):

let strokeRect = bounds.insetBy(dx: 4.5, dy: 4.5)
context.setStrokeColor(UIColor.starwarsYellow.cgColor)
context.setLineWidth(1)
context.stroke(strokeRect)

Here, you create a rectangle for stroking that’s inset from the background rectangle by 4.5 points in both the x and y directions. Then, you set the stroke color to yellow, the line width to one point and, finally, stroke the rectangle. Build and run your project.

Far far away (bordered cells)

Now, your starship list looks like it comes from a galaxy far, far away!

Building a Card Layout

Although StarshipsViewController is looking fancy, StarshipDetailViewController still needs some sprucing up!

Detail view, starter vs. finished

For this view, you’ll start by drawing a gradient on the table view background, using a custom UITableView subclass.

Create a new Cocoa Touch Class file, make it a subclass of UITableView and call it StarshipTableView. Add the following to the new class:

override func draw(_ rect: CGRect) {
  guard let context = UIGraphicsGetCurrentContext() else {
    return
  }

  context.drawLinearGradient(
    in: bounds, 
    startingWith: UIColor.starwarsSpaceBlue.cgColor, 
    finishingWith: UIColor.black.cgColor)
}

This should look familiar by now. In the draw(_:) method of your new table view subclass, you get the current CGContext, then draw a gradient in the bounds of the view, starting from blue at the top and heading into black at the bottom. Simple!

Open Main.storyboard and click the TableView in the Starship Detail View Controller scene. In the Identity inspector, set the class to your new StarshipTableView.

Using starship table view

Build and run the app, then tap the Y-wing row.

Detail view gradient background

Your detail view now has a nice full-screen gradient running from top to bottom, but the cells in the table view obscure the best parts of the effect. It’s time to fix this and add a bit more flair to the detail cells.

Open StarshipDetailViewController.swift and, at the bottom of tableView(_:cellForRowAt:), just before returning the cell for item field, add the following:

cell.textLabel?.textColor = .starwarsStarshipGrey
cell.detailTextLabel?.textColor = .starwarsYellow
cell.backgroundColor = .clear

This simply sets the cell’s field name and value to more appropriate colors for your Stars Wars theme and sets the background color to clear.

Then, after tableView(_:cellForRowAt:), add the following method to style the table view header:

override func tableView(
  _ tableView: UITableView, 
  willDisplayHeaderView view: UIView, 
  forSection section: Int
) {
  view.tintColor = .starwarsYellow
  if let header = view as? UITableViewHeaderFooterView {
    header.textLabel?.textColor = .starwarsSpaceBlue
  }
}

Here, you set the tint color of the table views’ header’s view to the theme yellow, giving it a yellow background, and its text color to the theme blue.

Drawing Lines

As a final bit of bling, you’ll add a splitter to each cell in the detail view. Open StarshipFieldCell.swift and add the following to the class:

override func draw(_ rect: CGRect) {
  guard let context = UIGraphicsGetCurrentContext() else {
    return
  }

  let y = bounds.maxY - 0.5
  let minX = bounds.minX
  let maxX = bounds.maxX

  context.setStrokeColor(UIColor.starwarsYellow.cgColor)
  context.setLineWidth(1.0)
  context.move(to: CGPoint(x: minX, y: y))
  context.addLine(to: CGPoint(x: maxX, y: y))
  context.strokePath()
}

Here, you use Core Graphics to stroke a line at the bottom of the cell’s bounds. Notice how the y-value used is half-a-point smaller than the bounds of the view to ensure the splitter is drawn fully inside the cell.

Now, you need to actually draw the line showing the splitter.

To draw a line between A and B, you first move to point A, which won’t cause Core Graphics to draw anything. You then add a line to point B, which adds the line from point A to point B into the context. You can then call strokePath() to stroke the line.

Build and run your app. Then, open the Y-wing detail view. Beautiful!

Finished starship detail

Where to Go From Here?

You can download the final project by clicking the Download Materials link at the top or bottom of this tutorial.

The download also includes two playgrounds. RedBluePlayground.playground contains the example set out in the context saving/restoring section, and ClippedBorderedView.playground demonstrates clipping a border unless it’s inset.

Additionally, DemoProject is a complete Xcode project, which strokes a rect over a one-point grid to help you understand the concept of anti-aliasing. It’s easy to understand now that you know the Core Graphics Swift API. :]

At this point, you should be familiar with some pretty cool and powerful techniques in Core Graphics: filling and stroking rectangles, drawing lines and gradients and clipping to paths. Not to mention your table view now looks pretty cool. Congratulations!

If this tutorial was a little hard to follow, or you want to make sure to cover your basics, check out the Beginning Core Graphics video series.

If you’re looking for something more advanced, take a look at the Intermediate Core Graphics course.

And if you don’t feel like you can commit to an entire course yet, try the Core Graphics Article Series, where you’ll learn how to draw an entire app, including graphs, from scratch with Core Graphics!

Plus, there are many more Core Graphics tutorials on raywenderlich.com.

If you have any questions or comments, please join the forum discussion below.

Average Rating

5/5

Add a rating for this content

1 rating

More like this

Contributors

Comments