October 6, 2013
Apple debuted many new typographic APIs with iOS 7 and has made its new text options very apparent to end users by changing the default system font. Text Kit brings many powerful features of CoreText to a higher level of abstraction and dramatically expands the approachability of the text layout features that have been available to developers for some time. Along with the new system font, Apple has also added Dynamic Type, a feature that gives users the ability to choose a base text size that is used throughout the system in both first- and third-party applications. Changing font sizes poses some new challenges for developers, however if you are already using Apple-suggested technologies like Auto Layout and localized strings your app may already have some of the necessary flexibility. The sample application included with this post demonstrates how to adapt to changes in the base text size, how to use custom fonts with Dynamic Type, and best practices to use in your application.
The system content size is exposed to users in Settings > General > Text Size as a slider with seven different base sizes where the default size roughly matches the font sizes previously recommended for iOS 6 and below.
The NSString
value for the current setting is available to developers as the preferredContentSizeCategory
property of UIApplication
. Typically you won't need to use this value directly unless you are using a custom interface font, which is discussed in a later section of this post. Instead, you should use a new UIFont
method preferredFontForTextStyle:
where you previously would have used systemFontOfSize:
or boldSystemFontOfSize:
to create a UIFont
instance. This method takes one of six text style options that will allow Apple to choose an appropriate font, weight, and size adjusted for the user's current content size:
UIFontTextStyleHeadline
UIFontTextStyleSubheadline
UIFontTextStyleBody
UIFontTextStyleFootnote
UIFontTextStyleCaption1
UIFontTextStyleCaption2
These six style options combined with the seven content size options yield a large number of font permutations, each of which has been meticulously tweaked by Apple designers for optimal display. Because of the font size variances possible at runtime, apps must be structured to have fluid layouts and avoid hard-coded width or size values.
When an iOS application is loaded for the first time, its user interface will be drawn with the system's current content size at launch time, however developers can't rely on this value staying constant throughout the lifetime of the application. Users can use the multitasking features of iOS to switch to Settings and update the text size setting at any time. To respond to these changes at runtime, your controllers and views will need to listen for the UIContentSizeCategoryDidChangeNotification
:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userTextSizeDidChange) name:UIContentSizeCategoryDidChangeNotification object:nil];
Your implementation of userTextSizeDidChange
will need to update fonts on all visible UI elements and recalculate a layout for the updated sizes of those elements. It's important to note that you must apply a new UIFont
instance with preferredFontForTextStyle:
to get an updated size. Simply calling invalidateIntrinsicContentSize
or setNeedsLayout
will not automatically apply the new content size because UIFont
instances are immutable.
For UITableViewController
subclasses, both steps can be accomplished with a call to reloadData
or reloadRowsAtIndexPaths:
for the visible cells. The table controller will recalculate row heights for the new text size and tableView:cellForRowAtIndexPath:
will apply the new fonts. Note that this approach requires fonts to be set in cellForRowAtIndexPath:
and not in an init method. An alternative is to have each cell instance listen for UIContentSizeCategoryDidChangeNotification
and adjust its fonts internally, and then the controller is only required to update the layout.
For UIViewController
s, UIScrollView
s, or plain UIView
s, apply the new fonts to your interface elements. If you are using Auto Layout, adjusting the fonts will be enough to activate the layout engine and will also calculate a new contentSize
for UIScrollView
s. For manual layouts, call setNeedsLayout
to tell iOS to update the positions of all subviews. The sample application uses Auto Layout, and I highly encourage you to adopt it if you haven't already.
It is also important to note that these adaptive fonts can give non-optimal layout values if used directly. For example, the sample application's table view implements boundingRectWithSize:options:attributes:context:
to determine the appropriate height for the row:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *codename = self.codenames[indexPath.row]; CGRect codenameRect = [codename boundingRectWithSize:CGSizeMake(CGRectGetWidth(CGRectIntegral(tableView.bounds)) - 40, // 40 = 20pt horizontal padding on each side MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [self fontForBodyTextStyle]} context:nil]; return MAX(44.0f, CGRectGetHeight(CGRectIntegral(codenameRect)) + 20); // 20 = 10pt vertical padding on each end }If you put a breakpoint in this method and examine
codenameRect
, you'll notice that it has a size that includes fractional points:
(lldb) p codenameRect (CGRect) $1 = origin=(x=0, y=0) size=(width=101.43001, height=16.702)These values can be used for layout, however using fractional point values forces the device to interpolate and blend pixels when drawing to the screen. This extra processing can have noticeable effects if done across many subviews, especially while scrolling. The fix is easy: simply call
CGRectIntegral()
on any CGRect
before you use it for layout calculations. If you are using Auto Layout, the fix is even easier; the layout system takes care of fractional points automatically.
Custom fonts are a great way to have your app stand out from the crowd, and fortunately it's very easy to support Dynamic Type with a simple category on UIFont
. If you need a refresher on using custom fonts on iOS, this Stack Overflow post covers the basics and iOS Fonts lists all the available fonts included with the system. When implementing Dynamic Type with a custom font, the basic idea is to have a mapping between the UIContentSizeCategory
string constants and a font size. In the sample project I created a category called AvenirContentSize
that adds preferredAvenirFontForTextStyle:
to UIFont
:
+ (UIFont *)preferredAvenirFontForTextStyle:(NSString *)textStyle { // choose the font size CGFloat fontSize = 16.0; NSString *contentSize = [UIApplication sharedApplication].preferredContentSizeCategory; if ([contentSize isEqualToString:UIContentSizeCategoryExtraSmall]) { fontSize = 12.0; } else if ([contentSize isEqualToString:UIContentSizeCategorySmall]) { fontSize = 14.0; } else if ([contentSize isEqualToString:UIContentSizeCategoryMedium]) { fontSize = 16.0; } else if ([contentSize isEqualToString:UIContentSizeCategoryLarge]) { fontSize = 18.0; } else if ([contentSize isEqualToString:UIContentSizeCategoryExtraLarge]) { fontSize = 20.0; } else if ([contentSize isEqualToString:UIContentSizeCategoryExtraExtraLarge]) { fontSize = 22.0; } else if ([contentSize isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { fontSize = 24.0; } // choose the font weight if ([textStyle isEqualToString:UIFontTextStyleHeadline] || [textStyle isEqualToString:UIFontTextStyleSubheadline]) { return [UIFont fontWithName:@"Avenir-Black" size:fontSize]; } else { return [UIFont fontWithName:@"Avenir-Medium" size:fontSize]; } }Now wherever I want to use Avenir in my application I can use that method just as I would
preferredFontForTextStyle:
to use the system font. You'll notice that I'm scaling the fontSize
linearly, however you can certainly get fancy by adjusting along a curve or interchanging different weights within the same font family. Similar adjustments can be made for the textStyle
argument to make headlines bold or use a different font variant.
If your custom font is close enough to the system font that you can use its metrics, another option is to simply use the point size and/or other traits from +[UIFontDescriptor preferredFontDescriptorWithTextStyle:]
.
I've come across two limitations of the Dynamic Type system:
UITextField
: The placeholder font is not exposed in the public API and doesn't use the value of the font
property, therefore you can't adjust its size as the content size changes. This leads to a weird effect where the text adjusts normally but if the field is cleared it jumps back to the original font size when displaying the placeholder. You can most likely fix this by subclassing UITextField
and implementing placeholderRectForBounds:
and drawPlaceholderInRect:
with adaptive fonts yourself, however this is left as an exercise for the reader.MKAnnotationView
: The text in the system-provided callout does not adjust as the content size changes. Unfortunately, fixing this requires you to implement the entire callout yourself.UIFont
category and one NSNotification
, so developers have little reason to skip Dynamic Type support.
The sample application is available on GitHub.