WWDC Quick Look đź’“ By SwiftGGTeam
Prepare your tvOS apps for Dynamic Type

Prepare your tvOS apps for Dynamic Type

Watch original video

Highlight

tvOS 27 introduces system-level Larger Text support for the first time. By replacing hard-coded fonts with semantic text styles and fixed constraints with flexible layouts, developers can let apps automatically adapt across seven text sizes from Large to Accessibility XXXL. Apps can also declare Larger Text support in App Store Accessibility Nutrition Labels to reach users who need it.

Key Takeaways

Imagine browsing movies on Apple TV from the couch. Your parents walk over, squint, and say, “The text is too small. I can’t read it.” You open Settings and increase the text size to the maximum, but half the title is clipped, button labels crowd together, and the whole app becomes unusable.

That was the normal state for every app before tvOS 27. iOS has supported Dynamic Type for years, but Apple TV was missing it. tvOS 27 finally fills that gap and brings system-level font scaling to the living room screen.

Users can go to Settings > Accessibility > Display & Text Size > Text Size and choose anything from Large through Accessibility XXXL. Standard UIKit and SwiftUI components such as Label, Button, and Navigation TabBar respond to that setting automatically. Your remaining job is to find custom, hard-coded text and layouts, then make them flexible.

Apple also provides an extra discovery surface: you can mark Larger Text support for tvOS in App Store Accessibility Nutrition Labels. That makes your app easier to find for users who actively look for accessible apps.

Three Issues You Must Audit

When you run your app in Larger Text mode, focus on three categories of problems.

The first is hard-coded font sizes. For example, a title using .system(size: 28) will not change no matter what the user selects in Settings. In Larger Text mode it will look out of place, and it may even be smaller than the surrounding text.

The second is fixed width and height constraints. A Text view locked to frame(width: 300) can only truncate when the text gets bigger. Or an HStack with spacing hard-coded to 40 pt may not have enough room once the text grows, causing content to crowd together.

The third is layout that does not adapt to text size. A poster wall may show six items per row at the standard size. Once text grows, the title under each poster gets longer and six columns no longer fit. If you do not reduce the column count, titles get clipped and the whole interface feels cramped.

Details

From Hard-Coded Values to Semantic Styles (04:58)

The session demonstrates the idea with a media app’s “Signup information” screen. The original code looks like this:

VStack(spacing: 20) {
  Text("Signup information")
    .font(.caption.bold())
    .lineLimit(1)
    .foregroundStyle(.secondary)
    .frame(width: 300, alignment: .leading)
  HStack(alignment: .top, spacing: 40) {
    // ...
  }
}

There are three problems here: .font(.caption.bold()) uses a semantic style, but other parts of the view hierarchy still contain hard-coded font sizes; .frame(width: 300) locks the width; and spacing: 40 may not be enough once the text grows.

The first fix is to replace the fixed width with a flexible constraint.

VStack(spacing: 20) {
  Text("Signup information")
    .font(.caption.bold())
    .lineLimit(1)
    .foregroundStyle(.secondary)
    .frame(maxWidth: .infinity, alignment: .leading)
  HStack(alignment: .top, spacing: 40) {
    // ...
  }
}

Key points:

  • maxWidth: .infinity tells SwiftUI to give this Text as much width as it needs instead of truncating.
  • The semantic .caption style automatically scales with Dynamic Type. You do not need to calculate a scale factor yourself.

Dynamic Type in UIKit (05:55)

If you use UIKit, the approach is similar, but there is one extra switch:

// Hard-coded font size. Do not do this.
titleLabel.font = UIFont.boldSystemFont(ofSize: 28)

// The right approach
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.adjustsFontForContentSizeCategory = true

Key points:

  • UIFont.preferredFont(forTextStyle:) returns a font size that matches the user’s setting.
  • adjustsFontForContentSizeCategory = true lets the label update automatically when the user changes text size in Settings, with no app restart required.

Use Container-Relative Frames to Adjust Grid Columns (07:09)

Poster walls are one of the most common tvOS app surfaces. Six columns may feel spacious at the standard text size, but Larger Text often needs four columns so each poster title has enough room.

struct MovieShelf: View {
  @Environment(\.dynamicTypeSize) private var dynamicTypeSize

  var body: some View {
    ScrollView(.horizontal) {
      LazyHStack(spacing: 40) {
        ForEach(Asset.allCases) { asset in
          Button {
            // ...
          } label: {
            asset.portraitImage
            Text(asset.title)
          }
          .containerRelativeFrame(
            .horizontal,
            count: dynamicTypeSize.isAccessibilitySize ? 4 : 6,
            spacing: 40)
        }
      }
    }
  }
}

Key points:

  • @Environment(\.dynamicTypeSize) reads the current text size.
  • dynamicTypeSize.isAccessibilitySize checks whether the user has selected an Accessibility text size from AX1 through AX5.
  • containerRelativeFrame(.horizontal, count:spacing:) divides the ScrollView’s available width across the requested number of child views and handles spacing automatically.
  • When isAccessibilitySize is true, the shelf shows four columns; otherwise it shows six.

If titles are especially long, you can also consider a custom marquee effect that scrolls text horizontally while the item has focus.

Switch Conditional Layouts with AnyLayout (08:07)

Content cards often place an image and text side by side. At Larger Text sizes, that horizontal layout can squeeze the text. The solution is to switch to a vertical stack for accessibility text sizes.

struct CardContentView: View {
  @Environment(\.dynamicTypeSize) private var dynamicTypeSize
  var asset: Asset

  var body: some View {
    let layout = dynamicTypeSize.isAccessibilitySize ?
      AnyLayout(VStackLayout(alignment: .leading, spacing: 10)) :
      AnyLayout(HStackLayout(alignment: .top, spacing: 10))
    layout {
      asset.image
      Text(asset.title)
      Text(asset.subtitle)
        .foregroundStyle(.secondary)
    }
  }
}

Key points:

  • AnyLayout is SwiftUI’s type-erased layout container. It lets you switch layout strategies at runtime.
  • Using AnyLayout instead of an if/else that swaps between VStack and HStack preserves view identity, which prevents the tvOS focus system from losing the current focus during layout changes.
  • VStackLayout and HStackLayout are concrete implementations of the layout protocol, and both can be wrapped by AnyLayout.

Respond to Content Size Changes in UIKit (08:31)

UIKit listens for font size changes through Trait Changes:

class AdaptiveLayoutViewController: UIViewController {
  let stackView = UIStackView()

  override func viewDidLoad() {
    super.viewDidLoad()
    updateLayout()

    let sizeTraits: [UITrait] = [UITraitPreferredContentSizeCategory.self]
    registerForTraitChanges(sizeTraits, action: #selector(updateLayout))
  }

  @objc private func updateLayout() {
    if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
      stackView.axis = .vertical
    } else {
      stackView.axis = .horizontal
    }
  }
}

Key points:

  • UITraitPreferredContentSizeCategory.self is the trait type to observe.
  • registerForTraitChanges(_:action:) replaces the older NSNotification approach on iOS/tvOS 17 and later, and it is more type-safe.
  • traitCollection.preferredContentSizeCategory.isAccessibilityCategory checks whether the current size is an Accessibility text size.
  • Switching UIStackView.axis between .horizontal and .vertical achieves the same effect as SwiftUI’s AnyLayout.

Ideas to Build

A TV shopping app that works for older adults

What to build: When Larger Text is enabled, change product cards from a horizontal arrangement to a vertical stack, let price information scale automatically with the .headline style, and expand product descriptions from a one-line summary to multiple lines.

Why it is worth doing: Older users often need Larger Text most, but fixed layouts tend to truncate or overlap at larger sizes. Flexible layouts keep the interface usable at every text size and expand the audience for the app.

How to start: Read the current text size with @Environment(\.dynamicTypeSize), then use AnyLayout to switch from HStackLayout at standard sizes to VStackLayout at Larger Text sizes.

An adaptive poster wall for a video-on-demand app

What to build: Use a six-column poster wall at standard text sizes, then reduce it to four columns with containerRelativeFrame for Larger Text so long titles have enough room. If titles are still too long, start a marquee scroll when the poster gets focus.

Why it is worth doing: Movie title length varies a lot, and a fixed column count causes truncation at Larger Text sizes. Adaptive column counts preserve readability across text sizes.

How to start: Detect text size with @Environment(\.dynamicTypeSize), then use containerRelativeFrame(.horizontal, count:spacing:) to show four columns when isAccessibilitySize is true and six otherwise.

Declare accessibility support on the settings page

What to build: In App Store Connect Accessibility Nutrition Labels, mark Larger Text support for the tvOS app.

Why it is worth doing: This is a zero-code change, but it can surface your app in accessibility filters. Users who specifically look for accessible apps can find it more easily.

How to start: Sign in to App Store Connect, open the app information page’s Accessibility section, and select Larger Text support for tvOS.

Clean up legacy code with regular expressions

What to build: Search an older project for UIFont.systemFont(ofSize: and .system(size:, then replace them with UIFont.preferredFont(forTextStyle:) and semantic text styles. Search for frame(width: and frame(height: and evaluate which constraints can become flexible.

Why it is worth doing: Hard-coded font sizes and fixed constraints are the biggest obstacles to Dynamic Type support in legacy code. Batch cleanup is the shortest path to giving an older app Larger Text support.

How to start: Use your IDE’s global search for those patterns, then replace each case with semantic styles and flexible constraints such as maxWidth: .infinity or minHeight:.

Test font adaptation with the tvOS focus system

What to build: On an Apple TV, set text size to the largest value, navigate through every screen, and verify that focus rings fully wrap content, text is not clipped, and layouts do not shift incorrectly.

Why it is worth doing: tvOS focus navigation is sensitive to layout changes. At Larger Text sizes, focus rings can be offset or incomplete. Real-device testing finds problems the simulator may miss.

How to start: In Apple TV Settings, set text size to Accessibility XXXL. Use the remote to traverse every screen, focusing on button labels, poster titles, and detail-page body text.

Comments

GitHub Issues · utterances