CoreGraphics Geometry Primitives
Unless you were a Math Geek or an Ancient Greek, Geometry probably wasn’t your favorite subject in school. More likely, you were that kid in class who dutifully programmed all of those necessary formulæ into your TI-8X calculator to avoid rote memorization.
So for those of you who spent more time learning TI-BASIC than Euclid, here’s the cheat-sheet for how geometry works in Quartz 2D, the drawing system used by Apple platforms:
- A
CGFloat
represents a scalar quantity. - A
CGPoint
represents a location in a two-dimensional coordinate system and is defined byx
andy
scalar components. - A
CGVector
represents a change in position in 2D space and is defined bydx
anddy
scalar components. - A
CGSize
represents the extent of a figure in 2D space and is defined bywidth
andheight
scalar components. - A
CGRect
represents a rectangle and is defined by an origin point (CGPoint
) and a size (CGSize
).
import Core Graphics
let float: CGFloat = 1.0
let point = CGPoint(x: 1.0, y: 2.0)
let vector = CGVector(dx: 4.0, dy: 3.0)
let size = CGSize(width: 4.0, height: 3.0)
var rectangle = CGRect(origin: point, size: size)
On iOS, the origin is located at the top-left corner of a window,
so x
and y
values increase as they move down and to the right.
macOS, by default, orients (0, 0)
at the bottom left corner of a window,
such that y
values increase as they move up.
Every view in an iOS or macOS app
has a frame
represented by a CGRect
value,
so one would do well to learn the fundamentals
of these geometric primitives.
In this week’s article, we’ll do a quick run through the APIs with which every app developer should be familiar.
Introspection
“First, know thyself.” So goes the philosophical aphorism. And it remains practical guidance as we begin our survey of CoreGraphics API.
As structures, you can access the member values of geometric types directly through their stored properties:
point.x // 1.0
point.y // 2.0
size.width // 4.0
size.height // 3.0
rectangle.origin // {x 1 y 2}
rectangle.size // {w 4 h 3}
You can mutate variables by reassignment
or by using mutating operators like *=
and +=
:
var mutable Rectangle = rectangle // {x 1 y 2 w 4 h 3}
mutable Rectangle.origin.x = 7.0
mutable Rectangle.size.width *= 2.0
mutable Rectangle.size.height += 3.0
mutable Rectangle // {x 7 y 2 w 8 h 6}
For convenience,
rectangles also expose width
and height
as top-level, computed properties;
(x
and y
coordinates must be accessed through the intermediary origin
):
rectangle.origin.x
rectangle.origin.y
rectangle.width
rectangle.height
Accessing Minimum, Median, and Maximum Values
Although a rectangle can be fully described by
a location (CGPoint
) and an extent (CGSize
),
that’s just one side of the story.
For the other 3 sides,
use the built-in convenience properties
to get the minimum (min
), median (mid
), and maximum (max
) values
in the x
and y
dimensions:
rectangle.min X // 1.0
rectangle.mid X // 3.0
rectangle.max X // 5.0
rectangle.min Y // 2.0
rectangle.mid Y // 3.5
rectangle.max Y // 5.0
Computing the Center of a Rectangle
It’s often useful to compute the center point of a rectangle.
Although this isn’t provided by the framework SDK,
you can easily extend CGRect
to implement it
using the mid
and mid
properties:
extension CGRect {
var center: CGPoint {
return CGPoint(x: mid X, y: mid Y)
}
}
Normalization
Things can get a bit strange when you use non-integral or negative values in geometric calculations. Fortunately, CoreGraphics has just the APIs you need to keep everything in order.
Standardizing Rectangles
We expect that a rectangle’s origin is situated at its top-left corner. However, if its size has a negative width or height, the origin could become any of the other corners instead.
For example, consider the following bizarro rectangle that extends leftwards and upwards from its origin.
let ǝןƃuɐʇɔǝɹ = CGRect(origin: point,
size: CGSize(width: -4.0, height: -3.0))
ǝןƃuɐʇɔǝɹ // {x 1 y 2 w -4 h -3}
We can use the standardized
property
to get the equivalent rectangle
with non-negative width and height.
In the case of the previous example,
the standardized rectangle
has a width of 4
and height of 3
and is situated at the point (-3, -1)
:
ǝןƃuɐʇɔǝɹ.standardized // {x -3 y -1 w 4 h 3}
Integrating Rectangles
It’s generally a good idea for all CGRect
values
to be rounded to the nearest whole point.
Fractional values can cause the frame to be drawn on a pixel boundary.
Because pixels are atomic units,
a fractional value causes drawing to be averaged over the neighboring pixels.
The result: blurry lines that don’t look great.
The integral
property takes the floor
each origin value
and the ceil
each size value.
This ensures that your drawing code aligns on pixel boundaries crisply.
let blurry = CGRect(x: 0.1, y: 0.5, width: 3.3, height: 2.7)
blurry // {x 0.1 y 0.5 w 3.3 h 2.7}
blurry.integral // {x 0 y 0 w 4 h 4}
Transformations
While it’s possible to mutate a rectangle by performing member-wise operations on its origin and size, the CoreGraphics framework offers better solutions by way of the APIs discussed below.
Translating Rectangles
Translation describes the geometric operation of moving a shape from one location to another.
Use the offset
method (or CGRect
function in Objective-C)
to translate a rectangle’s origin by a specified x
and y
distance.
rectangle.offset By(dx: 2.0, dy: 2.0) // {x 3 y 4 w 4 h 3}
Consider using this method whenever you shift a rectangle’s position. Not only does it save a line of code, but it more semantically represents intended operation than manipulating the origin values individually.
Contracting and Expanding Rectangles
Other common transformations for rectangles
include contraction and expansion around a center point.
The inset
method can accomplish both.
When passed a positive value for either component,
this method returns a rectangle that
shrinks by the specified amount from each side
as computed from the center point.
For example,
when inset by 1.0
horizontally (dy = 0.0
),
a rectangle originating at (1, 2)
with a width of 4
and height
equal to 3
,
produces a new rectangle originating at (2, 2)
with width equal to 2
and height equal to 3
.
Which is to say:
the result of insetting a rectangle by 1
point horizontally
is a rectangle whose width
is 2
points smaller than the original.
rectangle // {x 1 y 2 w 4 h 3}
rectangle.inset By(dx: 1.0, dy: 0.0) // {x 2 y 2 w 2 h 3}
When passed a negative value for either component, the rectangle grows by that amount from each side. When passed a non-integral value, this method may produce a rectangle with non-integral components.
rectangle.inset By(dx: -1.0, dy: 0.0) // {x 0 y 2 w 6 h 3}
rectangle.inset By(dx: 0.5, dy: 0.0) // {x 1.5 y 2 w 3 h 3}
Identities and Special Values
Points, sizes, and rectangles each have a zero
property,
which defines the identity value for each respective type:
CGPoint.zero // {x 0 y 0}
CGSize.zero // {w 0 h 0}
CGRect.zero // {x 0 y 0 w 0 h 0}
Swift shorthand syntax allows you to pass .zero
directly
as an argument for methods and initializers,
such as CGRect.init(origin:size:)
:
let square = CGRect(origin: .zero,
size: CGSize(width: 4.0, height: 4.0))
CGRect
has two additional special values: infinite
and null
:
CGRect.infinite // {x -∞ y -∞ w +∞ h +∞}
CGRect.null // {x +∞ y +∞ w 0 h 0}
CGRect.null
is conceptually similar to NSNot
,
in that it represents the absence of an expected value,
and does so using the largest representable number to exclude all other values.
CGRect.infinite
has even more interesting properties,
as it intersects with all points and rectangles,
contains all rectangles,
and its union with any rectangle is itself.
CGRect.infinite.contains(any point) // true
CGRect.infinite.intersects(any other rectangle) // true
CGRect.infinite.union(any other rectangle) // CGRect.infinite
Use is
to determine whether a rectangle is, indeed, infinite.
CGRect.infinite.is Infinite // true
But to fully appreciate why these values exist and how they’re used, let’s talk about geometric relationships:
Relationships
Up until this point, we’ve been dealing with geometries in isolation. To round out our discussion, let’s consider what’s possible when evaluating two or more rectangles.
Intersection
Two rectangles intersect if they overlap. Their intersection is the smallest rectangle that encompasses all points contained by both rectangles.
In Swift,
you can use the intersects(_:)
and intersection(_:)
methods
to efficiently compute the intersection of two CGRect
values:
let square = CGRect(origin: .zero,
size: CGSize(width: 4.0, height: 4.0))
square // {x 0 y 0 w 4 h 4}
rectangle.intersects(square) // true
rectangle.intersection(square) // {x 1 y 2 w 3 h 2}
If two rectangles don’t intersect,
the intersection(_:)
method produces CGRect.null
:
rectangle.intersects(.zero) // false
rectangle.intersection(.zero) // CGRect.null
Union
The union of two rectangles is the smallest rectangle that encompasses all of the points contained by either rectangle.
In Swift,
the aptly-named union(_:)
method does just this for two CGRect
values:
rectangle.union(square) // {x 0 y 0 w 5 h 5}
So what if you didn’t pay attention in Geometry class —
this is the real world.
And in the real world,
you have CGGeometry.h
and all of the types and functions it provides.
Know it well, and you’ll be on your way to discovering great new user interfaces in your apps. Do a good enough job with that, and you may encounter the best arithmetic problem of all: adding up all the money you’ve made with your awesome new app. Mathematical!