How does Dynamic Type work, how do you support it in UIKit and SwiftUI, and how do layouts adapt at accessibility sizes?

24.2.1 · L2 · Interview

Predict first

How big does 17-point body text get at the default, the largest standard and the largest accessibility size, and are the accessibility sizes flagged?

import UIKit

@MainActor func run() {
    func body(_ c: UIContentSizeCategory) -> String {
        let scaled = UIFontMetrics(forTextStyle: .body).scaledValue(for: 17, compatibleWith: UITraitCollection(preferredContentSizeCategory: c))
        return String(format: "%.1f", scaled)
    }
    print("large", body(.large), "xxxLarge", body(.extraExtraExtraLarge))
    print("AX1", body(.accessibilityMedium), UIContentSizeCategory.accessibilityMedium.isAccessibilityCategory)
    print("AX5", body(.accessibilityExtraExtraExtraLarge), UIContentSizeCategory.accessibilityExtraExtraExtraLarge.isAccessibilityCategory)
    print(UIContentSizeCategory.accessibilityLarge > UIContentSizeCategory.large)
}
MainActor.assumeIsolated { run() }
  • large 17.0 xxxLarge 22.3; AX1 26.3 true; AX5 48.0 true; true
  • It stays 17 at every size
  • Only the accessibility sizes scale

Predict first

Are accessibility type sizes flagged in SwiftUI, and ordered above the standard sizes?

import SwiftUI

print(DynamicTypeSize.accessibility1.isAccessibilitySize, DynamicTypeSize.xxxLarge.isAccessibilitySize, DynamicTypeSize.large < DynamicTypeSize.accessibility3)
  • true false true
  • false false false
  • true true true

Predict first

Does a row that switches from horizontal to vertical at accessibility sizes, with a scaled icon, compile?

import SwiftUI

struct Row: View {
    @Environment(\.dynamicTypeSize) var size
    @ScaledMetric(relativeTo: .body) var icon: CGFloat = 24
    var body: some View {
        let layout = size.isAccessibilitySize ? AnyLayout(VStackLayout(alignment: .leading)) : AnyLayout(HStackLayout())
        layout {
            Image(systemName: "bell").frame(width: icon, height: icon)
            Text("Notifications are switched on for this conversation").font(.body)
        }
    }
}
  • Yes
  • No, layouts cannot change with the type size

Mental model. Dynamic Type is a room with movable walls. Furniture that is bolted to fixed positions breaks the moment the walls move; furniture that flows with the walls always fits.

Users choose a preferred text size in Settings (and per app), and apps are expected to follow it. Use text styles (.body, .headline, .title, .caption…) rather than fixed point sizes: in SwiftUI .font(.body) scales automatically; in UIKit use UIFont.preferredFont(forTextStyle:) and set adjustsFontForContentSizeCategory = true so labels update live. For a custom font, wrap it with UIFontMetrics(forTextStyle:) (scaledFont(for:)) or Font.custom(_:size:relativeTo:) so it scales along the style’s curve. Non-text sizes that should grow with text (an icon beside a label, padding) use UIFontMetrics.scaledValue(for:) or SwiftUI’s @ScaledMetric. There are seven standard sizes and five larger accessibility sizes; the check below shows 17-point body text becoming 22.3 at the largest standard size and 48 at the largest accessibility size, close to three times larger, and isAccessibilityCategory or SwiftUI’s dynamicTypeSize.isAccessibilitySize tells you when you are in that range.

The layout must reflow, not clip. Let labels wrap (numberOfLines = 0, no lineLimit(1) on important text), avoid fixed heights, put content in scroll views, and at accessibility sizes change the arrangement: a row with an icon beside a sentence becomes a vertical stack (the SwiftUI sample switches layout on isAccessibilitySize; in UIKit change the stack view’s axis in traitCollectionDidChange). Avoid minimumScaleFactor as a fix, because it shrinks text back to unreadable. For bars and compact controls that cannot grow (tab bars, toolbars), enable the large content viewer (showsLargeContentViewer, .accessibilityShowsLargeContentViewer()) so a long press shows the item enlarged. Cap the maximum size only when you must (dynamicTypeSize(...DynamicTypeSize.accessibility2)) and test at the largest size anyway: it is the quickest way to find truncated labels and overlapping views. Use the simulator’s environment overrides or Xcode previews to flip sizes quickly.

Scaling follows a curve per style. The accessibility categories are flagged and scale far more.

The accessibility sizes are the five largest and are flagged.

A view can read the type size from the environment and choose its layout.

Common traps

  • Fixed font sizes and fixed heights. Text clips or overlaps at larger sizes, and small text cannot be enlarged.
  • Fixing overflow with minimumScaleFactor. It makes the text small again, which defeats the setting.

They might follow up with

  • How do you scale a custom font? Through UIFontMetrics or Font.custom(…, relativeTo:).
  • What do you do with a toolbar that cannot grow? Support the large content viewer so the item can be shown large on long press.

Related questions

  • 24.2.2 How do you respect Reduce Motion, Increase Contrast, Bold Text, Reduce Transparency, Differentiate Without…
  • 24.2.3 What contrast ratio and touch-target size should you meet, and how do you design for colour blindness?
  • 24.5.1 How do you test accessibility: the Accessibility Inspector, automated audits in XCUITest, identifiers versus…
  • 7.7.1 What is a trait collection, and how do size classes and trait changes drive adaptive layout?

How do you respect Reduce Motion, Increase Contrast, Bold Text… →