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.
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: How to Make a Glossy Button
30 mins
- Getting Started
- Configuring the Button’s UI
- Making Your Button Functional
- Drawing Rounded Rectangles
- Using the CGContextAddArcToPoint API
- Drawing Your Arcs
- Making Your Button Rounded
- Adding a Gradient
- Constraining a Gradient to a Sub-area
- Adding a Gloss Effect
- Styling the Button
- Highlighting the Button
- Where to Go From Here?
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:
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 UIButton
s.
The code also instructs createRoundedRectPath(for:radius:)
to draw the rounded rect in the following order:
Here’s the breakdown of what’s going on in the code:
- You move to the center of the top line segment.
- You declare each corner as a local constant.
- You add each arc to the path:
- 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. - Similarly, you add an arc for the lower right corner and the connecting line.
- Then you add an arc for the lower left corner and the connecting line.
- Last, you add an arc for the upper left corner and the connecting line.
- First, you add an arc for the upper right corner. Before drawing an arc,
- Finally, you connect the ending point of the arc with the starting point with
closeSubpath()
.
- 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. - Similarly, you add an arc for the lower right corner and the connecting line.
- Then you add an arc for the lower left corner and the connecting line.
- 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:
- You define your two colors.
- 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. - Next, you call the function you just wrote,
createRoundedRectPath(for:radius:)
, to create a path for your rounded rect. - Finally, you set the fill color and shadow, add the path to your context and call
fillPath()
to fill it with your current color.
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!
- The first thing you need is to get a color space that you’ll use to draw the gradient.
- 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.
- 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. - Then you create your gradient with
CGGradient(colorsSpace:colors:locations:)
, passing in the color space, color array and locations you previously made.
CGColorSpaceCreateDeviceRGB()
to get the reference that you need.CGColorSpaceCreateDeviceRGB()
to get the reference that you need.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()
- 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:)
.