Scaling Dynamic Type with Font Descriptors

If you’re supporting dynamic type you may find that you want to change the weight, style or size of the text. In this post I show a quick tip to scale the fonts that extends the usefulness of the built-in styles.

Last updated: May 19, 2021

Font Descriptors

You can change attributes for a font by first getting a font descriptor for the font. For example, to get a font descriptor for the built-in headline text style:

// Swift
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .headline)
// Objective-C
UIFontDescriptor *descriptor = [UIFontDescriptor
  preferredFontDescriptorWithTextStyle: UIFontTextStyleHeadline];

Once you have the descriptor you can use it to create a modified version of the original font. From the UIFontDescriptor class reference:

UIFontDescriptor objects provide a mechanism to describe a font with a dictionary of attributes. This font descriptor can be used later to create or modify a UIFont object. Font descriptors can be archived and unarchived.

For example, to make a bold version of the subheadline text style:

// Swift
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline)
if let boldDescriptor = descriptor.withSymbolicTraits(.traitBold) {
  let boldSubhead = UIFont(descriptor: boldDescriptor, size: 0)
}
// Objective-C
UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleSubheadline];
UIFontDescriptor *boldDescriptor = [descriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
UIFont *boldSubhead = [UIFont fontWithDescriptor:boldDescriptor size:0];

Using zero for the size parameter creates a font with the original point size.

Scaling the Font

If you only want to scale the original text style font we can avoid using the descriptor. I’ve created an extension on UIFont to return the scaled font directly:

// UIFont+Extension.swift
import UIKit

extension UIFont {
  static func preferredFont(forTextStyle style: UIFont.TextStyle, scaleFactor: CGFloat) -> UIFont {
    let font = UIFont.preferredFont(forTextStyle: style)
    return font.withSize(font.pointSize * scaleFactor)
  }
}
// UIFont+UYLScaledFont.h
@interface UIFont (UYLScaledFont)
+ (UIFont *)preferredFontForTextStyle:(NSString *)style
  scale:(CGFloat)scaleFactor;
@end

// UIFont+UYLScaledFont.m
@implementation UIFont (UYLScaledFont)
+ (UIFont *)preferredFontForTextStyle:(NSString *)style
    scale:(CGFloat)scaleFactor {
  UIFont *font = [UIFont preferredFontForTextStyle:style];
  return [font fontWithSize:font.pointSize * scaleFactor];
}
@end

Updating the User interface

To provide an example of how to apply the scaled fonts I have added a second view controller to the DynamicText example project that shows each of the built-in text styles scaled up by a factor of two.

We configure each of the text labels using our UIFont extension:

// Swift
private func configureView() {
  let scaleFactor: CGFloat = 2.0;
  title1Label.font = UIFont.preferredFont(forTextStyle: .title1, scaleFactor: scaleFactor)
  title2Label.font = UIFont.preferredFont(forTextStyle: .title2, scaleFactor: scaleFactor)
  ...
}
// Objective-C
- (void)configureView {
  CGFloat scaleFactor = 2.0;
  self.title1Label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle1 scale:scaleFactor];
  self.title2Label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle2 scale:scaleFactor];
  ...
}

We need to make sure to set the scaled version of the fonts when the view is loaded and any time the user changes the text size. That means we need to listen for the notification, we cannot rely on the automatic font adjustment mechanism introduced in iOS 10:

// Swift
override func viewDidLoad() {
  super.viewDidLoad()
  configureView()
  NotificationCenter.default.addObserver(self,
    selector: #selector(updateTextStyles(_:)),
    name: UIContentSizeCategory.didChangeNotification,
    object: nil)
}

@objc private func updateTextStyles(_ notification: Notification) {
  configureView()
}
// Objective-C
- (void)viewDidLoad {
  [super viewDidLoad];
  [self configureView];
  [[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(updateTextStyles:)
    name:UIContentSizeCategoryDidChangeNotification
    object:nil];
}

- (void)updateTextStyles:(NSNotification *)notification {
  [self configureView];
}

Some Limitations

When you use a UIFontDescriptor for one of the built-in text styles you are limited in which attributes you can modify. You can change any of the symbolic traits, so for example you could derive a new descriptor that was italic as follows:

// Swift
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style)
let myDescriptor = descriptor.withSymbolicTraits(.traitItalic)
// Objective-C
UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:style];
UIFontDescriptor *myDescriptor = [descriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic];

However if you attempt to change the font family or face it does not have any effect:

// Swift
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .headline)
let myDescriptor = descriptor.withFamily("Verdana")!
let myFont = UIFont(descriptor: myDescriptor, size: 0)
// Objective-C
UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:style];
UIFontDescriptor *myDescriptor = [descriptor fontDescriptorWithFamily:@"Verdana"];
UIFont *myFont = [UIFont fontWithDescriptor:myDescriptor size:0.0];

If you want to use dynamic type with a custom font you need to create it from scratch. See Using A Custom Font With Dynamic Type for an example.

Wrapping Up

You can find the updated example Dynamic Text Xcode project in my GitHub CodeExamples repository.

If you want learn more about using dynamic type to build adaptive layouts take a look at my book Modern Auto Layout.