CGRect struct might seem like a primitive beast, but it has a number of useful methods that let you adjust rectangles i a variety of ways.
Here are two simple
CGRect instances we can work with:
let rect1 = CGRect(x: 20, y: 20, width: 100, height: 100) let rect2 = CGRect(x: 50, y: 50, width: 100, height: 100)
You can see that they overlap each other, and
CGRect has three methods that let us evaluate that overlap. The first is the simplest:
intersects() returns true if two rectangles overlap. In our case, this would be true:
If you want more details, the
intersection() method tells you where the intersection lies. Both our rectangles are 100 points wide, but the second one starts at X:50 and Y:50 so that’s where our overlap will begin. So, this code will return a new
CGRect containing X:50 Y:50 width:70 height:70:
The third option is the
contains() method, which will return true if one rectangle entirely contains another. So, this will return false for us because the second rectangle lies partly outside the first:
There are four useful methods for transforming rectangles, but by far the most useful are
offsetBy(). The former is used for expanding or contracting rectangles, and the latter for adjusting their positions.
For example, this will produce a rectangle at X:40 Y:40 width:60 height:60:
rect1.insetBy(dx: 20, dy: 20)
You’re specifying how much to grow or shrink the rectangle horizontally (
dx) and vertically (
dy) – these numbers apply to all four edges, which is why a horizontal change of 20 took our width from 100 to 60.
You can also make rectangles larger, like this:
rect1.insetBy(dx: -20, dy: -20)
I find negative insets helpful if you’re drawing behind something and want to make sure you have a fixed amount of margin on all edges.
offsetBy(), this is simply designed to move rectangles by whatever X and Y amount you need. Internally it just modifies the origin of the rectangle while also making sure it has positive width and height values, but using
offsetBy() helps make your intent clearer.
So, this will move our rectangle 10 points down and to the right:
rect1.offsetBy(dx: 10, dy: 10)
For more advanced transformations, you can apply any
CGAffineTransform to a
CGRect by calling its
applying() method. You should experiment in a playground first, though – these transforms work in their own unique way.
As an example, take a look at this code:
let transform = CGAffineTransform(scaleX: 2, y: 2) rect1.applying(transform)
rect1 constant had X:20 Y:20 width:100 height:100, so after scaling by 2x there are several possible outcomes. For example, you might think that X and Y stay the same, but width and height double, or you might think that width and height double, but X and Y go down by 50 so the rectangle stays centered.
However, what this method actually does is scale up all four coordinate components: X, Y, width, and height all double, meaning that the finished rectangle starts at X:40 Y:40 – it has been scaled relative to the top-left corner.
One last method that’s worth mentioning is
divided(atDistance:), which lets you easily split a rectangle into two parts. This returns two rectangles: the slice up to the point you specified, and the remainder afterwards.
As an example, we could divide
rect1 into two rectangles, with the first slice taking up the first 40% of
rect1 and the remainder taking up everything else:
let (slice, remainder) = rect1.divided(atDistance: rect1.width * 0.4, from: .minXEdge)
All these methods are things you could yourself, usually in a dozen lines of code or less. But do you really want to be spending your time thinking about rectangle transformations when Apple can do all that work for you? Probably not.
CGRect isn't a complex struct, but it's worth letting it work its magic where possible!
Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.