Jun 11

One Way to Handle Dynamic Type in iOS 7

In iOS 7, Apple introduced a new feature called Dynamic Type. This new set of APIs helped developers by allowing them to query the system for the best fonts to use in various situations. The effect was two-fold:

  1. Developers could specify the style for pieces of text they wished to display. For example, a blog post title could use UIFontTextStyleHeadline while the content of that post could use UIFontTextStyleBody.
  2. Users could change their preferred text size setting in the iOS Settings app. Apps that supported this feature would then automatically scale their user interface appropriately.

When I began developing Patient IO, I knew that it was going to be important to support Dynamic Type. We wanted the experience of our app to be great and accessible, even for individuals with poor eyesight. From Apple’s documentation, it became apparent to me that the recommended way to support Dynamic Type was to make full use of the various UIFontTextStyle values and to have the app listen for and respond to changes in the user’s text size preference. What wasn’t apparent was which pieces of text in my user interface should use which style. This was further complicated by my designer having his own opinion on how the various labels in the app should look.

I started down the painful path of trying out each text style at various text sizes and comparing them to the designs I was sent. I would then attempt to choose the style that was most like the one the designer had chosen. However, this was a time-consuming process that did not create a result that made either of us happy. In certain scenarios the choice of style seemed completely arbitrary or even contrary to the name of the style. I was using caption fonts in labels that shows subtitles and caption fonts in labels that showed body text. The more I used these styles the more I felt like they were created with certain types of apps in mind like blogging apps, twitter apps, and e-mail clients. So instead I decided to try and handle the text-sizing myself without using UIFontTextStyle at all.

iOS allows you to query for the user’s current text size directly using code like the following:

NSString *category =
    [[UIApplication sharedApplication] preferredContentSizeCategory];
// category will be one of:
// UIContentSizeCategoryExtraSmall
// UIContentSizeCategorySmall
// UIContentSizeCategoryMedium
// UIContentSizeCategoryLarge
// UIContentSizeCategoryExtraLarge
// UIContentSizeCategoryExtraExtraLarge
// UIContentSizeCategoryExtraExtraExtraLarge
// UIContentSizeCategoryAccessibilityMedium
// UIContentSizeCategoryAccessibilityLarge
// UIContentSizeCategoryAccessibilityExtraLarge
// UIContentSizeCategoryAccessibilityExtraExtraLarge
// UIContentSizeCategoryAccessibilityExtraExtraExtraLarge

At this point, it becomes relatively trivial to create our own method for determining what font sizes our application should be using at any given time. I opted for a method that took four parameters:

  1. smallestSize — The smallest font size the text should be.
  2. normalSize — The standard font size assuming the user has never changed their font size preference.
  3. maxNormalSize — The largest the font size can be assuming the user has not turned on Larger Text in the iOS Accessibility settings.
  4. maxAccessibilitySize — The absolute largest size this text can be if the user has enabled the Larger Text accessibility setting.

The code ended up looking as follows:

UIFont+FILAdditions.h
#import <UIKit/UIKit.h>

@interface UIFont (FILAdditions)

+ (CGFloat)fil_fontSizeGivenSmallestSize:(CGFloat)smallestSize
                              normalSize:(CGFloat)normalSize
                           maxNormalSize:(CGFloat)maxNormalSize
                    maxAccessibilitySize:(CGFloat)maxAccessibilitySize;

@end
UIFont+FILAdditions.m
@implementation UIFont (FILAdditions)

+ (CGFloat)fil_fontSizeGivenSmallestSize:(CGFloat)smallestSize
                              normalSize:(CGFloat)normalSize
                           maxNormalSize:(CGFloat)maxNormalSize
                    maxAccessibilitySize:(CGFloat)maxAccessibilitySize
{
    static NSDictionary *categories;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        categories = @{UIContentSizeCategoryExtraSmall: @0,
                       UIContentSizeCategorySmall: @1,
                       UIContentSizeCategoryMedium: @2,
                       UIContentSizeCategoryLarge: @3,
                       UIContentSizeCategoryExtraLarge: @4,
                       UIContentSizeCategoryExtraExtraLarge: @5,
                       UIContentSizeCategoryExtraExtraExtraLarge: @6,
                       UIContentSizeCategoryAccessibilityMedium: @7,
                       UIContentSizeCategoryAccessibilityLarge: @8,
                       UIContentSizeCategoryAccessibilityExtraLarge: @9,
                       UIContentSizeCategoryAccessibilityExtraExtraLarge: @10,
                       UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @11};
    });

    NSString *category =
        [[UIApplication sharedApplication] preferredContentSizeCategory];

    NSNumber *number = [categories objectForKey:category];
    if (number == nil) number = [categories objectForKey:UIContentSizeCategoryLarge];
    NSInteger index = [number integerValue];
    
    CGFloat size;
    switch (index)
    {
        case 0:
            size = smallestSize;
            break;
        case 1 ... 2:
            size = smallestSize + (normalSize - smallestSize) * ((CGFloat)index / 3.0);
            break;
        case 4 ... 5:
            size = normalSize + (maxNormalSize - normalSize) * ((CGFloat)(index - 3) / 3.0);
            break;
        case 6:
            size = maxNormalSize;
            break;
        case 7 ... 11:
            size = maxNormalSize + (maxAccessibilitySize - maxNormalSize) * ((CGFloat) (index - 6) / 5.0);
            break;
        case 12:
            size = maxAccessibilitySize;
            break;
        case 3:
        default:
            size = normalSize;
            break;
    }
    
    return round(size);
}

@end

To me, this method felt more flexible and powerful than trying to scale across all content sizes using just a smallest and largest text size. It allowed me to take certain labels and only scale them in a single direction. It let me make the maxNormalSize and maxAccessibility size the same in situations where the text didn’t need to scale to incredibly large sizes. It let me control and avoid situations where the max size of UIFontTextStyleBody was much larger than a particular screen could adequately handle. And maybe most importantly, this method gave me the ability to exactly match the mockups I had been given using any font I liked and still make the text in the app scale appropriately given the user’s text size preference.

If the iOS-defined text styles make sense for your particular application, you should certainly use them. However, if you find yourself struggling and trying to force the text styles into an app that wasn’t designed for them, consider using the fonts that work best for you and then scaling the font sizes as I did.