Measuring Apple's Camera App From Screenshots
Apple does not publish the dimensions of the system camera's zoom dial, so I measured them: unroll the dial into polar coordinates, read brightness profiles, and recover font sizes from glyph ink width. Plus the snap check that fast drags skipped, and the habits the whole build left behind.
Yunseon Hong4 min read
Part six, the last of the notes from building Thirdstop, an iOS camera app. Some controls in the app are meant to feel like the system camera’s, the zoom dial above all. Apple publishes design guidance but not the geometry of its own Camera app, so I measured it from screenshots.
Everything here comes from screenshots on an iPhone 17 Pro with iOS 26.6.1, and the numbers are in points.
Unroll anything circular
The zoom dial is an arc. You can work back from the x positions of its labels, but that is imprecise. What worked much better was fitting a circle to the arc in the screenshot, then unrolling the image into angle by radius. After that the dial is a straight strip, and a brightness profile along it can be read like a ruler.
What that showed about the system dial:
- The tick scale is logarithmic: every doubling of zoom takes the same 48 points of arc.
- The dial’s radius is 240 points.
- Thick ticks fall on whole-number zoom factors.
- The dip in the ticks around the pointer is not a V shape. It is an exponential curve that falls to 1/e every 3.3 points of arc.
- The ends of the dial are not cut off. They fade out on a gradient.
That last one I had wrong by eye. The ends looked clipped. A yes-or-no judgment of “is it cut off” gives you whatever you expected to see. A brightness profile along the edge shows a gradient no matter what you expected.
Measure a capsule’s height at its vertical center
I measured the height of a capsule-shaped control near its rounded end and got 34 points. The real height, measured through the middle, is 48. I reverted that number twice before I noticed where I was measuring.
Recover font size from ink width
To match a label, I rendered the text at a range of candidate sizes with NSString.size(withAttributes:) and compared each width to the width of the ink in the screenshot. SF Rounded Semibold at 16 points matched to a fraction of a point. Ink height was useless for this. It was off by more than a point at every candidate size.
Fast drags jump over snap points
The dial snaps to certain zoom factors with a haptic tap. My first version checked whether the dial was within 8 points of a snap point on each frame. On a quick drag the haptic never fired. With the drag sensitivity multiplier the dial moved 17 points in a single frame and jumped clean over the 16-point window.
The check that works asks whether the dial passed the snap point between the previous frame and this one:
let crossed = (previous - detent) * (current - detent) <= 0
Exponential curves never arrive
That exponential dip caused a bug of its own. An animation curve built on it was supposed to end when its value reached 1. An exponential approach never reaches 1, so the branch never ran. It needs a threshold close enough to count as done.
Order of modifiers mattered in the same area. With .clipped() before .scaleEffect, the clipped edge scaled along with the content and the cut showed. Draw the content wider than needed, scale it, then clip.
Some positions are faster to match than to compute
Offsets measured from the safe area were the worst. I tried several times to compute where an element should sit and missed each time. Putting my screenshot beside the system camera’s screenshot and nudging until they matched was faster than any of those attempts.
When two elements have to line up, I stopped computing their coordinates separately and put them in the same ZStack so they share an origin. Two SwiftUI details decided a lot of these cases: .overlay does not affect the parent’s layout size, and a view at opacity(0) still takes up its full space.
Liquid Glass belongs behind text, not on top of it
With the glass effect placed over a label, the glass refracted the label itself, and the text smeared into several copies. Glass goes behind text. On a black camera background, translucent black is invisible, so I use glassEffect(.clear) on iOS 26 with a white fallback on older versions. The system .buttonStyle(.glass) did not fit one control either: its capsule is attached to the button’s frame, and the design needed a fixed capsule with items sliding underneath.
What the build left behind
Across six posts the same lesson kept coming back, and I now apply it before I start guessing. Numbers instead of eyes: luma correlation for orientation, a spectrum for image detail, a brightness profile for UI dimensions. Short debug probes that print each variant in one run, removed right after. And logging the resolved value instead of trusting the assigned one.
The rule I learned most expensively is this one: the second time I hear about the same symptom, I stop reasoning and build a way to check. A localization bug came back three times after three fixes. The cause was that the locale parameter of String(localized:locale:) is for formatting interpolated values. The reference says so in one line: it “doesn’t change which locale the system uses to look up the localized string.” A 20-line Swift script that printed the lookup would have made all three round trips unnecessary.
One small tool made the reading easier. Apple’s documentation pages come back empty when fetched by a script or an AI tool, because the content is rendered client side. The same content is available as JSON at developer.apple.com/tutorials/data/documentation/<path>.json, which is the dependable way to check an API detail from a script.
Filed under Build log