Practical Dynamic Type: Part 2, Testing
adjustsFontForContentSizeCategory cannot be unit tested, let’s make our own!
Here is an example of a view that incorporates Dynamic Type features using Chris’s
FontMetrics wrapper from Part 1:
That is not unit–testable. What are we going to do about it?
The reason that we cannot unit-test whether or not the label responds to Dynamic Type is that it is not possible to write to the system’s Dynamic Type setting:
UIScreen.main.traitCollection.preferredContentSizeCategory. It is a read-only source of the current system-wide setting. One possible solution (and the one we happen to use at Livefront) is to disregard the system’s
preferredContentSizeCategory and use our own “source of truth” for the
UIContentSizeCategory. This works because of two key reasons:
FontMetricscan observe the system’s Dynamic Type size change and read the new size.
- Crucially, we can also ask
UIFontto use our own
contentSizeinstead of the system’s when calling its
With those two key components, we can implement our own Dynamic Type but with our own writable
So what does that actually look like?
Before explaining what the changes to
FontMetrics are, here is the updated class:
Here is how I arrived at this new code:
FontMetrics needs to use its own source of
UIContentSizeCategory and not the system’s
preferredContentSizeCategory. This is reflected in lines 41–45. Notice that assigning a new value posts a notification for all observers of FontMetric’s
Secondly, this is not demonstrated in this article, but I changed over any references in my app’s code of the system-provided
UIContentSizeCategoryDidChange notification to our own:
Third, in case that the user changes his or her Dynamic Type setting on their device while the app is running,
FontMetrics observes for those changes and then changes its own
sizeCategory to match the new system setting for a seamless experience (which then posts a notification). We never want the user to notice that anything is different than how they expect system settings to work.
Fourth, now that
sizeCategory behaves exactly as we would like, we request
UIFontMetrics.default.scaledFont() to use our
ContentSizeCategory instead of the system’s own. This can be seen in lines 59, 69, 87, and 100.
Fifth, although this wasn’t strictly necessary, I created a static constant called
default so that calling
FontMetrics.default would always return the same instance. This is akin to
UIDevice.current. It just helps make users of
FontMetrics keep their code clean-looking and bug-free (by avoiding accidentally using a different instance of
FontMetrics. This also meant that any references to
FontMetrics() in my app was replaced with
Basically, we’ve recreated the mechanism behind
UIContentSizeCategoryDidChange and made our own writable version of
To take advantage of our new
FontMetrics wrapper, we must update
Hal9000 as well!
There is an important trade off that was made here: we can’t keep using the convenient property called
adjustsFontForContentSizeCategory = true as there is no way to ask it to observe our own
sizeCategory. Plus, we want to have more control over our text than
adjustsFontForContentSizeCategory offers. At Livefront, we’ve rolled our own
UILabel replacement that is even more convenient to use than setting
true. As a side-effect, we rarely have to actually observe the
FontMetricsContentSizeCategoryDidChange notification because our custom
UILabel (and a plethora of other controls) is already doing that for us behind the scenes.
Onto unit tests!
Finally, the part we’ve been waiting for. Let’s test
Hal9000’s Dynamic Type features!
Note that while we’re measuring the same
subject.dialog.font.pointSize three times, we’ll get a different result as we’re changing
FontMetric.default.sizeCategory before each measurement!
With this power, there are many things we can choose to do with it. Here are just some ideas:
- Unit test Auto Layout with many different font sizes, especially complex layouts involving UIStackViews that changes axis according to its contents
- Write (and be able to test!) custom controls and views that implement Dynamic Type features
- Design unit-testable interfaces which reduce white spaces at higher levels of Dynamic Type size (e.g. a form with vertical white space between fields that get closer together as the text gets bigger instead of the fields getting further apart)
- Sleep better at night knowing that any regression of Dynamic Type features will be caught! 🎉
So… What’s next?
At Livefront, we’ve rolled unit-testable Dynamic Type and many other features into our own versions of
UIButton, and others. It has made building new interfaces so much faster and much more reliable, and we encourage you to have your own personalized toolbox that is right for you.
Here, you can download the Xcode project and run the tests yourself!
- “Delivering an Exceptional Accessibility Experience” (WWDC 2018)
- “UIKit: Apps for Every Size and Shape” (WWDC 2018)
Thanks to Sean Berry, Collin Flynn, and Mike Bollinger for their editorial feedback!