How many times have you seen a perfectly laid out mockup that was then implemented in elegant code that immediately breaks when a slightly different text size or locale is imposed upon it? Such cruelty!
Building a design that’s responsive to both its contents and its environment is a one of the primary challenges of robust user interface programming. There are some false gods out there and some legitimate best practices. However, I’ve found a new strategy that really helps, especially for text that has been scaled up for accessibility reasons.
I don’t talk a lot about UI on this blog, so you’ll have to forgive the ASCII art drawing. The feature in question describes a specific week:
---------------------------------------------
| <- | Week of December 22, 2024 | [Now] | -> |
---------------------------------------------
There’s a “back” button, a description of the week, a button to jump back to the current date (which isn’t visible if you’re looking at the current week), and a “forward” button.
The description of the week takes up the most space here. It doesn’t really fit unless it has the most favorable conditions (large phone screen, no “Now” button, default font size, etc). Nobody wants to see an ellipsis in their app’s UI, so the tempting maneuver here is to simplify the date string to make it shorter, and move on. We found that lot of devices and contexts would just work by making the month shorter (“Dec” instead of “December”).
This was kind of an ugly solution, and it bothered me when I looked at it. Seeing a bunch of empty space when I was in a context that would fit a wider month was a bit sad to look at. Of course, it also was very fragile; any modification that reduces the amount of text that could fit there (such as scaling up the text size) would immediately break it again.
So it’s not going to work to just shorten the string a little bit to get the text to fit. It is an interesting idea, though, that you can make the date take up less space by configuring it slightly differently. How do you get it to show the narrower string only if it needs to? You could try to key it off the dynamicTypeSize
, which you can pull out of the environment:
@Environment(\.dynamicTypeSize) var dynamicTypeSize
But, this would ultimately be vague guesswork, and might break based on other factors, like screen size.
Fortunately, SwiftUI comes with a tool that helps us pick the best fitting option out of a series of compatible views, and it’s called ViewThatFits
.
For this particular situation, I created a bunch of options to format the string, from widest to narrowest.
let titleOptions = [
"Week of \(weekdayStart.formatted(.dateTime.month(.wide).day().year()))",
"Week of \(weekdayStart.formatted(.dateTime.month().day().year()))",
"Week of \(weekdayStart.formatted(.dateTime.month().day().year(.twoDigits)))",
"Week \(weekdayStart.formatted(.dateTime.month(.defaultDigits).day().year(.twoDigits)))",
"W \(weekdayStart.formatted(.dateTime.month(.defaultDigits).day().year(.twoDigits)))",
]
These options would render as:
"Week of December 22, 2024"
"Week of Dec 22, 2024"
"Week of Dec 22, 24"
"Week 12/22/24"
"W 12/22/24"
Then, when it was time to choose the best fitting option, a quick ViewThatFits
with all the options enumerated in a ForEach
yields the best fitting view with no ellipses.
ViewThatFits(in: .horizontal) {
ForEach(titleOptions, id: \.self) { title in
Text(title)
.font(.subheadline)
}
}
With this change, SwiftUI’s sizing system will now choose the best title option that will fit within the space that we have available. There are other uses of ViewThatFits for accessibility purposes, namely these two posts, but those are both focused on switching from a horizontal layout to a vertical layout. While that strategy is useful, the approach laid out here is slightly different.
I like this solution because it solves a lot of problems. It supports more screen sizes, more contexts, locales that have wider text on average (such as German), contexts where less space is available (like the addition of the “Now” button), and of course, varying dynamic font sizes.
(By the way, this code is part of a new app I’m working on, and I’m looking for more beta users to help playtest it. If you live with someone and have a little bit of a competitive streak, this might be an app for you. Reach out to me at soroush@khanlou.com.)