Implementing Dynamic Type on iOS 7

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.

Using the System Content Size

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.

Screenshot of the Text Size preferences in Settings

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.

Responding to Content Size Changes

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.

Screenshot of a table view at different text sizes

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 UIViewControllers, UIScrollViews, or plain UIViews, 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 UIScrollViews. 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.

Using Custom Fonts with Dynamic Type

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:].

Notable Limitations

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.
Even with these relatively minor limitations, the Dynamic Type system is great for improving the accessibility of your application to users with vision impairments. Implementation only requires an easy UIFont category and one NSNotification, so developers have little reason to skip Dynamic Type support.

The sample application is available on GitHub.

@jszumski