Swift Core Graphics
Introduction
Core Graphics is a powerful framework provided by Apple that allows developers to create custom drawings, shapes, and graphics in iOS applications. It's a low-level, high-performance 2D rendering engine that enables pixel-perfect control over the visual elements in your app.
In this guide, you'll learn the fundamentals of Core Graphics in Swift, how to create various shapes and patterns, and how to implement practical drawing functionality in your iOS applications. Core Graphics is an essential skill for iOS developers who want to create visually appealing and customized user interfaces.
Core Graphics Fundamentals
The Graphics Context
The foundation of Core Graphics is the graphics context, which is essentially a canvas where all drawing takes place. Every drawing operation in Core Graphics is performed within a context.
// Creating a custom view with Core Graphics drawing
class DrawingView: UIView {
override func draw(_ rect: CGRect) {
// Get the current graphics context
guard let context = UIGraphicsGetCurrentContext() else { return }
// Drawing code goes here
}
}
Basic Drawing Concepts
Coordinate System
Core Graphics uses a coordinate system where (0,0) is at the bottom-left corner, with the y-axis pointing upward. However, in UIKit, (0,0) is at the top-left corner with the y-axis pointing downward. This is an important distinction to remember!
Paths and Strokes
A path defines a shape that you want to draw. After creating a path, you can stroke it (draw its outline) or fill it (color its interior).
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Create a path
let path = CGMutablePath()
path.addRect(CGRect(x: 50, y: 50, width: 200, height: 100))
// Add the path to the context
context.addPath(path)
// Set the stroke color to blue
context.setStrokeColor(UIColor.blue.cgColor)
context.setLineWidth(5)
// Stroke the path
context.strokePath()
}
Result: This code creates a blue rectangular outline with a width of 200 points, height of 100 points, positioned 50 points from the left and top edges of the view.
Drawing Basic Shapes
Rectangles
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Draw a filled rectangle
context.setFillColor(UIColor.red.cgColor)
context.fill(CGRect(x: 20, y: 20, width: 100, height: 100))
// Draw a rectangle with both fill and stroke
context.setFillColor(UIColor.green.cgColor)
context.setStrokeColor(UIColor.black.cgColor)
context.setLineWidth(3)
let rectangle = CGRect(x: 150, y: 20, width: 100, height: 100)
context.addRect(rectangle)
context.drawPath(using: .fillStroke)
}
Result: This draws a red filled square and a green square with a black border.
Circles and Ellipses
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Draw a circle
context.setFillColor(UIColor.purple.cgColor)
context.fillEllipse(in: CGRect(x: 50, y: 50, width: 100, height: 100))
// Draw an ellipse with a stroke
context.setStrokeColor(UIColor.orange.cgColor)
context.setLineWidth(5)
context.strokeEllipse(in: CGRect(x: 200, y: 50, width: 150, height: 80))
}
Result: This creates a purple filled circle and an orange ellipse outline.
Lines and Curves
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Draw a line
context.setStrokeColor(UIColor.blue.cgColor)
context.setLineWidth(2)
context.move(to: CGPoint(x: 20, y: 20))
context.addLine(to: CGPoint(x: 300, y: 200))
context.strokePath()
// Draw a curve
context.setStrokeColor(UIColor.green.cgColor)
context.move(to: CGPoint(x: 20, y: 250))
context.addCurve(to: CGPoint(x: 300, y: 250),
control1: CGPoint(x: 100, y: 150),
control2: CGPoint(x: 220, y: 350))
context.strokePath()
}
Result: This draws a blue straight line and a green curved line (Bezier curve).
Custom Paths and Shapes
Creating Complex Paths
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Start a new path
context.beginPath()
// Move to starting point
context.move(to: CGPoint(x: 100, y: 100))
// Add line segments
context.addLine(to: CGPoint(x: 200, y: 100))
context.addLine(to: CGPoint(x: 150, y: 200))
context.addLine(to: CGPoint(x: 100, y: 100))
// Close the path
context.closePath()
// Set fill and stroke colors
context.setFillColor(UIColor.yellow.cgColor)
context.setStrokeColor(UIColor.black.cgColor)
context.setLineWidth(3)
// Fill and stroke the path
context.drawPath(using: .fillStroke)
}
Result: This creates a yellow triangle with a black border.
Drawing a Star
Let's create a more complex example - drawing a five-pointed star:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let center = CGPoint(x: rect.width/2, y: rect.height/2)
let radius: CGFloat = min(rect.width, rect.height) / 2 - 20
let starPoints = 5
// Calculate points of the star
var points = [CGPoint]()
for i in 0..<starPoints * 2 {
let radius = i % 2 == 0 ? radius : radius * 0.4
let angle = CGFloat(i) * .pi / CGFloat(starPoints)
let x = center.x + radius * sin(angle)
let y = center.y - radius * cos(angle)
points.append(CGPoint(x: x, y: y))
}
// Create the path
context.beginPath()
context.move(to: points[0])
for i in 1..<points.count {
context.addLine(to: points[i])
}
context.closePath()
// Fill and stroke
context.setFillColor(UIColor.yellow.cgColor)
context.setStrokeColor(UIColor.orange.cgColor)
context.setLineWidth(3)
context.drawPath(using: .fillStroke)
}
Result: This code generates a five-pointed yellow star with an orange border.
Colors, Gradients, and Patterns
Working with Colors
Core Graphics provides several ways to work with colors:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Using RGB color
context.setFillColor(red: 0.5, green: 0.2, blue: 0.8, alpha: 1.0)
context.fill(CGRect(x: 20, y: 20, width: 100, height: 100))
// Using UIKit colors
context.setFillColor(UIColor.systemTeal.cgColor)
context.fill(CGRect(x: 140, y: 20, width: 100, height: 100))
}
Creating Gradients
Gradients are transitions between colors and can add depth to your drawings:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let colors = [UIColor.red.cgColor, UIColor.yellow.cgColor] as CFArray
let colorSpace = CGColorSpaceCreateDeviceRGB()
// Create gradient locations (where each color should be positioned from 0 to 1)
let locations: [CGFloat] = [0.0, 1.0]
// Create the gradient
guard let gradient = CGGradient(
colorsSpace: colorSpace,
colors: colors,
locations: locations
) else { return }
// Draw a linear gradient
let startPoint = CGPoint(x: 0, y: 0)
let endPoint = CGPoint(x: rect.width, y: rect.height)
context.drawLinearGradient(
gradient,
start: startPoint,
end: endPoint,
options: []
)
}
Result: This creates a red to yellow diagonal gradient filling the entire view.
Radial Gradient
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let colors = [UIColor.blue.cgColor, UIColor.clear.cgColor] as CFArray
let colorSpace = CGColorSpaceCreateDeviceRGB()
let locations: [CGFloat] = [0.0, 1.0]
guard let gradient = CGGradient(
colorsSpace: colorSpace,
colors: colors,
locations: locations
) else { return }
let center = CGPoint(x: rect.width/2, y: rect.height/2)
// Draw a radial gradient
context.drawRadialGradient(
gradient,
startCenter: center,
startRadius: 10,
endCenter: center,
endRadius: 200,
options: []
)
}
Result: This creates a blue circle that fades to transparent at the edges.
Transformations and State Management
Transformations
Core Graphics allows you to apply transformations like rotation, scaling, and translation to your drawings:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Draw a rectangle
let rectangle = CGRect(x: -50, y: -25, width: 100, height: 50)
// Save the current graphics state
context.saveGState()
// Move to the center of the view
context.translateBy(x: rect.width/2, y: rect.height/2)
// Rotate by 45 degrees
context.rotate(by: .pi/4)
// Draw the rectangle with the applied transformations
context.setFillColor(UIColor.purple.cgColor)
context.fill(rectangle)
// Restore the graphics state
context.restoreGState()
// Draw another shape without the transformation
context.setFillColor(UIColor.green.cgColor)
context.fill(CGRect(x: 50, y: 50, width: 80, height: 80))
}
Result: This draws a rotated purple rectangle in the center of the view and a green square in the top-left area.
Saving and Restoring State
The saveGState()
and restoreGState()
methods are crucial for managing the drawing state:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Set initial state
context.setLineWidth(2)
context.setStrokeColor(UIColor.black.cgColor)
// Draw a line with the initial state
context.move(to: CGPoint(x: 20, y: 20))
context.addLine(to: CGPoint(x: 100, y: 20))
context.strokePath()
// Save the state
context.saveGState()
// Modify the state
context.setLineWidth(5)
context.setStrokeColor(UIColor.red.cgColor)
// Draw with the modified state
context.move(to: CGPoint(x: 20, y: 50))
context.addLine(to: CGPoint(x: 100, y: 50))
context.strokePath()
// Restore to the original state
context.restoreGState()
// Draw with the restored state
context.move(to: CGPoint(x: 20, y: 80))
context.addLine(to: CGPoint(x: 100, y: 80))
context.strokePath()
}
Result: This draws three horizontal lines - a thin black line at the top, a thick red line in the middle, and another thin black line at the bottom.
Text and Images
Drawing Text
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let text = "Hello Core Graphics!"
let font = UIFont.systemFont(ofSize: 24)
let textAttributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.darkBlue
]
// Calculate text size
let textSize = text.size(withAttributes: textAttributes)
// Calculate position for centered text
let textRect = CGRect(
x: (rect.width - textSize.width) / 2,
y: (rect.height - textSize.height) / 2,
width: textSize.width,
height: textSize.height
)
// Draw the text
text.draw(in: textRect, withAttributes: textAttributes)
}
Drawing Images
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// Get an image
guard let image = UIImage(named: "example-image") else { return }
// Draw the image
image.draw(in: CGRect(x: 50, y: 50, width: 200, height: 200))
// Draw image with alpha
context.saveGState()
context.setAlpha(0.5)
image.draw(in: CGRect(x: 150, y: 150, width: 200, height: 200))
context.restoreGState()
}
Real-World Example: Custom Progress Bar
Let's create a practical example of a custom progress bar using Core Graphics:
class CustomProgressBar: UIView {
// Progress value from 0.0 to 1.0
var progress: CGFloat = 0.0 {
didSet {
// Ensure progress stays between 0 and 1
progress = min(1.0, max(0.0, progress))
setNeedsDisplay()
}
}
// Colors
var progressColor: UIColor = .systemBlue
var trackColor: UIColor = .systemGray5
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let cornerRadius: CGFloat = rect.height / 2
// Draw track (background)
let trackPath = UIBezierPath(
roundedRect: rect,
cornerRadius: cornerRadius
)
trackColor.setFill()
trackPath.fill()
// Draw progress
let progressWidth = rect.width * progress
let progressRect = CGRect(
x: 0,
y: 0,
width: progressWidth,
height: rect.height
)
if progressWidth > 0 {
let progressPath = UIBezierPath(
roundedRect: progressRect,
cornerRadius: cornerRadius
)
progressColor.setFill()
progressPath.fill()
}
}
}
// Usage:
let progressBar = CustomProgressBar(frame: CGRect(x: 50, y: 100, width: 300, height: 20))
view.addSubview(progressBar)
// Animate the progress
UIView.animate(withDuration: 2.0) {
progressBar.progress = 0.75
}
Result: This creates a rounded progress bar that animates from empty to 75% full over 2 seconds.
Real-World Example: Pie Chart
Another practical use of Core Graphics is creating data visualizations like charts:
class PieChartView: UIView {
// Data for pie segments (values should sum to 1.0)
var segments: [(value: CGFloat, color: UIColor)] = [
(0.3, .systemRed),
(0.25, .systemBlue),
(0.2, .systemGreen),
(0.15, .systemOrange),
(0.1, .systemPurple)
]
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) * 0.4
// Start at the top (negative is up in CG coordinates)
var startAngle: CGFloat = -.pi / 2
// Draw each segment
for segment in segments {
let endAngle = startAngle + (segment.value * .pi * 2)
// Create path for this segment
context.move(to: center)
context.addArc(
center: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: false
)
context.closePath()
// Set fill color and fill
context.setFillColor(segment.color.cgColor)
context.fillPath()
// Update start angle for next segment
startAngle = endAngle
}
}
}
// Usage:
let pieChart = PieChartView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
view.addSubview(pieChart)
Result: This creates a pie chart with five colored segments representing the provided data values.
Summary
Core Graphics is a powerful framework that allows iOS developers to create custom drawings and graphics with precise control. In this guide, we've covered:
- The fundamentals of Core Graphics and how to work with contexts
- Drawing basic shapes like rectangles, circles, and lines
- Creating complex paths and custom shapes
- Working with colors, gradients, and patterns
- Applying transformations and managing state
- Drawing text and images
- Real-world examples like progress bars and charts
Remember that Core Graphics is performance-intensive, so it's best to cache drawings when possible rather than redrawing them repeatedly.
Additional Resources
- Apple's Core Graphics Framework Reference
- Quartz 2D Programming Guide
- WWDC Sessions on Graphics and Animation
Exercises
- Create a custom loading spinner that animates using Core Graphics
- Build a bar chart that displays multiple data points
- Design a custom switch control with animated transitions
- Implement a drawing app that lets users draw on the screen
- Create a custom image filter effect using Core Graphics
By mastering Core Graphics, you'll be able to create unique, visually appealing UI components that will make your iOS applications stand out.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)