WWDC Quick Look 💓 By SwiftGGTeam
Get started with Dynamic Type

Get started with Dynamic Type

元の動画を見る

ハイライト

Dynamic Type は Apple プラットフォームの文字サイズ自動調整メカニズムです。ユーザーは設定で 7 段階の通常サイズと 5 段階の支援機能用大サイズから選べます。アプリはすべてのサイズに対応する必要があります。この Session では、文字スケーリング、レイアウト調整、画像・シンボル処理、Large Content Viewer によるフォールバックの 4 層で適応戦略を解説します。


主要内容

固定 17pt の文字と HStack による水平レイアウトでアプリを作ったとします。ほとんどのユーザーは不満を言いません。ある日、ユーザーがシステム文字サイズを最大の支援機能レベルに設定したとき、タイトルが切り詰められ、ボタンの文字は半分しか見えず、水平に並んだ 4 つのアイコンが固まってしまいます。そのユーザーはアプリを閉じました。

Dynamic Type はこの問題を解決するメカニズムです。ユーザーは「設定 > アクセシビリティ > 表示と文字サイズ > より大きな文字」で好みのサイズを選びます。システムは 7 段階の通常サイズを提供し、「より大きな支援機能サイズ」を有効にするとさらに 5 段階の大きいオプションが追加されます。アプリがシステム Text Styles を使えば、文字はユーザーの選択に自動的に追従します。固定サイズをハードコードしている場合、ユーザー設定は完全に無視されます。

この Session では 4 層で適応を解説します。第一に、固定サイズをシステム Text Styles に置き換える。第二に、大きなサイズで水平配置が収まらなくなったら垂直レイアウトに切り替える。第三に、大きなサイズでの画像と SF Symbols のスケーリング戦略。第四に、Tab Bar のように文字サイズに追従できないコントロールには Large Content Viewer をフォールバックとして使う。

詳細

1. システム Text Styles で文字をスケーリング

SwiftUI では .font() 修飾子で Text Style を指定します(03:53):

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .font(.title)
    }
}

キーポイント:

  • .font(.title) はシステム title 文字スタイルを使い、Dynamic Type に自動追従する
  • システムは body、headline、title など複数のスタイルを提供し、それぞれ視覚的階層を維持する

UIKit では preferredFont(forTextStyle:) を使い、adjustsFontForContentSizeCategory を設定します(04:06):

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let label = UILabel(frame: .zero)
        setupConstraints()
        label.text = "Hello, World!"
        label.adjustsFontForContentSizeCategory = true
        label.font = .preferredFont(forTextStyle: .title1)
        label.numberOfLines = 0
        self.view.addSubview(label)
    }
}

キーポイント:

  • adjustsFontForContentSizeCategory = true でシステム文字サイズ変更時にラベルのフォントが自動更新される
  • numberOfLines = 0 で任意行数を許可し、切り詰めを防ぐ
  • preferredFont(forTextStyle:) は対応スタイルの動的フォントを返す

問題検出ツール: Xcode Preview で Variants ボタンをクリックし、Dynamic Type Variants を選べばすべてのサイズ変体を一度にプレビューできます。Xcode デバッガーで Dynamic Type 設定を上書きすることも、支援機能監査で文字切り詰め・クリッピング・コントラスト問題を確認することもできます(04:36)。

2. 動的レイアウト切り替え

文字サイズが支援機能範囲に入ると、水平レイアウトが収まらなくなることがあります。SwiftUI では dynamicTypeSize 環境変数と AnyLayout で動的切り替えを行います(07:20):

import SwiftUI

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

    var dynamicLayout: AnyLayout {
        dynamicTypeSize.isAccessibilitySize ?
        AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())
    }

    let systemImageName: String
    let imageTitle: String

    var body: some View {
        dynamicLayout {
            FigureImage(systemImageName: systemImageName)
            FigureTitle(imageTitle: imageTitle)
        }
    }
}

キーポイント:

  • @Environment(\.dynamicTypeSize) で現在の文字サイズを読み取る
  • isAccessibilitySize で支援機能の大サイズかどうかを判定する
  • AnyLayout で実行時に HStackLayout と VStackLayout を切り替えられる
  • 単一 cell は支援機能サイズで上下(VStack)から左右(HStack)レイアウトに切り替わる

外側のコンテナも同期して切り替える必要があります(07:52):

import SwiftUI

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

    var dynamicLayout: AnyLayout {
        dynamicTypeSize.isAccessibilitySize ?
        AnyLayout(VStackLayout(alignment: .leading)) : AnyLayout(HStackLayout(alignment: .top))
    }

    var body: some View {
        dynamicLayout {
            FigureCell(systemImageName: "figure.stand", imageTitle: "Standing Figure")
            FigureCell(systemImageName: "figure.wave", imageTitle: "Waving Figure")
            FigureCell(systemImageName: "figure.walk", imageTitle: "Walking Figure")
            FigureCell(systemImageName: "figure.roll", imageTitle: "Rolling Figure")
        }
    }
}

キーポイント:

  • 外側は支援機能サイズで HStack から VStack に切り替わり、各 cell が画面幅いっぱいになる
  • alignment パラメータで各レイアウトに応じた配置を設定する

UIKit では UIStackView を使います(08:20):

import UIKit

class ViewController: UIViewController {
    private var mainStackView: UIStackView = UIStackView()

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        NotificationCenter.default.addObserver(self, selector: #selector(textSizeDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupStackView()
    }

    @objc private func textSizeDidChange(_ notification: Notification?) {
        let isAccessibilityCategory = self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory
        mainStackView.axis = isAccessibilityCategory ? .vertical : .horizontal
        setupConstraints()
    }
}

キーポイント:

  • UIContentSizeCategory.didChangeNotification を監視して文字サイズ変更に応答する
  • isAccessibilityCategory で支援機能サイズかどうかを判定する
  • UIStackView.axis を変更するだけで水平/垂直を切り替えられる

3. 画像と SF Symbols のスケーリング戦略

装飾用画像は拡大に追従すべきではありません。設定アプリの各リスト項目左側の小アイコンは大サイズでも元のサイズを保ち、文字はアイコンの周りに折り返されます。SwiftUI の List はこの折り返しを自動処理します(10:12)。UIKit では NSAttributedString + NSTextAttachment でインライン画像を実装し、文字が自然に回り込みます(10:30)。

画像を拡大に追従させる必要がある場合(文字や重要アイコンを含む画像など)、SwiftUI では @ScaledMetric を使います(11:05):

import SwiftUI

struct ContentView: View {
    @ScaledMetric var imageWidth = 125.0
    var body: some View {
        VStack {
            Image("Spatula")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: imageWidth)
            Text("Grill Party!")
                .frame(alignment: .center)
        }
    }
}

キーポイント:

  • @ScaledMetric プロパティラッパーで値が Dynamic Type に自動スケールする
  • 基準幅 125pt を指定し、実行時に文字サイズに応じて調整される
  • SF Symbols は @ScaledMetric 不要。システムが自動スケールする

UIKit では UIImage.SymbolConfiguration と textStyle を組み合わせます(11:38):

import UIKit

func imageWithBodyConfiguration(systemImageName: String) -> UIImage? {
    let imageConfiguration = UIImage.SymbolConfiguration(textStyle: .body)
    let configuredImage = UIImage(systemName: systemImageName, withConfiguration: imageConfiguration)
    return configuredImage
}

キーポイント:

  • SymbolConfiguration(textStyle: .body) で SF Symbol が body スタイルと同期してスケールする
  • Symbol 以外のカスタム画像は UITraitCollection で手動スケーリングが必要

4. Large Content Viewer フォールバック

Tab Bar のような常駐コントロールは文字サイズに追従して拡大できません。拡大すると Tab Bar が画面の 4 分の 1 を占めます。システム Tab Bar には Large Content Viewer が組み込まれています。カスタムコントロールには手動追加が必要です。

SwiftUI では .accessibilityShowsLargeContentViewer 修飾子を使います(13:15):

import SwiftUI

struct FigureBar: View {
    @Binding var selectedFigure: Figure

    var body: some View {
        HStack {
            ForEach(Figure.allCases) { figure in
                FigureButton(figure: figure, isSelected: selectedFigure == figure)
                    .onTapGesture {
                        selectedFigure = figure
                    }
                    .accessibilityShowsLargeContentViewer {
                        Label(figure.imageTitle, systemImage: figure.systemImage)
                    }
            }
        }
    }
}

キーポイント:

  • .accessibilityShowsLargeContentViewer はクロージャを受け取り、長押し時に表示する拡大ビューを返す
  • クロージャ内で Label を使い、文字とアイコンの両方を提供する
  • 長押しで中央に拡大表示が現れ、スワイプで隣接コントロールに切り替えられる

UIKit では UILargeContentViewerInteraction を使います(13:45):

import UIKit

class FigureCell: UIStackView {
    var systemImageName: String!
    var imageTitle: String!

    init(systemImageName: String, imageTitle: String) {
        super.init(frame: .zero)
        self.systemImageName = systemImageName
        self.imageTitle = imageTitle
        setupFigureCell()
        self.addInteraction(UILargeContentViewerInteraction())
        self.showsLargeContentViewer = true
        self.largeContentImage = UIImage(systemName: systemImageName)
        self.scalesLargeContentImage = true
        self.largeContentTitle = imageTitle
    }
}

キーポイント:

  • addInteraction(UILargeContentViewerInteraction()) でインタラクションを追加する
  • showsLargeContentViewer = true で表示を有効化する
  • largeContentImagelargeContentTitle でポップアップ内のアイコンと文字を設定する
  • scalesLargeContentImage = true でポップアップ内のアイコンを拡大する

重要ポイント

  • 何をするか: すべての固定サイズをシステム Text Styles に置き換える。なぜ価値があるか: Dynamic Type 対応の第一歩で、変更は小さく効果は大きい。文字がすぐにユーザー設定に追従する。始め方: SwiftUI では .font(.system(size: 17)).font(.body) / .font(.title) に置き換える。UIKit では UIFont.systemFont(ofSize:)preferredFont(forTextStyle:) に置き換え、adjustsFontForContentSizeCategory = true を設定する。

  • 何をするか: 水平配置のカード型レイアウトに「水平から垂直へ」の切り替え能力を持たせる。なぜ価値があるか: 支援機能大サイズでは水平スペースが足りず文字が切り詰められる。垂直レイアウトで各項目が幅いっぱいになり、読みやすくなる。始め方: SwiftUI では @Environment(\.dynamicTypeSize) + AnyLayoutisAccessibilitySize に応じて切り替える。UIKit では UIStackViewdidChangeNotification を監視して axis を切り替える。

  • 何をするか: カスタム Tab Bar とツールバーに Large Content Viewer を追加する。なぜ価値があるか: これらのコントロールは文字サイズに追従できず、大サイズユーザーが長押ししてもコンテンツラベルが見えず、ナビゲーション能力を失う。始め方: SwiftUI では .accessibilityShowsLargeContentViewer { Label(...) } を追加。UIKit では UILargeContentViewerInteraction を追加し largeContentTitlelargeContentImage を設定する。

  • 何をするか: UI テストに支援機能監査を組み込む。なぜ価値があるか: 各イテレーションで文字切り詰め、クリッピング、低コントラストなど Dynamic Type 問題を自動検出でき、手動のページごと確認が不要になる。始め方: XCTest で支援機能監査 API を呼び出し、異なる Dynamic Type 設定でテストを実行する。

関連セッション

コメント

GitHub Issues · utterances