Core Graphics is an API included in both Cocoa and Cocoa Touch. It allows you to draw graphic objects on the graphic destination.
So, the Core Graphics framework is based on the Quartz advanced drawing engine. It provides low-level, lightweight 2D rendering with unmatched output fidelity. You use this framework to handle path-based drawing, transformations, color management, offscreen rendering, patterns, gradients and shadings, image data management, image creation, and image masking, as well as PDF document creation, display, and parsing.
CoreGraphics include some helpful types: CGFloat, CGPoint, CGSize, CGRect.
CoreGraphics include some helpful classes: CGContext, CGImage, CGPath/CGMutablePath, CGColor, etc.
Let's build some simple shapes. The UIGraphicsImageRenderer
class was introduced in iOS 10 as a modern class which allows closures. It also support the wide color gamut of devices like the iPad Pro.
func drawLines() { let renderer = UIGraphicsImageRenderer(size: CGSize(width: 280, height: 250)) let img = renderer.image { ctx in ctx.cgContext.move(to: CGPoint(x: 20.0, y: 20.0)) ctx.cgContext.addLine(to: CGPoint(x: 260.0, y: 230.0)) ctx.cgContext.addLine(to: CGPoint(x: 100.0, y: 200.0)) ctx.cgContext.addLine(to: CGPoint(x: 20.0, y: 20.0)) ctx.cgContext.setLineWidth(10) ctx.cgContext.setStrokeColor(UIColor.black.cgColor) ctx.cgContext.strokePath() } imageView.image = img } func drawRectangle() { let renderer = UIGraphicsImageRenderer(size: CGSize(width: 280, height: 250)) let img = renderer.image { ctx in let rectangle = CGRect(x: 0, y: 0, width: 280, height: 250) ctx.cgContext.setFillColor(UIColor.yellow.cgColor) ctx.cgContext.setStrokeColor(UIColor.gray.cgColor) ctx.cgContext.setLineWidth(20) ctx.cgContext.addRect(rectangle) ctx.cgContext.drawPath(using: .fillStroke) } imageView.image = img } func drawCircle() { let renderer = UIGraphicsImageRenderer(size: CGSize(width: 280, height: 250)) let img = renderer.image { ctx in let rect = CGRect(x: 5, y: 5, width: 270, height: 240) ctx.cgContext.setFillColor(UIColor.blue.cgColor) ctx.cgContext.setStrokeColor(UIColor.black.cgColor) ctx.cgContext.setLineWidth(10) ctx.cgContext.addEllipse(in: rect) ctx.cgContext.drawPath(using: .fillStroke) } imageView.image = img }
Custom Drawing on Views
First, let’s build a view that will draw a yellow star.
There are two steps for custom drawings:
draw(_:)
and add some Core Graphics drawing code.Overriding the draw method
The most common place to draw using Core Graphics is in the draw
method of UIView
. This method is called when a view is first laid out and any time that the view needs to be redrawn.
Create a subclass of UIView
called Star
. The draw method shows up in the UIView template, but it’s commented out. Uncomment the draw method:
class Star: UIView { override func draw(_ rect: CGRect) { // Drawing code } }
Note that the draw
method is passed a rect parameter containing the dimensions available to you to draw in.
Describing a path
To draw both simple and complex shapes in Core Graphics, you first need to describe their paths. Paths are described with a CGPath
object, but UIKit class UIBezierPath
is often used, because it has additional functionality and can provide you with a CGPath
object anyway, via its cgPath
property.
Simple shapes are easy to define in UIBezierPath
- it has initializers that define ovals, rectangles, rounded rectangles, and arcs. For example, the following will create a circle path that fits inside a rectangle:
UIBezierPath(rect: CGRect(x: 0, y: 0, width: 100, height: 100))
To create a complex path, after instantiating an empty UIBezierPath
object, you’d move to the initial point of the path with the move method, draw lines to each point in the path with the addLine
method, and finally close the path with the close
method.
You’ll use the UIBezierPath
method to draw a star. Add the following method to your Star
class that returns a UIBezierPath
object that describes the path to draw a star:
func getStarPath() -> UIBezierPath { let path = UIBezierPath() path.move(to: CGPoint(x: 12, y: 1.2)) path.addLine(to: CGPoint(x: 15.4, y: 8.4)) path.addLine(to: CGPoint(x: 23, y: 9.6)) path.addLine(to: CGPoint(x: 17.5, y: 15.2)) path.addLine(to: CGPoint(x: 18.8, y: 23.2)) path.addLine(to: CGPoint(x: 12, y: 19.4)) path.addLine(to: CGPoint(x: 5.2, y: 23.2)) path.addLine(to: CGPoint(x: 6.5, y: 15.2)) path.addLine(to: CGPoint(x: 1, y: 9.6)) path.addLine(to: CGPoint(x: 8.6, y: 8.4)) path.close() return path }
To draw the shape you defined with Core Graphics, you need a graphics context.
Drawing into the graphics context
The graphics context is where all your Core Graphics drawing is performed. You can get a reference to the current graphics context with the global UIGraphicsGetCurrentContext
method.
Add a reference to the current graphics context in the draw method:
let context = UIGraphicsGetCurrentContext()
Now that you have a graphics context, you can set the stroke or fill color, add the path you defined earlier, and then draw the path using either a fill, a stroke, or both.
Draw the star path into the current graphics context using an orange fill. Use the cgColor
property of UIColor
to pass in a CGColor
object.
context?.setFillColor(UIColor.orange.cgColor) context?.addPath(getStarPath().cgPath) context?.drawPath(using: .fill)
Saving and restoring graphics state
Every time you make a change to an attribute in the graphics context (such as setting the fill color, font name, line width, anti-aliasing, or transforms — the list goes on) you’re adjusting the graphics state, and any future graphics calls will be affected by these changes.
If you only want to adjust the graphics state temporarily for the current operation you’re performing, it’s a good idea to save the graphics state to the stack first and then restore the graphics state from the stack when you’re finished, to leave the graphics state as you found it.
Surround the drawing of the star path with saving and restoring the graphics state:
context?.saveGState() //change graphics state //draw operation (e.g. draw star) context?.restoreGState()
Drawing paths with UIBezierPath drawing methods
An additional feature of the UIBezierPath
wrapper for CGPath
is the ability to stroke or fill a path into the current graphics context directly from the path object. Using the UIBezierPath
drawing methods not only avoids the need for a reference to the graphics context, but will automatically perform the administrative detail of saving and restoring graphics state for you.
Replace the graphics context–focused code from earlier with the UIBezierPath
drawing methods. You can set the fill on the UIColor
class itself, and then fill the path by calling the fill method on the UIBezierPath
object.
override func draw(_ rect: CGRect) { UIColor.orange.setFill() getStarPath().fill() }
Add a fill
property to the Star
class, which determines whether the star should be filled or given a stroke.
var fill = false override func draw(_ rect: CGRect) { if fill { UIColor.orange.setFill() getStarPath().fill() } else { UIColor.orange.setStroke() getStarPath().stroke() } }
When the fill
property is set, the star should be redrawn. However, the star is being drawn in the draw
method, and you should never call the draw
method directly. Instead, you should notify the system that the view needs to be redrawn with the setNeedsDisplay
method.
Add a didSet
property observer to the fill
property, which calls setNeedsDisplay
.
var fill: Bool = false { didSet { setNeedsDisplay() } }
Rendering views in Interface Builder
It would be great to see the star you’ve drawn. Let’s look at what you have so far in Interface Builder.
With the main storyboard open, drag in a temporary view controller and then drag a view into its root view.
In the Identity Inspector, give the view the custom class of Star
.
Add the @IBDesignable
attribute before the class declaration for Star
.
@IBDesignable class Star: UIView {
Return to the main storyboard, and the star view should now render nicely. But it’s defaulting to not filled.
You can specify that a property be adjustable directly from Interface Builder by adding the @IBInspectable
attribute before declaring the property.
Add the @IBInspectable
attribute before the fill property in the Star
class.
@IBInspectable var fill: Bool = false {
Return to the main storyboard and select the Attributes Inspector for the star view. You should find a new attribute, called Fill.
Select On, and your star view should appear filled in the canvas.
Creating a star-rating view
Now that the star view is ready, you can set up your star-rating view. Similar to the star view, the star-rating view will render in Interface Builder, and will have inspectable properties to customize its appearance.
Create a Rating
class that subclasses UIView
, and make it render in the storyboard with the @IBDesignable
attribute.
@IBDesignable class Rating: UIView {
Set up a property to define how many stars the rating view should fill. Make the property inspectable, and include a property observer that registers that the view requires layout when it is set.
@IBInspectable var rating: Double = 3 { didSet {setNeedsLayout()} }
Add the star subviews in the layoutSubviews
method of the star-rating view. Check that the stars array is empty. If it is, you need to create the star views, adding them to the view and the stars array. You need to clear the background color of each star view because it will default to black otherwise when generated from the draw method. Finally, use the rating property to determine how many stars should be filled.
var stars: [Star] = [] let numberOfStars = 5 override func layoutSubviews() { if stars.count == 0 { // add stars for i in 0..<numberOfStars { let star = Star(frame: CGRect(x: CGFloat(30 * i), y: 0, width: 25, height: 25)) star.backgroundColor = UIColor.clear self.addSubview(star) stars.append(star) } } for (i,star) in stars.enumerated() { star.fill = Double(i) < rating } }
Open the main storyboard, and in the Identity Inspector, change the subclass of your temporary view to your new Rating
class. Your rating view should now render nicely in the storyboard.
Play with the number of stars and rating properties in the Attributes Inspector, and change the look of the rating view in the canvas.
Now you want to make your ratings view interactive in your app, so that the user can select ratings.
Override the touchesBegan
method in the Rating
class. Determine the index of the star view the user touched from the stars
array, and use this index to set the rating property.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else {return} guard let star = touch.view as? Star else {return} guard let starIndex = stars.index(of: star) else {return} rating = Double(starIndex) + 1 }
Because the rating property calls setNeedsDisplay
in its didSet
property observer, setting the rating property is all that’s needed for the star-rating view to update visually when the user selects a different rating.
For auto layout and scroll views to manage the size of the star-rating view correctly, you'll need to specify its intrinsic content size.
Override the intrinsicContentSize
property in the Rating
class.
override var intrinsicContentSize: CGSize { return CGSize(width: 30 * numberOfStars, height: 25) }
Useful links