Managing custom font text styles in Interface Builder and in code has always been a pain point for me when developing for iOS. For instance, what’s the best place to define text styles? Is it in the Interface Builder document? This grants you a great WYSIWYG experience, allows you to quickly change and iterate, and maybe even lets non-developers modify appearance. Or should it be in code? This makes changes to the appearance more explicit and easier to merge, as well as being an easier target for large-scale refactoring when an app-wide design change inevitably happens. I thought there must be a better way, and here’s my proposal. If you aren’t one for rhetoric, here’s some working code.
In both of the above scenarios, the text style is static: it’s defined either in the nib file or in the source file, and barring some runtime changes, it is constant. On the contrary, if your app uses the default iOS font, which is the beautifully utilitarian San Fransisco font since iOS 9, your app can take advantage of a system-level feature known as Dynamic Type. Dynamic Type allows the user to set the base font size used by the operating system on a scale from extra small to extra large. Apps that use system text styles can scale their fonts’ sizes up or down to account for the user’s preferences. But not all of us live in an all-white room where beauty can meet form and function: sometimes an app needs a custom font to express its own personality. It would be a shame if this requirement came at the cost of adjusting type size to a user’s preference, and fortunately, I have some code which lets you have the best of all these worlds:
- Define text styles in Interface Builder to take advantage of the WYSIWYG editor and great user experience;
- Change custom fonts quickly and easily by modifying source code;
- Adjust your custom font to user text style preferences using Dynamic Type.
The way that I accomplished this is two-fold: first you need to define your custom font’s various text styles, and secondly you need to override your Label’s and Text View’s font with your own with a custom subclass which overrides the font descriptor for text style method, as we’ll see.
I found out how to define custom sizes and font styles from this Stack Overflow post. The Stack Overflow answer isn’t quite right for me, because I wanted an all-Swift solution with all text styles, but it lays out the methodology for an implementation which I’ve used. Here’s the definition for UIFontTextStyleBody
:
extension UIFontDescriptor { class func preferredAvenirNextFontDescriptorWithTextStyle(style: String) -> UIFontDescriptor { var onceToken: dispatch_once_t = 0 var fontSizeTable: [String: Dictionary] = Dictionary() dispatch_once(&onceToken, { fontSizeTable = [ UIFontTextStyleBody: [ UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: 21, UIContentSizeCategoryAccessibilityExtraExtraLarge: 20, UIContentSizeCategoryAccessibilityExtraLarge: 19, UIContentSizeCategoryAccessibilityLarge: 19, UIContentSizeCategoryAccessibilityMedium: 18, UIContentSizeCategoryExtraExtraExtraLarge: 18, UIContentSizeCategoryExtraExtraLarge: 17, UIContentSizeCategoryExtraLarge: 16, UIContentSizeCategoryLarge: 15, UIContentSizeCategoryMedium: 14, UIContentSizeCategorySmall: 13, UIContentSizeCategoryExtraSmall: 12 ], ... ]}) let contentSize = UIApplication.sharedApplication().preferredContentSizeCategory let sizes = fontSizeTable[style]! let size = sizes[contentSize]! return UIFontDescriptor(name:self.preferredFontName(), size: CGFloat(size)) } }
This code will first key on your text style name, then key on your users content size, which will yield a font size that you or your designers define. Next, we’ll get an example nib going with some labels with all of the text styles we want to support. I added a label for each text style and placed them in a Stack View:
Interface Builder is going to show the system font with Apple’s font sizes. While we cannot change this about Interface Builder, we can change how our labels will interpret their text styles at runtime, and we can do that with a short and sweet label subclass, which we’ll set all our labels to. This way of getting a Label’s font description comes from this Stack Overflow post. Check it out:
class CFDTLabel: UILabel { override func awakeFromNib() { super.awakeFromNib() if let textStyle = self.font.fontDescriptor().objectForKey(UIFontDescriptorTextStyleAttribute) as? String { let font = UIFont(descriptor: UIFontDescriptor.preferredAvenirNextFontDescriptorWithTextStyle(textStyle), size: 0) self.font = font } } }
Now, each time we have a label and set its text style, it’ll ask out custom method for the text style we defined in our UIFont extension. And that’s it, when we run our app we see our beautiful custom font which adjusts wit Dynamic Type.
This approach will be well-suited to you if you need a solution which lets define text styles in Interface Builder, adjust text styles quickly across your whole application, and scales up or down to meet your user’s font preferences. This last point is an important feature for people with different eyesight than the average: I’ve seen people use smaller text to fit more on the screen and larger text to make it easier to see.