Core Graphics transforms provide control over coordinate systems, drawing contexts, and paths by enabling you to apply rotation, scaling, and translations. As I’ve been writing about Swift Style, I hope I’m now better able to articulate why I hate the way they’ve been automatically named into Swift.
The current Swift constructors and transformers are:
init(rotationAngle: CGFloat)
init(scaleX: CGFloat, y: CGFloat)
init(translationX: CGFloat, y: CGFloat)
rotated(by: CGFloat)
- scaledBy(x: CGFloat, y: CGFloat)
translatedBy(x: CGFloat, y: CGFloat)
Here are my issues:
Confusing naming. “Scale” can be a noun or verb. You can only figure out which one was intended by looking at the translation API. If you have to look at another API to figure out some meaning, the API was written wrong.
In this example, if the two arguments were swapped around and balanced (xScale, yScale), the API would make a lot more sense. I’m not arguing for those terms. I just want to make the point that you should avoid using ambiguous words. This is a classic example of that error.
Missing Types. Why scale and translate by identical meaningless “x” and “y” values? Or rotate by “rotationAngle”? There are better, more exact terms of art and a couple of missing types to support those terms.
Although Core Graphics added CGVector
, it is still missing two key types: CGAngle
(stores radians or degrees) and CGScale
(stores sx, sy scaling factor pairs).
If I were in charge, you’d be able to initialize a CGAngle using radians, degrees, and π count (for example, 360 degrees is 2π and 45 degrees is 0.25 π). And you’d be able to pull out properties for each of those items as well from a unified structure. A CGScale would provide a natural way to store scaling factors.
Redundancy. If you can come up with a more redundant term than rotationAngle
, I’d like to hear it. rotationAngle
uses two nearly identical words to do the work of one: “radians”. It presumes only one confusing initialization style while ignoring other common use cases like “degrees” and “multiples of pi”.
Incorrect Type Use. Although it’s convenient to break out factors into component elements, you’re really translating by a vector or scaling by an (sx, sy) pair. You should be allowed to use these types (for example, scale: CGScale
) as well as the broken down member calls (for example, sx:sy:
). This is most notable in the use of CGVector
. Although the CGVector type was introduced long after Affine Transforms, transforms were never updated to take advantage of a semantically richer approach.
Mix and Match Prepositions. Core Graphics includes rotated(by:)
, scaledBy()
, translatedBy().
So where does that “by” belong? In a coherent API, each “by” should either be in the parentheses or outside.
I vote “out”. Doing so enables you to create call families like these, where the specifics of the labels preserve share abstractions and the by
isn’t glommed onto the first argument label, throwing off the API’s balance.
public func translatedBy(tx: CGFloat, ty: CGFloat) -> CGAffineTransform public func translatedBy(vector: CGVector) -> CGAffineTransform
Speaking of unbalanced arguments, scaleX
is way longer than y
, as is translationX
vs y
, yet both arguments have equal weight and priority in the calls. Unbalanced labels offer another good indicator of badly designed APIs.