Yesterday, I asked you to pick a style for some sample code about character-by-character layout.
- Choice A used a procedural for-in loop
- Choice B used a map-reduce pair
- Choice C used a series of maps, followed by a reduce.
The people have spoken, and they spoke primarily in favor of choice A:
[socialpoll id=”2395012″]
Summarizing the majority view, David wrote “For my money, A wins by a mile. It’s just far more readable, maintainable, with far less mental work required.”
Here’s the layout I was working on presenting:
And the code I ended up using was this. It ended up closer to choice C than Choice A, but I took my guidance from the “make it clearer crowd” A crowd. I hope that I picked up the “more readable, maintainable, less mental work” theme the voters asked for.
// Convert characters to attributed strings // and measure them let letters = characters.lazy .map({ String($0) }) .map({ NSAttributedString(string: $0, attributes: attributes) }) let letterSizes = letters.map({ $0.size().width }) // Calculate the full extent let fullSize = letterSizes.reduce(0 as CGFloat, +) var consumedSize: CGFloat = 0 // Draw each letter proportionally for (letter, letterSize) in zip(letters, letterSizes) { let halfWidth = letterSize / 2.0 consumedSize = consumedSize + halfWidth; defer { consumedSize += halfWidth } pushDraw(in: context) { // Rotate the context let theta = 2 * π * consumedSize / fullSize context.cgContext.rotate(by: theta) // Translate up to the edge of the radius // and move left by half the letter width. context.cgContext.translateBy(x: -halfWidth, y: -r) letter.draw(at: .zero) } }
I took Paul C’s advice to heart: “I like each step to be a useful thought that conveys to a human some but not too much new information about what’s going on.” My revised approach breaks the code down more into steps.
Reader feedback led me to consider a few excellent points. Was I going to re-use any of the information? (Yes I was) If so, I needed to break down the functional chain. If I went functional, shouldn’t I be using lazy mapping? (Yes I should).
The zipped for-loop let me pair letters with their sizes so I could perform the rest of the layout without re-calculating each letter size. One perfectly cromulent approach put forth (that I decided not to go with) was that I could merge the characters into a single string (presumably disabling ligatures and kerning) and calculate the full width once.
If I were only calculating the width, this would work fine. Once I decided to retain and re-use individual sizing, that approach became less attractive, but I thank the several people who suggested it.
Finally shout out to Rob N, who suggested Core Text layout for better spacing, which I’ll try out this morning.
Thank you everyone for your feedback and input.
Update: I gave it a try, using TextKit, laying out the text in a line and then using the resulting glyph widths. I had to enable kerning to get a better layout result (my first attempts were identical to yesterday’s). Here’s the updated layout, plus an overlay of the two. I think the “W” looks a lot better.
4 Comments
Are you going for proportional spacing, or equal spacing?
The second looks better because the spacing is more equal between all the letters.
But in the second, they also seem to tilted a little to much to the right. Neither ‘A’ nor ‘Z’ seem like they are tangent to the circle they are sitting on. (Actually it looks like they are drawn an extra half-width to the left instead of being centered like the first version).
Post your TextKit version.
Here you go: https://gist.github.com/erica/fe785645fb28a783e0f851b24d37dd6d
Am I wicked to point out that halfWidth + halfWidth != letterSize due to floating point inexactness?
Nah, but I would point out this use case doesn’t need to be exact