SwiftUI layout lessons from matching a design
I compared every Span screen with its design on a simulator the same size as the design. Most differences were spacing. A few were bugs that taught me how SwiftUI actually lays things out.
A big child makes a big ZStack
The room scan page has yellow dots that are meant to run past the edge of their card. I drew them in a ZStack, and the whole page became wider than the phone, which clipped the text.
In SwiftUI, a ZStack is as big as its biggest child. A 438 point wide decoration made the stack 438 points wide, and that pushed everything else out. Moving it with .offset did not help, because offset moves where something is drawn but not the space it takes up in the layout.
The fix was .overlay. An overlay is drawn on top of a view but never changes that view’s size, which is exactly what a decoration should do.
nil does not mean “go back to the system”
The Measure and Level screens are always dark, so they set .preferredColorScheme(.dark). Going back to the light Projects screen set it to nil, which I expected to hand control back to the system. On iOS it does not do that reliably. The clock in the status bar stayed white, on a white screen.
The fix was to read the real system setting and always pass an explicit .light or .dark, never nil.
Building your own means losing free behaviour
On iOS 26, the system tab bar became a floating glass capsule, and half height sheets float away from the screen edges. The design had neither, so I built both by hand. That matched the design, but a UI test later found that tapping the active tab did nothing. The system tab bar scrolls back to the top for free, and a custom one has to do it itself. Anything you rebuild, you have to rebuild all of, including the behaviour people expect without thinking about it.
The largest text sizes
iOS lets people make text much larger in Accessibility settings, and SwiftUI scales text to match. At the largest size, the Measure screen’s controls overflowed and collided.
The design had its own layout for very large text, so I built that. The first version was still too big. Its 20 point labels came out around 33 points, and “Capture” broke into “Captur” and “e”. The design’s sizes were already the final large sizes, and Dynamic Type scaled them a second time.
The fix was one line on that layout, .dynamicTypeSize(.large), which tells SwiftUI these numbers are already final. Text in the rest of the app stops growing at accessibility1, which is about the design’s large body size. The quote’s price field also cut off at that size, so it now grows with its text.