Supporting Dynamic Text in your iOS apps
I recently gave a talk on Dynamic Fonts at Cocoaheads meeting. You can download the presentation and sample code from here.
iOS7 introduced a cool feature called Dynamic Fonts. What it essentially allows you to do is to set your preferred reading text size in Settings (General or Accessibility) app of your phone and voilà, all apps that support dynamic type automatically adjust to display text content according to the preferred size setting
In this post, I’ll go over what you would need to do to support dynamic type within your apps so that yours will be one of the cool apps that reacts to the preferred text size change!
UITextKit, Text Styles and Font Descriptors
iOS7 introduced UITextKit – a powerful framework that allows you to support rich text content, layouts and without the complexities of drawing it with Core Text or having to use UIWebView.
An important component of UITextKit is TextStyles. Text Styles describe an intended use of a font .
The table below shows the list of supported text styles and the font descriptors that define them for the case when we set the preferred text size slider settings to the center.
Text Styles
|
Font Descriptor
|
UIFontTextStyleHeadline
(headings) |
NSCTFontUIUsageAttribute = UICTFontTextStyleHeadline;
NSFontNameAttribute = ".AppleSystemUIHeadline"; NSFontSizeAttribute = 17 |
UIFontTextStyleSubheadline
(subheadings) |
NSCTFontUIUsageAttribute = UICTFontTextStyleSubhead;
NSFontNameAttribute = ".AppleSystemUISubhead"; NSFontSizeAttribute = 15; |
UIFontTextStyleBody
(body text) |
NSCTFontUIUsageAttribute = UICTFontTextStyleBody;
NSFontNameAttribute = ".AppleSystemUIBody"; NSFontSizeAttribute = 17; |
UIFontTextStyleFootnote
(footnotes) |
NSCTFontUIUsageAttribute = UICTFontTextStyleFootnote;
NSFontNameAttribute = ".AppleSystemUIFootnote"; NSFontSizeAttribute = 13; |
UIFontTextStyleCaption1
(standard captions) |
NSCTFontUIUsageAttribute = UICTFontTextStyleCaption1;
NSFontNameAttribute = ".AppleSystemUICaption1"; NSFontSizeAttribute = 12; |
UIFontTextStyleCaption2
(alternate captions) |
NSCTFontUIUsageAttribute = UICTFontTextStyleCaption2;
NSFontNameAttribute = ".AppleSystemUICaption2"; NSFontSizeAttribute = 11; |
When we adjust the preferred text size slider to the far right (maximum possible size), the text styles and corresponding font descriptors are as shown in the table below.
Notice that the NSFontSizeAttribute attribute changes based on the text size settings.
NOTE: While not obvious from table above, in addition to adjusting the size of the font, the system takes care of adjusting the font attributes so it works well for the given text size – such as adjusting the inter-letter spacing, making letters bolder and such.
Text Styles | Font Descriptor |
UIFontTextStyleHeadline (headings) |
NSCTFontUIUsageAttribute =UICTFontTextStyleHeadline; NSFontNameAttribute = ".AppleSystemUIHeadline"; NSFontSizeAttribute = 20; |
UIFontTextStyleSubheadline (subheadings) |
NSCTFontUIUsageAttribute = UICTFontTextStyleSubhead; NSFontNameAttribute = ".AppleSystemUISubhead"; NSFontSizeAttribute = 18; |
UIFontTextStyleBody (body text) |
NSCTFontUIUsageAttribute = UICTFontTextStyleBody; NSFontNameAttribute = ".AppleSystemUIBody"; NSFontSizeAttribute = 20; |
UIFontTextStyleFootnote (footnotes) |
NSCTFontUIUsageAttribute = UICTFontTextStyleFootnote; NSFontNameAttribute = ".AppleSystemUIFootnote"; NSFontSizeAttribute = 16; |
UIFontTextStyleCaption1 (standard captions) |
NSCTFontUIUsageAttribute = UICTFontTextStyleCaption1; NSFontNameAttribute = ".AppleSystemUICaption1"; NSFontSizeAttribute = 15; |
UIFontTextStyleCaption2 (alternate captions) |
NSCTFontUIUsageAttribute = UICTFontTextStyleCaption2; NSFontNameAttribute = ".AppleSystemUICaption2"; NSFontSizeAttribute = 14; |
So what we observe is that depending on user text size preferences , the font (as described by its font descriptor) corresponding to each of the Text Style changes. This is how Dynamic Types are enabled.
A sample project that demonstrates the dynamic text capability can be downloaded from this link. Use the project as reference and follow along the rest of the post .
Enabling Dynamic Font Support In your App using System Text Styles :
Step 1: Use preferredFontForTextStyle: to specify the font to be used to display text content using an appropriate UI element (UITextView, UILabel etc). As an example, lets assume we have a UILabel text container called headlingLabel that holds a heading type text.
self.headingLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
Step 2: Add an observer for UIContentSizeCategoryDidChangeNotification notification to detect changes to user’s preferred text size settings. So whenever the user changes the Text Size settings preference, the app will be notified of the change via this notification when it is activated again.
if (&UIContentSizeCategoryDidChangeNotification != nil)
{
[[NSNotificationCenter defaultCenter]addObserverForName:UIContentSizeCategoryDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self handleTextSizeChangeNotification];
}];
}
Step 3: React to UIContentSizeCategoryDidChangeNotification notification by resetting the font associated with the text containers and sending a setNeedsLayoutfor text containers laid out manually or a invalidateIntrinsicContentSize for text containers laid out using AutoLayout. So in our example, we reset the font associated with our headingLabel UILabel. Since the font used by the text styles change dynamically depending on text size, the UILabel is now associated with a font matching user’s text size preferences.
-(void)handleTextSizeChangeNotification
{
self.headingLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
[self.headingLabel invalidateIntrinsicContentSize];
}
Pretty easy?
Now, what if you wanted to support your own TextStyles ? Read on…
Enabling Dynamic Type Support In your App using Custom TextStyles
Let’s suppose you are defining a custom font for a heading type and you want the a heading style that is bold and italicized and of size 24.0 points. For this, you need to follow these steps –
Step 1: First retrieve the UIFontDescriptor associated with UIFontTextStyleHeadline.
UIFontDescriptor* fontDesc = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline];
Step 2: Update the font descriptor attributes appropriately per required font traits
// Adjust font trait
UIFontDescriptorSymbolicTraits newSymbolicTrait = fontDesc.symbolicTraits | UIFontDescriptorTraitItalic|UIFontDescriptorTraitBold;
// Adjust font descriptor per trait
UIFontDescriptor* updatedFontDesc = [fontDesc fontDescriptorByAddingAttributes:@{UIFontDescriptorTraitsAttribute:@{UIFontSymbolicTrait:[NSNumber numberWithInteger:newSymbolicTrait]}}];
Step 3: Use a scaling factor to adjust the font size to the desired size. Assuming that we want to use a UILabel called customHeadingLabel to contain the heading.
// Get default font size
CGFloat pointSize = fontDesc.pointSize;
// Scale it per requirement
float scale = 1.1; // since we wanted 24 point
self.customheadingLabel.font = [UIFont fontWithDescriptor:updatedFontDesc size:pointSize* scale];
Step 4: As in the earlier case, add an observer for UIContentSizeCategoryDidChangeNotification notification to detect changes to user’s preferred text size. Then, when your app receives a UIContentSizeCategoryDidChangeNotification, scale the font as specified in Steps 3.
UIFontDescriptor* fontDesc = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline];
self.customheadingLabel.font = [UIFont fontWithDescriptor:fontDesc size:pointSize* scale];
You can define a category on UIFont that encapsulates the all the custom font styles that you’d use in your app. For example, this category includes a custom heading style.
@interface UIFont (CustomStyles)
+(UIFont*)ls_preferredHeadingFont;
@end
@implementation UIFont (CustomStyles)
// Defines a custom heading style
+(UIFont*)ls_preferredHeadingFont
{
UIFont* font = NULL;
// Get font descriptor associated with default UIFontTextStyleHeadline
UIFontDescriptor* fontDesc = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline];
// Get its point size (This would change depending on user's text size preferences)
CGFloat pointSize = fontDesc.pointSize;
// Set the traits as appropriate (Bold, italics in this case)
UIFontDescriptorSymbolicTraits newSymbolicTrait = fontDesc.symbolicTraits | UIFontDescriptorTraitItalic|UIFontDescriptorTraitBold;
// Create new font descriptor for your font
UIFontDescriptor* updatedFontDesc = [fontDesc fontDescriptorByAddingAttributes:@{UIFontDescriptorTraitsAttribute:@{UIFontSymbolicTrait:[NSNumber numberWithInteger:newSymbolicTrait]}}];
// Scale the default UIFontTextStyleHeadline style as appropriate
float scale = 1.1;
// Create the font with new descriptor
font = [UIFont fontWithDescriptor:updatedFontDesc size:pointSize* scale];
return font;
}
@end
NOTE:
When you adjust the size of the text, the bounding rect / frame of the UILabel needs to be adjusted to accomodate the resized text. You can use auto layout and let the system take care of resizing of the frame for you or you can take care of it yourself using the following –
self.headingLabel.numberOfLines = 0;
[self.headingLabel sizeToFit];
The sample project can be downloaded from here