WWDC Quick Look 💓 By SwiftGGTeam
tvOSアプリをDynamic Typeに対応させる

tvOSアプリをDynamic Typeに対応させる

元の動画を見る

ハイライト

tvOS 27では、システムレベルの大きな文字サポートが初めて導入されます。ハードコードされたフォントをセマンティックなテキストスタイルに置き換え、固定制約を柔軟なレイアウトに変えるだけで、アプリはLargeからAccessibility XXXLまで7段階の文字サイズに自動対応できます。さらにApp StoreのAccessibility Nutrition Labelsで大きな文字への対応を示すことで、それを必要とするユーザーに見つけてもらいやすくなります。

重要ポイント

Apple TVで映画を探している場面を想像してみてください。家族が近づいてきて、目を細めながら「字が小さくて読めない」と言います。設定を開いて文字を最大まで大きくしたところ、タイトルは半分で切れ、ボタンのラベルは押し合い、アプリ全体が使いにくくなってしまいます。

tvOS 27以前のアプリでは、これが普通でした。iOSは何年も前からDynamic Typeをサポートしていましたが、Apple TVにはその仕組みがありませんでした。tvOS 27でようやくこの穴が埋まり、リビングの大画面にもシステムレベルのフォント拡大が入ります。

ユーザーは「設定 > アクセシビリティ > 画面表示とテキストサイズ > 文字サイズ」から、LargeからAccessibility XXXLまで選べます。Label、Button、Navigation TabBarなどの標準UIKit/SwiftUIコンポーネントは、この設定に自動で反応します。開発者に残る仕事は、カスタム実装されたハードコードのテキストやレイアウトを見つけて、柔軟な形に直すことです。

Appleは、見つけてもらうための追加の場所も用意しています。App StoreのAccessibility Nutrition Labelsで、tvOSのLarger Text対応にチェックを入れられます。アクセシビリティに対応したアプリを探しているユーザーに、あなたのアプリが届きやすくなります。

必ず確認したい3つの問題

アプリを大きな文字モードで動かすときは、特に3種類の問題を探します。

1つ目は、ハードコードされたフォントサイズです。たとえばタイトルに.system(size: 28)を使っていると、ユーザーが設定を変えてもその文字は変わりません。大きな文字モードでは周囲と合わず、ほかのテキストより小さく見えることさえあります。

2つ目は、固定された幅や高さの制約です。Textビューがframe(width: 300)で固定されていると、文字が大きくなったときに切り詰めるしかありません。あるいはHStackの間隔を40ptに固定していると、文字だけが大きくなり、内容が詰まって見えることがあります。

3つ目は、文字サイズに合わせて変わらないレイアウトです。標準サイズではポスターを6列で並べられても、文字を大きくすると各ポスター下のタイトルが長くなり、6列では収まらなくなります。列数を減らさなければタイトルが切れ、画面全体が窮屈に見えます。

詳細

ハードコードからセマンティックなスタイルへ(04:58

このセッションでは、メディアアプリの「Signup information」画面を例に説明しています。元のコードは次のような形です。

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

ここには3つの問題があります。.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は、このTextに必要なだけ幅を与え、切り詰めないようSwiftUIに伝えます。
  • セマンティックな.captionスタイルはDynamic Typeに合わせて自動で拡大縮小されるため、自分で倍率を計算する必要はありません。

UIKitでのDynamic Type(05:55

UIKitを使う場合も考え方は同じですが、追加で1つのスイッチが必要です。

// フォントサイズのハードコード。これは避ける。
titleLabel.font = UIFont.boldSystemFont(ofSize: 28)

// 正しい方法
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.adjustsFontForContentSizeCategory = true

ポイント:

  • UIFont.preferredFont(forTextStyle:)は、ユーザー設定に合ったフォントサイズを返します。
  • adjustsFontForContentSizeCategory = trueにすると、ユーザーが設定で文字サイズを変えたときにLabelが自動更新されます。アプリの再起動は不要です。

コンテナ相対フレームでグリッド列数を調整する(07:09

ポスターウォールはtvOSアプリでよく使われる画面です。標準文字サイズでは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で、ユーザーが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の型消去されたレイアウトコンテナで、実行時に異なるレイアウト戦略へ切り替えられます。
  • if/elseVStackHStackを入れ替えるのではなくAnyLayoutを使うと、ビューの同一性が保たれます。これにより、レイアウト切り替え時にtvOSのフォーカスシステムが現在のフォーカスを失うのを避けられます。
  • VStackLayoutHStackLayoutはLayoutプロトコルの具体実装で、どちらも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が監視するTraitの種類です。
  • registerForTraitChanges(_:action:)はiOS/tvOS 17以降で古いNSNotification方式を置き換えるもので、より型安全です。
  • traitCollection.preferredContentSizeCategory.isAccessibilityCategoryで、現在がアクセシビリティ文字サイズか判断します。
  • UIStackView.axis.horizontal.verticalで切り替えることで、SwiftUIのAnyLayoutと同じ効果を実現できます。

実装アイデア

高齢のユーザーにも使いやすいテレビショッピングアプリ

作るもの: 大きな文字を検出したら、商品カードを横並びから縦積みに変えます。価格は.headlineスタイルで自動拡大し、商品説明は1行の省略表示から複数行表示に切り替えます。

なぜ価値があるか: 高齢のユーザーほど大きな文字を必要としますが、固定レイアウトでは文字が切れたり重なったりしがちです。柔軟なレイアウトなら、どの文字サイズでも正常に表示でき、対象ユーザーを広げられます。

始め方: @Environment(\.dynamicTypeSize)で現在の文字サイズを読み取り、標準サイズではHStackLayout、大きな文字ではVStackLayoutAnyLayoutで切り替えます。

動画配信アプリ向けの適応型ポスターウォール

作るもの: 標準文字サイズでは6列のポスターウォールを使い、大きな文字ではcontainerRelativeFrameで4列に減らして長いタイトルの余白を確保します。それでも長い場合は、フォーカスされたポスターでMarqueeスクロールを始めます。

なぜ価値があるか: 映画タイトルの長さはばらつきが大きく、固定列数では大きな文字でタイトルが切れます。列数を適応させることで、文字サイズが変わっても読みやすさを保てます。

始め方: @Environment(\.dynamicTypeSize)で文字サイズを検出し、containerRelativeFrame(.horizontal, count:spacing:)isAccessibilitySizeのときは4列、それ以外は6列を表示します。

設定ページでアクセシビリティ対応を示す

作るもの: App Store ConnectのAccessibility Nutrition Labelsで、tvOSアプリのLarger Text対応にチェックを入れます。

なぜ価値があるか: コード不要の変更ですが、アクセシビリティの絞り込みでアプリが見つかる可能性が高まります。アクセシブルなアプリを探しているユーザーに届きやすくなります。

始め方: App Store Connectにログインし、アプリ情報ページの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にし、リモコンですべての画面を移動します。ボタンラベル、ポスタータイトル、詳細ページ本文を重点的に確認します。

関連セッション

コメント

GitHub Issues · utterances