Core Graphics Tutorial: Lines, Rectangles, and Gradients


Update note: Tom Elliott updated this tutorial for iOS 12, Xcode 10 and Swift 4.2. Ray Wenderlich wrote the original.

This is the first in a series of Core Graphics tutorials that will to take the mystery out of Core Graphics. You’ll learn the APIs step by step with practical exercises, starting by beautifying table views with Core Graphics.

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

With a history that dates back to the very early days of OS X, Core Graphics is one of the oldest APIs still in use 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 is comprised of a master view that contains 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, how to fill and stroke rectangles and how to 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

Use the Download Materials button at the top or bottom of this tutorial to download the starter and finished projects. Open the starter project and have a quick look around. The app is based on the Master-Detail App template provided by Xcode. The master view controller contains a list of Star Ships and the detail view controller shows details for each ship.

Open MasterViewController.swift. At the top of the class, notice a starships variable, which contains an array of type Starship and a dataProvider variable of type StarshipDataProvider.

Jump into StarshipDataProvider.swift by Command-clicking StarshipDataProvider and selecting Jump to Definition. This is a simple class that reads a bundled file, Starships.json, and converts the contents into an array of Starship.

You can find the definition for a Starship in Starship.swift. It is just a simple struct with properties for common properties of Starships.

Next, open DetailViewController.swift. Defined at the top of the file before the class definition is an enum, FieldsToDisplay, which defines the human readable titles for the Starship properties you want to display as the cases in the enum. In this file, tableView(_:cellForRowAt:) is just a big switch statement for formatting the data for each Starship property into the right format.

Build and run the app.

Starting starship list

The landing page is the MasterViewController showing a list of Starships from the Star Wars universe. Tap to select the X-wing and the app will navigate to the detail view for that ship, which shows an image of an X-wing followed by various properties like how much it costs and how fast it can fly.

Starting starship detail

This is a fully functional, if pretty boring, app. Time to add some bling!

Analyzing the Table View Style

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

In the master view controller, 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 the detail view controller:

  • 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 just need to know how to draw rectangles, gradients and lines with Core Graphics, which is exactly what you’re about to learn. :]

Hello, Core Graphics!

While this tutorial covers using Core Graphics on iOS, it’s important to know that 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 ordering of drawing operations matters. If you draw overlapping shapes, for example, then the last one you add will be on top and overlap the ones below.

Apple architected Core Graphics in such a way that you, as a developer, provide instructions on the what to draw in a separate moment than the where.

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 canvas where the painter paints.

Core Graphics Contexts are State Machines. That is, when you set, say, a fill color you set it for the entire canvas and any shapes 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 prior to calling draw(_:).

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

Drawing Rectangles

To get started, create a new view file by selecting New ▸ File… from the File menu. Select Cocoa Touch Class, press Next and then set the class name to StarshipsListCellBackground. 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.red.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 prior to its calling draw(_:). If you cannot 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 the CGContext and then, you tell the context what to paint with that color separately.

You 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 used internally by Core Graphics 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 MasterViewController.swift. In tableView(_:cellForRowAt:), add the following code immediately after dequeuing the cell in the first line of the method:

if !(cell.backgroundView is StarshipsListCellBackground) {
  cell.backgroundView = StarshipsListCellBackground()
}
    
if !(cell.selectedBackgroundView is StarshipsListCellBackground) {
  cell.selectedBackgroundView = StarshipsListCellBackground()
}

This code sets the cells’ background view to be that of your new view. Build and run the app, and you will see a lovely, if garish, red background in every cell.

Red cells

Amazing! You can now draw with Core Graphics. And believe it or not, you’ve already learned a bunch of really 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 pretty nice UI with just that.

But you’re going to take it a step further and learn about one of the most useful 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 UIColorExtensions.swift. 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, since you’re going to draw a lot of gradients in this project, add a helper method for drawing gradients. This will simplify the project by keeping the gradient code in one place and avoid having to repeat yourself.

Select File ▸ New ▸ File… and create a new Swift File called CGContextExtensions.swift. Replace the contents of the file with the following:

import UIKit

extension CGContext {
  func drawLinearGradient(
    in rect: CGRect, 
    startingWith startColor: CGColor, 
    finishingWith endColor: CGColor
  ) {
    // 1
    let colorSpace = CGColorSpaceCreateDeviceRGB()

    // 2
    let locations = [0.0, 1.0] as [CGFloat]    

    // 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. There’s a lot you can do 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 that you passed into your method. Notice the use of CFArray, rather than Array, here as you are working 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 that Core Graphics Contexts are state machines. You have to be careful when setting state on a context, especially within functions that you pass a context or, as in this case, methods on the context itself, as you cannot 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 may think that 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 out for yourself 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 complete 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()
  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 variables like midX and maxY to make this really simple.
  2. Next, since 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, then, 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’ll set the provided rectangle on the context and clip before finally calling drawLinearGradient(_:start:end:options:) to draw the gradient.

Time to give this method a whirl! Open StarshipsListCellBackground.swift and, after getting the current UIGraphicsContext, replace the code with the following:

let backgroundRect = bounds
context.drawLinearGradient(
  in: backgroundRect, 
  startingWith: UIColor.starwarsSpaceBlue.cgColor, 
  finishingWith: UIColor.black.cgColor
)

Build and run the app.

Ugly cell gradient

You have now successfully 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. Time to fix that with some standard UIKit theming.

Fixing the Theme

Open Main.storyboard and select the table view in the Master scene. In the Attributes inspector, set Separator to None.

Plain table view

Then, select the Navigation Bar in the Master Navigation Controller scene and set the Navigation Bar Style to Black and deselect Translucent. Repeat for the Navigation Bar in the Detail Navigation Controller scene.

Black navigation bar

Next, open MasterViewController.swift. At the end of viewDidLoad(), add the following:

tableView.backgroundColor = .starwarsSpaceBlue

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

cell.textLabel!.textColor = .starwarsStarshipGrey

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

// Theming
UINavigationBar.appearance().tintColor = .starwarsYellow
UINavigationBar.appearance().barTintColor = .starwarsSpaceBlue
UINavigationBar.appearance().titleTextAttributes = 
  [.foregroundColor: UIColor.starwarsStarshipGrey]

Build and run the app.

Less ugly cell gradient

That’s better! Your master table view is starting to look very 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 are 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 only extends 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 that it sits inside the view.

The diagram below shows a yellow rectangle with a red stroke one point wide on a grey background, which is 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 grey 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, if you are unfamiliar with what it is (even if you may have heard about it on a computer game settings screen!), is a technique rendering engines use to avoid “jagged” appearances of edges and lines when graphics being displayed 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 is not possible to light up just a 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 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 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, such that 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 really make them stand out. This time, you’re going to 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 StarshipsListCellBackground.swift and add the following to the bottom of draw(_:):

let strokeRect = backgroundRect.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 is 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 really looks like it comes from a galaxy far, far away!

Building a Card Layout

While your master view controller is looking fancy, the detail view controller is still in need of some sprucing up!

Detail view, starter vs finished

For this view, you are going to start by drawing a gradient on the table view background by using a custom UITableView subclass.

Create a new Swift File called StarshipTableView.swift. Replace the generated code with the following:

import UIKit

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

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

This should be starting to 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 on the TableView in the Detail scene. In the Identity inspector, set the class to your new StarshipTableView.

Using starship table view

Build and run the app, then tap the X-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. Time to fix this and add a bit more flair to the detail cells.

Back in Main.storyboard, select FieldCell in the Detail Scene. In the Attributes inspector, set the background to Clear Color. Next, open DetailViewController.swift and, at the very bottom of tableView(_:cellForRowAt:), just before returning the cell, add the following:

cell.textLabel!.textColor = .starwarsStarshipGrey
cell.detailTextLabel!.textColor = .starwarsYellow

This simply sets the cells’ field name and value to more appropriate colors for your Stars Wars theme.

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’re setting 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. Create a new Swift file, this time called YellowSplitterTableViewCell.swift. Replace the generated code with the following:

import UIKit

class YellowSplitterTableViewCell: UITableViewCell {
  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()
  }
}

In YellowSplitterTableVIewCell, you are using Core Graphics to stroke a line at the bottom of the cells’ 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.

Finally, open Main.storyboard again and set the class of the FieldCell in the Detail scene to be your newly created YellowSplitterTableViewCell using the Identity inspector. Build and run your app. Then, open the X-wing detail view. Beautiful!

Finished starship detail

Where to Go From Here?

You can download the final project using 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 full Xcode project which strokes a rect over a one point grid. This project is written in Objective-C so you can run it without modification on non-retina devices like the iPad 2, which require iOS 9, to see the anti-aliasing effects for yourself. But don’t panic! It’s easy to understand now you know the Core Graphics Swift API. :]

At this point, you should be familiar with some pretty cool and powerful techniques with 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 a full 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, all recently updated for Xcode 10, on the site.

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



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *