In which order do CGAffineTransform operations apply, and what do scaledBy, rotated and translatedBy do differently from concatenating?
Predict first
Where does the point (10, 0) land for each ordering?
import UIKit
let p = CGPoint(x: 10, y: 0)
let T = CGAffineTransform(translationX: 100, y: 0), S = CGAffineTransform(scaleX: 2, y: 2)
print(p.applying(T.concatenating(S)), p.applying(S.concatenating(T)))
print(p.applying(T.scaledBy(x: 2, y: 2)), p.applying(S.translatedBy(x: 100, y: 0)))
print(T.scaledBy(x: 2, y: 2) == S.concatenating(T), S.translatedBy(x: 100, y: 0) == T.concatenating(S))
- (220, 0) (120, 0), then (120, 0) (220, 0), then true true
- (120, 0) (220, 0), then (220, 0) (120, 0), then false false
- (220, 0) (120, 0), then (220, 0) (120, 0), then true false
Mental model. Each call stacks a new lens on the front of the camera: the last lens you add is the first one the light passes through.
A CGAffineTransform is a 2D matrix; concatenating reads left to right as “first this, then that”: T.concatenating(S) translates by 100 then scales by 2, so the point (10, 0) goes to (220, 0). Reverse it and you scale first then translate: (120, 0). The convenience methods are easy to misread because they prepend: t.scaledBy(x: 2, y: 2) means “scale first, then apply t”, so T.scaledBy(x: 2, y: 2) equals S.concatenating(T) and gives (120, 0), and S.translatedBy(x: 100, y: 0) equals T.concatenating(S), giving (220, 0). The check proves both equalities. So a chain of calls reads backwards compared with how the matrix acts.
Practical consequences: rotating a view about a point other than its centre is translate, rotate, translate back; and view.transform is applied about the view’s anchor point, which is why a plain rotation spins about the centre and needs no translations. In Core Graphics drawing the same rule applies to ctx.translateBy, rotate and scaleBy: they modify the current transform matrix so that later drawing is in the new space, and the order you call them is the reverse of the order in which they act on a point. Wrap temporary changes in saveGState() and restoreGState(), and use inverted() to map a touch back into the drawing space.
concatenating is left to right; the convenience methods prepend, so they read in reverse.
Common traps
- Reading a chain of
scaledByandtranslatedBytop to bottom. The newest call acts first on the point. - Expecting translate-then-scale and scale-then-translate to match. They differ by the scale factor on the offset.
They might follow up with
- How do you rotate about a corner? Translate to it, rotate, translate back.
- How do you convert a touch to drawing space? Apply
inverted()of the current transform.
Related questions
- 9.5.1 How do you draw with Core Graphics and UIBezierPath, and how do scale and caching affect it?
- 9.9.2 How do CATransform3D, perspective (m34), zPosition and sublayerTransform work, and what happens to frame?
- 7.2.2 What is the difference between frame and bounds, and what happens to them under a transform?
How do the non-zero and even-odd fill rules decide what is inside a… →