WWDC Quick Look 💓 By SwiftGGTeam
Prepare your tvOS apps for Dynamic Type

Prepare your tvOS apps for Dynamic Type

观看原视频

Highlight

tvOS 27 首次引入系统级大字体支持,开发者只需将硬编码字体替换为语义化文本样式、将固定约束改为弹性布局,即可让 App 自动适配从 Large 到 Accessibility XXXL 的七档字体大小,同时可在 App Store 的 Accessibility Nutrition Labels 中标注支持大字体以触达有需求的用户。

核心内容

想象你正在沙发上用 Apple TV 浏览电影。你父母走过来,眯着眼说:“这字太小了,我看不清。“你打开设置,把字体调到最大,结果界面上的标题被截断了一半,按钮标签挤成一团,整个 App 变得没法用。

这就是 tvOS 27 之前所有 App 的常态。iOS 早在多年前就支持 Dynamic Type,但 Apple TV 一直缺席。tvOS 27 终于补齐了这个缺口,把系统级字体缩放带到了客厅大屏。

用户可以在「设置 > 辅助功能 > 显示与文字大小 > 文字大小」中,从 Large 一路调到 Accessibility XXXL。标准 UIKit 和 SwiftUI 组件——Label、Button、Navigation TabBar——会自动响应这个设置。你的任务只剩下一件事:找出那些自定义的、硬编码的文本和布局,把它们改成弹性的。

Apple 还提供了一个额外的曝光渠道:你可以在 App Store 的 Accessibility Nutrition Labels 中勾选 tvOS 的 Larger Text 支持。这会让专门寻找无障碍 App 的用户更容易发现你的作品。

三个必须排查的问题

当你把 App 跑在大字体模式下,重点找三类问题。

第一类是硬编码字号。比如一段标题用了 .system(size: 28),这个字无论用户怎么调设置都不会变。它在大字体模式下会显得格格不入,甚至比其他文字还小。

第二类是固定宽高约束。一个 Text 视图被 frame(width: 300) 锁死,文字放大后只能截断。或者一个 HStack 的间距写死了 40pt,字变大了但空间没变,内容就挤在一起。

第三类是布局没有随字体调整。一个海报墙在标准字体下能并排显示 6 张,字放大后每张海报下面的标题变长,6 列就塞不下了。如果不减少列数,标题会被截断,整个界面显得局促。

详细内容

从硬编码到语义化样式(04:58

Session 用一个媒体 App 的「注册信息」界面来演示。原始代码长这样:

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

这里有三个问题:.font(.caption.bold()) 虽然用了语义化样式,但整个视图结构里还有其他硬编码字号;.frame(width: 300) 把宽度锁死;spacing: 40 在字变大后可能不够。

修复第一步:把固定宽度改成弹性约束。

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

关键点:

  • maxWidth: .infinity 告诉 SwiftUI:这个 Text 需要多少宽度就给多少,不要截断。
  • 语义化样式 .caption 会自动随 Dynamic Type 缩放,不需要你手动计算比例。

UIKit 中的 Dynamic Type(05:55

如果你用 UIKit,做法类似,但多一个开关:

// 硬编码字号——不要这样做
titleLabel.font = UIFont.boldSystemFont(ofSize: 28)

// 正确的做法
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.adjustsFontForContentSizeCategory = true

关键点:

  • UIFont.preferredFont(forTextStyle:) 返回与用户设置匹配的字体大小。
  • adjustsFontForContentSizeCategory = true 让 Label 在用户在设置中调整字体大小时自动更新,不需要重启 App。

用容器相对帧调整网格列数(07:09

海报墙是 tvOS App 最常见的界面。标准字体下 6 列很宽敞,大字体下需要减少到 4 列,给每张海报的标题留足空间。

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)
        }
      }
    }
  }
}

关键点:

  • @Environment(\.dynamicTypeSize) 读取当前字体大小级别。
  • dynamicTypeSize.isAccessibilitySize 判断用户是否开启了 Accessibility 级别的大字体(AX1 到 AX5)。
  • containerRelativeFrame(.horizontal, count:spacing:) 把 ScrollView 的可用宽度均分给指定数量的子视图,自动处理间距。
  • isAccessibilitySize 为 true 时显示 4 列,否则显示 6 列。

如果标题特别长,还可以考虑自定义 Marquee 滚动效果,让文字在获得焦点时横向滚动显示完整内容。

用 AnyLayout 实现条件布局切换(08:07

内容卡片通常把图片和文字并排摆放。大字体下,并排布局会导致文字被压缩。解决方案:在无障碍字体下切换为垂直堆叠。

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)
    }
  }
}

关键点:

  • AnyLayout 是 SwiftUI 的类型擦除布局容器,允许你在运行时切换不同的布局策略。
  • 使用 AnyLayout 而不是 if/else 切换 VStack/HStack,可以保留视图的身份标识,避免 tvOS 焦点系统在布局切换时丢失当前焦点。
  • VStackLayoutHStackLayout 是布局协议的具体实现,可以被 AnyLayout 统一包装。

UIKit 中响应内容尺寸变化(08:31

UIKit 通过 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
    }
  }
}

关键点:

  • UITraitPreferredContentSizeCategory.self 是要监听的特征类型。
  • registerForTraitChanges(_:action:) 在 iOS/tvOS 17+ 中替代了旧的 NSNotification 监听方式,类型更安全。
  • traitCollection.preferredContentSizeCategory.isAccessibilityCategory 判断当前是否处于 Accessibility 字体级别。
  • UIStackView.axis.horizontal.vertical 之间切换,实现与 SwiftUI AnyLayout 相同的效果。

核心启发

做一个长辈友好的电视购物 App

做什么:检测到大字体时,把商品卡片从横向排列改成纵向堆叠,价格信息用 .headline 样式自动放大,商品描述从一行缩略改成多行展开。

为什么值得做:长辈用户最需要大字体,但固定布局在大字体下会截断或重叠。弹性布局让界面在任何字体大小下都能正常显示,扩大目标用户群体。

怎么开始:用 @Environment(\.dynamicTypeSize) 读取当前字体大小,用 AnyLayout 在标准字体下用 HStackLayout、大字体下切换为 VStackLayout

为视频点播 App 做自适应海报墙

做什么:标准字体下 6 列海报墙很美观,大字体下用 containerRelativeFrame 降到 4 列,给长标题留空间。如果标题仍然超长,给获得焦点的海报启动 Marquee 滚动。

为什么值得做:电影标题长度差异很大,固定列数在大字体下会导致标题截断。自适应列数让界面在不同字体大小下都保持可读性。

怎么开始:用 @Environment(\.dynamicTypeSize) 检测字体大小,containerRelativeFrame(.horizontal, count:spacing:)isAccessibilitySize 时显示 4 列,否则 6 列。

在设置页标注无障碍支持

做什么:在 App Store Connect 的 Accessibility Nutrition Labels 中为 tvOS App 勾选 Larger Text 支持。

为什么值得做:这是零代码的改动,但能让你的 App 在无障碍筛选中获得曝光。专门寻找无障碍 App 的用户更容易发现你的作品。

怎么开始:登录 App Store Connect,找到 App 信息页的 Accessibility 部分,勾选 tvOS 的 Larger Text 支持选项。

用正则批量清洗遗留代码

做什么:老项目里搜索 UIFont.systemFont(ofSize:.system(size:,全部替换为 UIFont.preferredFont(forTextStyle:) 和语义化样式。再搜索 frame(width:frame(height:,评估哪些可以改成弹性约束。

为什么值得做:遗留代码中硬编码字号和固定约束是 Dynamic Type 支持的最大障碍。批量替换是最短路径,让旧项目快速获得大字体适配能力。

怎么开始:用 IDE 全局搜索上述模式,逐个替换为语义化样式和 maxWidth: .infinityminHeight: 等弹性约束。

为 tvOS 焦点系统做字体适配测试

做什么:在 Apple TV 上把字体调到最大档,遍历所有界面验证焦点框是否完整包裹内容、文字是否被截断、布局是否错位。

为什么值得做:tvOS 的焦点导航系统对布局变化敏感,大字体下焦点框可能偏移或包裹不完整。真实设备测试比模拟器更能发现问题。

怎么开始:在 Apple TV 设置中将文字大小调到 Accessibility XXXL,用遥控器遍历每个界面,重点检查按钮标签、海报标题和详情页正文。

关联 Session

评论

GitHub Issues · utterances