Core Graphics: How to Make a Glossy Button

In this tutorial, you’ll learn how to create a customizable, reusable glossy button using only Core Graphics. By Lea Marolt Sonnenschein.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Drawing Rounded Rectangles

It’s true that you can easily create square buttons, but button styles come and go faster than the weather changes in Chicago!

In fact, since we originally released this tutorial, square buttons and rounded rectangle buttons have flip-flopped back and forth for the number-one spot in the button pageant, so it’s a good idea to know how to make both versions.

You can also argue that it’s quite easy to create rounded rectangles by simply changing the corner radius of a UIView, but where’s the fun in that? There’s so much more gratification, or maybe madness, in doing it the hard way. :]

One way to make rounded rectangles is to draw arcs using the CGContextAddArc API. Using that API, you can draw an arc at each corner and draw lines to connect them. But that’s cumbersome and requires a lot of geometry.

Fortunately, there’s an easier way! You don’t have to do as much math and it works well with drawing rounded rectangles. It’s the CGContextAddArcToPoint API.

Using the CGContextAddArcToPoint API

The CGContextAddArcToPoint API lets you describe the arc to draw by specifying two tangent lines and a radius. The following diagram from the Quartz2D Programming Guide shows how it works:

CGContextAddArcToPoint Diagram

When you’re working with a rectangle, you know the tangent lines for each arc you want to draw — they are simply the edges of the rectangle! And you can specify the radius based on how rounded you want the rectangle to be — the larger the arc, the more rounded the corners will be.

The other neat thing about this function is that, if the current point in the path isn’t set to where you tell the arc to begin drawing, it will draw a line from the current point to the beginning of the path. So you can use this as a shortcut to draw a rounded rectangle in just a few calls.

Drawing Your Arcs

Since you’re going to create a bunch of rounded rectangles in this Core Graphics tutorial, and you want your code to be as reusable as possible, create a separate file for all of your drawing methods.

Go to File ▸ New ▸ File…, and choose iOS ▸ Swift File. Press Next, call it Drawing and click Create.

Now, replace import Foundation with the following:

import UIKit
import CoreGraphics

extension UIView {
  func createRoundedRectPath(for rect: CGRect, radius: CGFloat) -> CGMutablePath {
    let path = CGMutablePath()
    
    // 1
    let midTopPoint = CGPoint(x: rect.midX, y: rect.minY)
    path.move(to: midTopPoint)
    
    // 2
    let topRightPoint = CGPoint(x: rect.maxX, y: rect.minY)
    let bottomRightPoint = CGPoint(x: rect.maxX, y: rect.maxY)
    let bottomLeftPoint = CGPoint(x: rect.minX, y: rect.maxY)
    let topLeftPoint = CGPoint(x: rect.minX, y: rect.minY)
    
    // 3
    path.addArc(tangent1End: topRightPoint, 
      tangent2End: bottomRightPoint, 
      radius: radius)

    path.addArc(tangent1End: bottomRightPoint, 
      tangent2End: bottomLeftPoint, 
      radius: radius)

    path.addArc(tangent1End: bottomLeftPoint,
      tangent2End: topLeftPoint, 
      radius: radius)

    path.addArc(tangent1End: topLeftPoint,
      tangent2End: topRightPoint, 
      radius: radius)

    // 4
    path.closeSubpath()
    
    return path
  }
}

The code above creates a global extension for everything of type UIView, so you can use it for more than just UIButtons.

The code also instructs createRoundedRectPath(for:radius:) to draw the rounded rect in the following order:

How to draw a rounded rect with Core Graphics

Here’s the breakdown of what’s going on in the code:

  1. You move to the center of the top line segment.
  2. You declare each corner as a local constant.
  3. You add each arc to the path:
    1. First, you add an arc for the upper right corner. Before drawing an arc, CGPathAddArcToPoint will draw a line from the current position in the middle of the rect to the beginning of the arc for you.
    2. Similarly, you add an arc for the lower right corner and the connecting line.
    3. Then you add an arc for the lower left corner and the connecting line.
    4. Last, you add an arc for the upper left corner and the connecting line.
  4. Finally, you connect the ending point of the arc with the starting point with closeSubpath().
  1. First, you add an arc for the upper right corner. Before drawing an arc, CGPathAddArcToPoint will draw a line from the current position in the middle of the rect to the beginning of the arc for you.
  2. Similarly, you add an arc for the lower right corner and the connecting line.
  3. Then you add an arc for the lower left corner and the connecting line.
  4. Last, you add an arc for the upper left corner and the connecting line.

Making Your Button Rounded

OK, now it’s time to put this method to work! Open CoolButton.swift and replace draw(_:) with the following:

override func draw(_ rect: CGRect) {
  guard let context = UIGraphicsGetCurrentContext() else {
    return
  }
  
  // 1
  let outerColor = UIColor(
    hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
  let shadowColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.5)
  
  // 2
  let outerMargin: CGFloat = 5.0
  let outerRect = rect.insetBy(dx: outerMargin, dy: outerMargin)
  // 3
  let outerPath = createRoundedRectPath(for: outerRect, radius: 6.0)
  
  // 4
  if state != .highlighted {
    context.saveGState()
    context.setFillColor(outerColor.cgColor)
    context.setShadow(offset: CGSize(width: 0, height: 2), 
      blur: 3.0, color: shadowColor.cgColor)
    context.addPath(outerPath)
    context.fillPath()
    context.restoreGState()
  }
}

To break this down:

  1. You define your two colors.
  2. Then you use insetBy(dx:dy:) to get a slightly smaller rectangle (5 pixels on each side) where you’ll draw the rounded rect. You’ve made it smaller so that you’ll have space to draw a shadow on the outside.
  3. Next, you call the function you just wrote, createRoundedRectPath(for:radius:), to create a path for your rounded rect.
  4. Finally, you set the fill color and shadow, add the path to your context and call fillPath() to fill it with your current color.
Note: You only want to run the code if your button isn’t currently highlighted; for example, it isn’t being tapped.

Build and run the app; if all works well, you should see the following:

Adding a Gradient

All right, the button’s starting to look pretty good, but you can do even better! How about adding a gradient?

Add the following function to Drawing.swift, to make it universally available for any UIView:

func drawLinearGradient(
  context: CGContext, rect: CGRect, startColor: CGColor, endColor: CGColor) {
  // 1
  let colorSpace = CGColorSpaceCreateDeviceRGB()
  
  // 2
  let colorLocations: [CGFloat] = [0.0, 1.0]
  
  // 3
  let colors: CFArray = [startColor, endColor] as CFArray
  
  // 4
  let gradient = CGGradient(
    colorsSpace: colorSpace, colors: colors, locations: colorLocations)!

  // More to come...
}

It doesn’t look like much, but there’s a lot going on in this function!

  1. The first thing you need is to get a color space that you’ll use to draw the gradient.
  2. Note: There’s a lot you can do with color spaces, but 99% of the time you just want a standard device-dependent RGB color space. So simply use the function CGColorSpaceCreateDeviceRGB() to get the reference that you need.
  3. 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, 1 means the end of the gradient. You only have two colors, and you want the first to be at the start and the second to be at the end, so you pass in 0 and 1.
  4. 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. This can be useful for certain effects.
  5. After that, you create an array with the colors that you passed into your function. You use a plain old array here for convenience, but you need to cast it as a CFArray, since that’s what the API requires.
  6. Then you create your gradient with CGGradient(colorsSpace:colors:locations:), passing in the color space, color array and locations you previously made.
Note: There’s a lot you can do with color spaces, but 99% of the time you just want a standard device-dependent RGB color space. So simply use the function CGColorSpaceCreateDeviceRGB() to get the reference that you need.
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. This can be useful for certain effects.

You now have a gradient reference, but it hasn’t actually drawn anything yet. It’s just a pointer to the information you will use when actually drawing it later.

Complete the function by adding the following at the end of drawLinearGradient(context:rect:startColor:endColor:):

// 5
let startPoint = CGPoint(x: rect.midX, y: rect.minY)
let endPoint = CGPoint(x: rect.midX, y: rect.maxY)

context.saveGState()

// 6
context.addRect(rect)
// 7
context.clip()

// 8
context.drawLinearGradient(
  gradient, start: startPoint, end: endPoint, options: [])

context.restoreGState()
  1. The first thing you do is calculate the start and end point where you want to draw the gradient. You just set this as a line from the “top middle” to the “bottom middle” of the rectangle.

The rest of the code helps you draw a gradient into the provided rectangle, the key function being drawLinearGradient(_:start:end:options:).