Home iOS & Swift Books Auto Layout by Tutorials

11
Dynamic Type Written by Jayven Nhan

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Dynamic Type is an iOS feature that enables app content to scale according to the user’s font size preference. For the millions of people without perfect vision, having support for Dynamic Type makes all the difference. Without it, your app’s user experience will likely suffer.

Visual impairment is one of the world’s leading disabilities. Yet, most apps on the App Store fail to support Dynamic Type. But there’s no reason your apps have to fall into that category.

In this chapter, you’ll learn about:

  • Reasons for supporting Dynamic Type.
  • Supporting Dynamic Type on an existing app.
  • Preferred font sizes.
  • Growing and shrinking text.
  • Supporting Dynamic Type using custom fonts.
  • Growing and shrinking non-text UI elements.
  • Managing layout changes based on font preferences.

By the end of this chapter, you’ll know how to add support for Dynamic Type in your iOS apps.

Why Dynamic Type?

Dynamic Type makes your app usable by a broader audience, regardless of age or eyesight. In this section, you’ll learn about five reasons to support Dynamic Type.

Readability

Having a readable app is important. Supporting larger font sizes may not make a huge difference to someone with near-perfect vision, but many do not fall into that category. Look at the following image:

Temporary or chronic injuries

You may think that users seldom need to change their text size preferences once they find a size that works. However, sometimes a person’s eyesight deteriorates. Whether the deterioration is temporary or chronic, Dynamic Type can help ease the burden with large font size.

Competition differentiation

Building apps accessible for everyone isn’t always easy, but an accessible app differentiates itself from non-accessible apps. A comprehensive feature-packed app isn’t worth much to users if they can’t see what they’re doing. When it comes to downloading apps, users will almost always choose the one that brings more value and is easier to use, so make sure your app is beautiful at all text sizes.

High user retention rate over time

If your app accommodates all users regardless of how well they see, users won’t need to go looking for an alternate solution should they ever need to increase the app’s font size. Consequently, the app retention rate goes up over time.

Monetary gains

You can gain more users by ensuring that your app works equally great for all who use it. So, don’t regularly tax your user’s brainpower, which can make using your app exhausting. Instead, let users access their font preferences so they can use your app with relative ease. This makes for happy users, and happy users can’t wait to come back for more.

Setting the preferred font size

Before you begin, the first step is to change your device’s preferred font size. On iOS 13, follow these steps:

Making labels support Dynamic Type

Dynamic Type almost works right out of the box. With Storyboards and text styles, you can size a label’s text.

Making custom fonts support Dynamic Type

Making custom fonts dynamic requires a few more steps. At the time of writing this chapter, Interface Builder does not support setting up dynamic custom fonts. Consequently, you need to set a dynamic custom font using code.

fileprivate let customFontSizeDictionary: 
  [UIFont.TextStyle: CGFloat] =
  [.largeTitle: 34,
   .title1: 28,
   .title2: 22,
   .title3: 20,
   .headline: 17,
   .body: 17,
   .callout: 16,
   .subheadline: 15,
   .footnote: 13,
   .caption1: 12,
   .caption2: 11]

enum FontWeight: String {
  case regular = "Regular"
  case bold = "Bold"
}

enum CustomFont: String {
  case avenirNext = "AvenirNext"
  case openDyslexic = "OpenDyslexic"
}
extension UILabel {
  func set(
    customFont: CustomFont,
    fontWeight: FontWeight,
    textStyle: UIFont.TextStyle = .body
  ) {
    // 1
    let name = "\(customFont.rawValue)-\(fontWeight.rawValue)"
    guard let size = customFontSizeDictionary[textStyle]
      else { return }
    // 2
    guard let font = UIFont(name: name, size: size)
      else { fatalError("Retrieve \(name) with error.") }
    // 3
    let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
    self.font = fontMetrics.scaledFont(for: font)
    // 4
    adjustsFontForContentSizeCategory = true
  }
}
private func setupDynamicCustomFont(_ customFont: CustomFont) {
  headerLabel.set(
    customFont: customFont,
    fontWeight: .regular,
    textStyle: .title1)
  descriptionLabel.set(
    customFont: customFont,
    fontWeight: .regular)
}
setupDynamicCustomFont(.openDyslexic)

Managing layouts based on the text size

In addition to having readable text, you need your app to look good when users change their text size. In code, you’ll need to identify the user’s preferred font size to make that happen. The first way to identify the user’s preferred font size is to get the view’s trait collection.

func updateTraitCollectionLayout() {
  // 1
  let preferredContentSizeCategory =
    traitCollection.preferredContentSizeCategory
  // 2
  let scaledValue = UIFontMetrics.default
    .scaledValue(for: profileImageViewWidth)
  profileImageViewWidthConstraint.constant = scaledValue
  // 3
  topStackView.axis =
    preferredContentSizeCategory.isAccessibilityCategory
    ? .vertical : .horizontal
  // 4
  profileImageStackView.isHidden = preferredContentSizeCategory
    == .accessibilityExtraExtraExtraLarge
}
updateTraitCollectionLayout()
override func traitCollectionDidChange(
  _ previousTraitCollection: UITraitCollection?
) {
  super.traitCollectionDidChange(previousTraitCollection)
  guard traitCollection.preferredContentSizeCategory !=
    previousTraitCollection?.preferredContentSizeCategory
    else { return }
  let indexPaths = (0..<messages.endIndex)
    .map { IndexPath(row: $0, section: 0) }
  DispatchQueue.main.async { [weak self] in
    self?.tableView.reloadRows(at: indexPaths, with: .none)
  }
}

Supporting Dynamic Type with UITableView

Dynamic Type works right out of the box with the standard UITableView. By default, the header and cell height dynamically scale to fit the header and cell content.

Supporting Dynamic Type with UICollectionView

The standard UICollectionView requires more work to support self-sizing cells. Specifically, when you use a collection view for non-line based layouts, you’ll need to set up a custom UICollectionViewLayout.

Preparing custom collection view layouts

Open CollectionViewLayout.swift. Add the following properties to CollectionViewLayout:

// 1
weak var delegate: CollectionViewLayoutDelegate?
// 2
private var columns: Int {
  return UIDevice.current.orientation ==
    .portrait ? 1 : 2
}
// 3
private let cellPadding: CGFloat = 8
// 4
private var contentWidth: CGFloat = 0
private var contentHeight: CGFloat = 0
// 5
private var contentBounds: CGRect {
  let origin: CGPoint = .zero
  let size = CGSize(
    width: contentWidth,
    height: contentHeight)
  return CGRect(origin: origin, size: size)
}
// 6
private var cachedLayoutAttributes:
  [UICollectionViewLayoutAttributes] = []
override func prepare() {
  super.prepare()
  // 1
  guard let collectionView = collectionView
    else { return }
  cachedLayoutAttributes.removeAll()
  // 2
  let size = collectionView.bounds.size
  let safeAreaContentInset = collectionView.safeAreaInsets
  collectionView.contentInsetAdjustmentBehavior = .always
  contentWidth = size.width -
    safeAreaContentInset.horizontalInsets
  contentHeight = size.height -
    safeAreaContentInset.verticalInsets
  // 3
  makeAttributes(for: collectionView)
}

Creating collection view layout attributes

Add the following code to makeAttributes(for:):

// 1
guard let delegate = delegate else { return }
let itemWidth = contentWidth / CGFloat(columns)
// 2
var xOffsets: [CGFloat] = []
(0..<columns).forEach {
  xOffsets.append(CGFloat($0) * itemWidth)
}
// 3
var column = 0
// 4
var yOffsets = [CGFloat](repeating: 0, count: columns)
// 5
let items = 0..<collectionView.numberOfItems(inSection: 0)
for item in items {
  // 1
  let indexPath = IndexPath(item: item, section: 0)
  // 2
  let itemHeight = makeItemHeight(
    atIndexPath: indexPath,
    itemWidth: itemWidth,
    withCollectionView: collectionView,
    delegate: delegate)
  // 3
  let frame = CGRect(
    x: xOffsets[column], 
    y: yOffsets[column],
    width: itemWidth, 
    height: itemHeight)
  // 4
  let insetFrame = frame.insetBy(
    dx: cellPadding, 
    dy: cellPadding)
  // 5
  let layoutAttributes =
    UICollectionViewLayoutAttributes(
      forCellWith: indexPath)
  layoutAttributes.frame = insetFrame
  cachedLayoutAttributes.append(layoutAttributes)
  // 6
  contentHeight = max(contentHeight, frame.maxY)
  // 7
  yOffsets[column] += itemHeight
  // 8
  column = column < columns - 1 ? column + 1 : 0
}
// 1
let imageHeight = delegate.collectionView(
  collectionView,
  heightForImageAtIndexPath: indexPath)
// 2
let labelText = delegate.collectionView(
  collectionView, 
  labelTextAtIndexPath: indexPath)
let maxLabelHeightSize = CGSize(
  width: itemWidth,
  height: CGFloat.greatestFiniteMagnitude)
let boundingRect = labelText.boundingRect(
  with: maxLabelHeightSize,
  options: [.usesLineFragmentOrigin],
  attributes:
    [NSAttributedString.Key.font:
     UIFont.preferredFont(forTextStyle: .headline)],
  context: nil)
let labelHeight = ceil(boundingRect.height)
// 3
let itemHeight = cellPadding * 2 + imageHeight + labelHeight
return itemHeight

Overriding layout attributes methods

Add the following method overrides to CollectionViewLayout:

// 1
override func layoutAttributesForItem(
  at indexPath: IndexPath)
  -> UICollectionViewLayoutAttributes? {
    return cachedLayoutAttributes[indexPath.item]
}
// 2
override func layoutAttributesForElements(
  in rect: CGRect)
  -> [UICollectionViewLayoutAttributes]? {
    return cachedLayoutAttributes.filter {
      rect.intersects($0.frame)
    }
}

Setting the collection view content size

Add the following property and method overrides to CollectionViewLayout:

// 1
override var collectionViewContentSize: CGSize {
  return contentBounds.size
}
// 2
override func shouldInvalidateLayout(
  forBoundsChange newBounds: CGRect) -> Bool {
  guard let collectionView = collectionView
    else { return false }
  return newBounds.size
    != collectionView.bounds.size
}

Setting up the collection view layout delegate

Open CollectionViewController.swift.

extension CollectionViewController: CollectionViewLayoutDelegate {
  func collectionView(
    _ collectionView: UICollectionView,
    heightForImageAtIndexPath indexPath: IndexPath
  ) -> CGFloat {
    return shapes[indexPath.item].image.size.height
  }
  
  func collectionView(
    _ collectionView: UICollectionView,
    labelTextAtIndexPath indexPath: IndexPath
  ) -> String {
    return shapes[indexPath.item].shapeName.rawValue
  }
}
guard let collectionViewLayout =
  collectionView.collectionViewLayout
    as? CollectionViewLayout else { return }
collectionViewLayout.delegate = self

Invalidating collection view layout for trait collection

Finally, add the following code to CollectionViewController:

override func traitCollectionDidChange(
  _ previousTraitCollection: UITraitCollection?
) {
  super.traitCollectionDidChange(previousTraitCollection)
  guard previousTraitCollection?.preferredContentSizeCategory
    != traitCollection.preferredContentSizeCategory 
    else { return }
  collectionView.collectionViewLayout.invalidateLayout()
}

Challenges

Build and run. Tap the Text Styles bar button item, and you’ll see InfoViewController. Your challenge is to make InfoViewController support Dynamic Type.

Key points

  • The Dynamic Type feature allows users to scale content sizes based on the device’s font size preference.
  • Supporting Dynamic Type makes your app usable for a broader audience, which comes with benefits, including being different from the competition and increasing app retention rate.
  • Apps using Apple’s SF font can easily support Dynamic Type.
  • You can implement custom fonts with Dynamic Type.
  • You can scale non-text user interface elements according to the font size preferences.
  • Stack view makes it easy to make layout changes according to the font preferences.
  • When a cell uses Auto Layout, the standard UITableView supports Dynamic Type out of the box.
  • For collection views using non-line based layouts, you can support Dynamic Type using custom collection view layouts.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.