Core Graphics Tutorial: Curves and Layers

In this tutorial, you will learn the Core Graphics drawing model and how it dictates the order that you draw your shapes. By Sanket Firodiya.

Leave a rating/review
Download materials
Save for later
Share
Update note: Sanket Firodiya updated this tutorial for Xcode 10, iOS 12 and Swift 4.2. Ray Wenderlich wrote the original.

In this tutorial, you’ll learn how to draw on the screen using Core Graphics. You’ll learn how to draw Quadratic and Bezier curves as well as apply transforms to existing shapes. Finally, you’ll use Core Graphics layers to clone your drawings with the ease and style of a James Bond super-villain. :]

There’s a lot to cover, so make yourself comfortable, crack those knuckles and fire up Xcode. It’s time to do some drawing.

Getting Started

Start by downloading the starter project using the Download Materials button at the top or bottom of this tutorial. Once downloaded, open RageTweet.xcworkspace (not the .xcodeproj!) in Xcode.

Build and run the app. You should see the following:

In this tutorial, you’ll replace the flat blue background color with a scenic mountain background. With each change of emotions, the sky will turn a different color to represent that state.

There’s one catch — there is no source Photoshop file. This isn’t a case of exporting different background PNG files for each emotion. You’ll draw it all from scratch! :]

This is how the final output will look:

Go back to the app and swipe through the different faces and prepare to be amazed. Touch a face to send a Tweet.

Note: This project makes use of Twitter Kit to send tweets. Since iOS 11, support for Twitter through the built-in social framework has been deprecated. To learn more about how to migrate from the Social Framework and adopt Twitter Kit in your apps, check out their excellent guide on this topic. You’ll need to test on a device to be able to send Tweets, since Twitter Kit does not support sending Tweets from the simulator.

Core Graphics’ Painter’s Model

Before writing even one drawing command, it’s important you understand how things are drawn to the screen. Core Graphics utilizes a drawing model called a painter’s model. In a painter’s model, each drawing command is on top of the previous one.

This is similar to painting on an actual canvas.

When painting, you might first paint a blue sky on the canvas. When the paint has finished drying, you might paint some clouds in the sky. As you paint the clouds, the original blue color behind the clouds is obscured by fresh white paint. Next, you might paint some shadows on the clouds, and now some of the white paint is obscured by a darker paint, giving the cloud some definition.

Here’s an image straight out of Apple’s developer documentation that illustrates this idea:

Core Graphics Painting Model

In summary, the drawing model determines the drawing order you must use.

An Image Is Worth a Thousand Words

It’s time to start drawing. Open SkyView.swift and add the following method within the class:

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

  let colorSpace = CGColorSpaceCreateDeviceRGB()

  //  drawSky(in: rect, context: context, colorSpace: colorSpace)
  //  drawMountains(in: rect, in: context, with: colorSpace)
  //  drawGrass(in: rect, in: context, with: colorSpace)
  //  drawFlowers(in: rect, in: context, with: colorSpace)
}

In this method, you get the context of the current graphics and create a standard color space for the device. The comments represent future drawing calls to draw the sky, mountains, grass and flowers.

Drawing the Sky

You’ll use a three color gradient to draw the sky. After draw(_:), add the following code to do this:

private func drawSky(in rect: CGRect, context: CGContext, colorSpace: CGColorSpace?) {
  // 1
  context.saveGState()
  defer { context.restoreGState() }

  // 2
  let baseColor = UIColor(red: 148.0 / 255.0, green: 158.0 / 255.0, 
                          blue: 183.0 / 255.0, alpha: 1.0)
  let middleStop = UIColor(red: 127.0 / 255.0, green: 138.0 / 255.0, 
                           blue: 166.0 / 255.0, alpha: 1.0)
  let farStop = UIColor(red: 96.0 / 255.0, green: 111.0 / 255.0, 
                        blue: 144.0 / 255.0, alpha: 1.0)

  let gradientColors = [baseColor.cgColor, middleStop.cgColor, farStop.cgColor]
  let locations: [CGFloat] = [0.0, 0.1, 0.25]

  guard let gradient = CGGradient(
    colorsSpace: colorSpace, 
    colors: gradientColors as CFArray, 
    locations: locations) 
    else {
      return
  }

  // 3
  let startPoint = CGPoint(x: rect.size.height / 2, y: 0)
  let endPoint = CGPoint(x: rect.size.height / 2, y: rect.size.width)
  context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: [])
}

Here’s what this code does:

  1. First of all, save the graphics state. You should always do this when you’re about to do some drawing. Also make sure to restore the state when you finish drawing. You can do this when the method exits by using defer.
  2. Set up the colors, locations and finally the actual gradient itself.
  3. Draw the gradient.

If this code is unfamiliar to you, check out this Core Graphics tutorial, which covers gradients.

Next, in draw(_:), uncomment the call to drawSky(in:context:colorSpace:).

Build and run the app. Now you should see the following:

You’ll notice that the actual gradient occurs near the top of the rectangle as opposed to being evenly applied throughout the whole of it.

This is the actual sky portion of the drawing. The lower half will be obscured by subsequent drawing calls.

With the sky complete, it’s now time to draw the mountains.

Getting Comfortable with Curves

Take a look at the source drawing and observe the mountains. While you can certainly produce the same effect by drawing a series of arcs, a much easier method is to use curves.

There are two kinds of curves available in Core Graphics. One is called a Quadratic Curve and its big brother is known as a Bezier Curve. These curves are based on mathematical principles that allow them to infinitely scale, no matter the resolution.

Quadratic Curves and Bezier Curves

If looking at that diagram makes your stomach twist into knots, take a deep breath followed by a shot of whiskey. :]

Now look at it again. Feel better? No? All right. Have another shot and realize this… you do not need to know any mathematical concepts to draw these beasts.

In practice, these curves are actually quite easy to draw. Like any line, you first need to know a starting point and an ending point. Then, you add a control point.

A control point essentially dictates the curve of the line. The closer you place the control point to the line, the less dramatic the curve. By placing the control point farther away from the line, the more pronounced the curve. Think of a control point as a little magnet that pulls the line towards it.

On a practical level, the main difference between a Quadratic Curve and a Bezier Curve is the number of control points. A Quadratic curve has one control point. A Bezier curve has two. That’s about it.

Note: Another good way to develop an intuitive understanding of how control points affect Bezier curves is to spend some time playing around with them at http://cubic-bezier.com

In SkyView.swift, just underneath drawSky(in:context:colorSpace:), add this new method:

private func drawMountains(in rect: CGRect, in context: CGContext, 
                   with colorSpace: CGColorSpace?) {
  let darkColor = UIColor(red: 1.0 / 255.0, green: 93.0 / 255.0, 
                          blue: 67.0 / 255.0, alpha: 1)
  let lightColor = UIColor(red: 63.0 / 255.0, green: 109.0 / 255.0, 
                           blue: 79.0 / 255.0, alpha: 1)
  let rectWidth = rect.size.width

  let mountainColors = [darkColor.cgColor, lightColor.cgColor]
  let mountainLocations: [CGFloat] = [0.1, 0.2]
  guard let mountainGrad = CGGradient.init(colorsSpace: colorSpace, 
        colors: mountainColors as CFArray, locations: mountainLocations) else {
    return
  }

  let mountainStart = CGPoint(x: rect.size.height / 2, y: 100)
  let mountainEnd = CGPoint(x: rect.size.height / 2, y: rect.size.width)

  context.saveGState()
  defer { context.restoreGState() }

  // More coming 1...
}

This code sets up the basis of the method, where more code will follow shortly. It creates some colors and points that you’ll use later.

As you can see from the source diagram, the mountains start out a deep green and subtly change to a light tanish color. Now, it’s time to draw the actual curves. You’ll start with a Quadratic curve.

Within the same method, replace // More coming 1... with the following:

let backgroundMountains = CGMutablePath()
backgroundMountains.move(to: CGPoint(x: -5, y: 157), transform: .identity)
backgroundMountains.addQuadCurve(to: CGPoint(x: 77, y: 157), 
                                 control: CGPoint(x: 30, y: 129), 
                                 transform: .identity)

// More coming 2...

The first thing you’re doing here is creating a path object. You add a single quadratic curve to the path. The move(to:transform:) call sets the starting point of the line. The next bit is where all the magic happens.

backgroundMountains.addQuadCurve(to: CGPoint(x: 77, y: 157), 
  control: CGPoint(x: 30, y: 129), transform: .identity)
  • The first argument is a CGPoint with the values 77 and 157 for x and y, respectively. This indicates the position for the end of the line.
  • The next argument is a CGPoint with the values 30 and 129 for x and y, respectively. This indicates the position of the control point.
  • The last argument is an affine transform. For example, if you wanted to apply a rotation or scale the curve, you would supply the transformation here. You’ll be using such transforms later.

In short, you now have a quadratic curve.

To see this in action, replace the // More coming 2... with the following:

// Background Mountain Stroking
context.addPath(backgroundMountains)
context.setStrokeColor(UIColor.black.cgColor)
context.strokePath()

// More coming 3...

This draws the path, in black, on the graphics context.

Next, in draw(_:), uncomment the call to drawMountains(in:in:with:).

Now, build and run the app. You should see the following:

You’ve created a nice little curve here. It’s not the Mona Lisa, but it’s a start.

Now it’s time to tackle a Bezier curve. Back in drawMountains(in:in:with:), underneath backgroundMountains.addQuadCurve..., add the following:

backgroundMountains.addCurve(to: CGPoint(x: 303, y: 125), 
                             control1: CGPoint(x: 190, y: 210), 
                             control2: CGPoint(x: 200, y: 70), 
                             transform: .identity)

The big difference between this call and the previous call is the addition of another set of x and y points for the next control point. This one is a bezier curve rather than a quadratic curve.

  • The first argument is a CGPoint with the values 303 and 125 for x and y, respectively. This indicates the end of the line.
  • The second argument is a CGPoint with the values 190 and 210 for x and y, respectively. This indicates the position of the first control point.
  • The third argument is a CGPoint with the values 200 and 70 for x and y, respectively. This indicates the position of the second control point.
  • Like before, backgroundMountains is a CGPath, and you’re applying the default identity transform to it.

Build and run the app and you should now see the following:

The key thing to remember about curves is that the more you use them, the easier it is to determine the placement of the control points for your desired curves.

Now it’s time to complete the first set of mountains. Add the following code right under the line you just added:

backgroundMountains.addQuadCurve(to: CGPoint(x: 350, y: 150), 
                                 control: CGPoint(x: 340, y: 150), 
                                 transform: .identity)
backgroundMountains.addQuadCurve(to: CGPoint(x: 410, y: 145), 
                                 control: CGPoint(x: 380, y: 155), 
                                 transform: .identity)
backgroundMountains.addCurve(to: CGPoint(x: rectWidth, y: 165), 
                             control1: CGPoint(x: rectWidth - 90, y: 100), 
                             control2: CGPoint(x: rectWidth - 50, y: 190), 
                             transform: .identity)
backgroundMountains.addLine(to: CGPoint(x: rectWidth - 10, y: rect.size.width),
                            transform: .identity)
backgroundMountains.addLine(to: CGPoint(x: -5, y: rect.size.width), 
                            transform: .identity)
backgroundMountains.closeSubpath()

// Background Mountain Drawing
context.addPath(backgroundMountains)
context.clip()
context.drawLinearGradient(mountainGrad, start: mountainStart, 
                           end: mountainEnd, options: [])
context.setLineWidth(4)

This finishes the mountains with a few more curves that extend beyond the length of the device. It also adds in drawing of the gradient for the mountains.

Build and run the app. It should look like the following:

Now, add some foreground mountains. Replace // More coming 3... with the following code:

// Foreground Mountains
let foregroundMountains = CGMutablePath()
foregroundMountains.move(to: CGPoint(x: -5, y: 190), 
                         transform: .identity)
foregroundMountains.addCurve(to: CGPoint(x: 303, y: 190), 
                             control1: CGPoint(x: 160, y: 250), 
                             control2: CGPoint(x: 200, y: 140), 
                             transform: .identity)
foregroundMountains.addCurve(to: CGPoint(x: rectWidth, y: 210), 
                             control1: CGPoint(x: rectWidth - 150, y: 250), 
                             control2: CGPoint(x: rectWidth - 50, y: 170), 
                             transform: .identity)
foregroundMountains.addLine(to: CGPoint(x: rectWidth, y: 230), 
                            transform: .identity)
foregroundMountains.addCurve(to: CGPoint(x: -5, y: 225), 
                             control1: CGPoint(x: 300, y: 260), 
                             control2: CGPoint(x: 140, y: 215), 
                             transform: .identity)
foregroundMountains.closeSubpath()

// Foreground Mountain drawing
context.addPath(foregroundMountains)
context.clip()
context.setFillColor(darkColor.cgColor)
context.fill(CGRect(x: 0, y: 170, width: rectWidth, height: 90))

// Foreground Mountain stroking
context.addPath(foregroundMountains)
context.setStrokeColor(UIColor.black.cgColor)
context.strokePath()

This adds some more mountains in front of the ones you just added.

Now build and run the app. You should see the following:

With just a few curves and a splash of gradients, you can already construct a nice looking background!