Claude Media
Claude CodeでSwiftUIのアクセシビリティ抜けを洗う — ラベルとDynamic Type

Claude CodeでSwiftUIのアクセシビリティ抜けを洗う — ラベルとDynamic Type

画像ボタンのaccessibilityLabel欠落と固定フォントサイズを、grepとXcodeのAccessibility Inspector監査で洗い出し、Claude Codeに直させて、UIテストの監査で完了を判定する手順です。

SwiftUIのアクセシビリティ抜けは、画面を眺めても見つかりません。アイコンだけのボタンにVoiceOver用の名前が付いていなくても、固定ポイントのフォントが文字サイズの設定に追従しなくても、通常のサイズで動かす限り見た目は正常だからです。

この手の抜けは、検出と判定と修正を分けるとClaude Codeに任せやすくなります。検出はgrepとXcodeのAccessibility Inspectorの監査、判定と修正はClaude Code、完了の確認はUIテストの監査、という分担です。本記事ではその流れを、コマンドとSwiftコードで順に示します。

手順

洗い出しから完了判定までの流れ

  1. 1

    grepで候補を出す

    アイコンだけのボタンと、固定ポイントのフォント指定を一覧にします。

  2. 2

    Claude Codeに一件ずつ判定させる

    装飾か操作部品か、ラベルの文言は何か、を文脈から決めます。

  3. 3

    Accessibility Inspectorの監査と突き合わせる

    grepで拾えない抜け(文字の切れ、タップ領域)を実機の画面で補います。

  4. 4

    UIテストの監査で完了を決める

    監査が通るまでClaude Codeが止まらない構成にします。

SwiftUIのアクセシビリティ抜けは3つの型に分かれる

洗い出す対象は、次の3種類に絞れます。

抜けの型

洗う対象の3種類

  • 名前のない操作部品

    アイコンだけのボタンなど、文字を持たない部品にVoiceOver用の名前がない状態です。

  • 固定サイズの文字

    ポイント数で固定した指定が、ユーザーの文字サイズ設定に追従しない状態です。

  • 大きな文字で崩れる配置

    固定の高さや横並びのまま、文字が大きくなると切れる、はみ出す状態です。

1つ目と2つ目はソースコードの書き方で見つかります。3つ目は、実際に大きな文字サイズで画面を出さないと分かりません。このため、検出の手段も2系統に分けます。コードを読む系統はgrepとClaude Code、画面を測る系統はAccessibility InspectorとUIテストです。

SwiftUIが自動でやってくれる範囲を先に押さえる

SwiftUIは、ナビゲーション、リスト、テキストフィールド、スライダー、ボタンなどの一般的な部品を読み取り、基本的なラベルと値を既定で付けます。Appleのドキュメントも、標準の機能を有効にするために追加の作業は要らないと書いています。

つまり、洗う対象は「標準部品に任せた箇所」ではなく、「標準の推測が効かない箇所」です。代表がアイコンだけの部品です。

accessibilityLabel(_:) の説明は、アイコンのように文字を表示しないviewにラベルを付ける用途を挙げています。例は、音楽を再生するボタンに「再生」を意味する Play と付けるものです。同じ説明に、Play button のようにボタンであることまで書かないよう注意があります。ボタンかどうかはtraitが伝えるため、ラベルに重ねると読み上げが冗長になります。

Image には2つの扱いがあります。画像を操作部品として使うなら、label 引数を取る初期化子を使います。見た目のためだけの画像は decorative 引数の初期化子を使い、アクセシビリティ機能に無視させます。

この区別が、Claude Codeに判定させる中身そのものです。「画像なら全部にラベルを付ける」では、装飾画像まで読み上げられてしまいます。

手順1: grepで候補を出し、Claude Codeに一件ずつ判定させる

最初に、候補をソースから機械的に集めます。次のコマンドは、リポジトリのルートで実行する例です。

# SF Symbolsを使う箇所(アイコンだけの部品の候補)
grep -rn --include='*.swift' 'Image(systemName:' .
 
# ポイント数で固定したフォント(Dynamic Type非追従の候補)
grep -rnE --include='*.swift' \
  '\.font\(\.system\(size:|fixedSize:' .
 
# 固定の幅・高さ(大きな文字で切れる候補)
grep -rnE --include='*.swift' '\.frame\((width|height):' .

出力は「候補」であって「違反」ではありません。たとえば Image(systemName:) の直後に Text が並ぶ行は、ボタン全体がテキストから名前を得られるので、ラベル追加は不要かもしれません。逆に、Button { } label: { Image(...) } の形は、名前を足す必要がある典型です。

候補をClaude Codeに渡すときは、判定の基準を依頼文に書きます。例えば次のような依頼です。

Sources 配下の SwiftUI で、アイコンだけの Button と
Image(systemName:) を洗ってください。
- 操作部品なら accessibilityLabel を付ける
- 装飾なら accessibilityHidden(true) か Image(decorative:) にする
- 隣に Text があり名前が伝わる箇所は変更しない
- ラベルに「ボタン」とは書かない
変更前に、箇所ごとの判定理由を一覧で見せてください。

最後の一文が効きます。判定理由を先に見せてもらえば、人が確認できます。ラベルの文言は画面の文脈に依存するため、文言そのものは必ずレビューの対象にします。

固定フォントの側は、判定の軸が違います。AppleのFontのドキュメントには、body や headline のような標準のテキストスタイルがあり、Font.TextStyle は「動的なテキストスタイル」と説明されています。一方、system(size:weight:design:) はポイント数を直接指定する書き方で、ドキュメントの例は17ポイントです。カスタムフォントは custom(_:size:relativeTo:) なら指定したスタイルに対して拡大縮小し、custom(_:fixedSize:) はDynamic Typeで拡大縮小しない固定サイズです。

依頼文には、この区別を使います。

.font(.system(size:)) と Font.custom(_:fixedSize:) の箇所を
一覧にし、標準のテキストスタイルに置き換えられるかを
判定してください。意図的な固定(ロゴなど)は理由付きで残し、
置き換えは私が確認してから適用します。

「意図的な固定は残す」という逃げ道を用意するのがコツです。固定サイズを全消しすると、ロゴやバッジの意匠が崩れます。

手順2: Accessibility Inspectorの監査と突き合わせる

grepで拾えるのは、コードに現れる書き方だけです。実際の画面で文字が切れるか、タップ領域が足りるかは、アプリを動かして測ります。そのためにXcodeのAccessibility Inspectorを使います。

起動は、XcodeのメニューからXcode > Open Developer Tool > Accessibility Inspectorです。対象のアプリを選び、AuditsペインでRun Auditを押すと、現在の画面を監査します。画面ごとに実行する必要があります。ドキュメントは、ログイン画面、空の一覧、項目のある一覧のように、画面を分けて監査する例を挙げています。

iOSで選べる監査の種類と、この記事の対象との対応は次のとおりです。

監査の種類検出する内容対応する抜け
Element description検出する内容各要素のラベルの有無対応する抜け名前のない操作部品
Dynamic Type検出する内容文字がDynamic Typeに対応しているか対応する抜け固定サイズの文字
Clipped text検出する内容大きな文字サイズで切れる可能性対応する抜け崩れる配置
Hit region検出する内容要素が小さすぎないか対応する抜けタップ領域の不足
Contrast検出する内容重なる要素の色のコントラスト対応する抜けこの記事の対象外

Clipped textとDynamic Typeは、iOS・watchOS・tvOSで選べる監査です。Element description、Hit region、Contrastは全プラットフォーム共通です。

監査結果は、Claude Codeに次の形で渡せます。

  • Captureアイコンで、問題箇所に青いハイライトが付いたスクリーンショットを取る。画像は Ctrl+V で貼り付けるか、パスを書いて渡す
  • File > Save Audit Report As… で、全監査結果をまとめたHTMLを保存し、そのパスを渡す

画像の扱いはClaude Code公式のCommon workflowsに載っています。画像のドラッグ&ドロップ、Ctrl+V での貼り付け、パス指定の3通りです。

各監査の問題には、修正の提案(Fix suggestions)も付きます。色、フォントサイズ、使うAPIの指針が出ると説明されています。この提案は、Claude Codeに「この提案の趣旨を、このファイルの該当箇所に当てはめる」と頼む材料に使えます。

渡す依頼文は、箇所を限定すると結果が安定します。

添付の監査スクリーンショットは Element description の指摘です。
青く囲まれたボタンを、SettingsView.swift から特定して
accessibilityLabel を付けてください。

直し方の型: ラベル、装飾画像、Dynamic Type

判定が済んだ箇所の直し方を、型で示します。Claude Codeに「この型で直して」と渡すと、修正が揃います。

前と後

アイコンだけのボタンにラベルを付ける

BEFORE

名前がない

Button {
    player.play()
} label: {
    Image(systemName: "play.fill")
}
AFTER

用途を表す名前を付ける

Button {
    player.play()
} label: {
    Image(systemName: "play.fill")
}
.accessibilityLabel("再生")

飾りの画像は、読み上げの対象から外します。

// 画像ファイルの装飾
Image(decorative: "divider-wave")
 
// SF Symbolsの装飾
Image(systemName: "sparkles")
    .accessibilityHidden(true)

accessibilityHidden(_:) は、viewをシステムのアクセシビリティ機能から隠すための修飾子です。操作部品の中のアイコンを隠すときは、隣のテキストが名前になっているかを確かめてから使います。

Dynamic Type側は、固定指定を標準のテキストスタイルに置き換えるのが基本です。

// Before: ポイント数で固定
Text(title).font(.system(size: 17, weight: .semibold))
 
// After: テキストスタイルにして重みだけ指定
Text(title).font(.headline)

余白やアイコンの大きさのように、フォントではない数値を文字に合わせて動かしたいときは ScaledMetric を使います。Appleの説明では、数値を拡大縮小する動的プロパティです。

struct RowView: View {
    @ScaledMetric(relativeTo: .body) private var iconSize = 24
 
    var body: some View {
        Image(systemName: "bell")
            .frame(width: iconSize, height: iconSize)
    }
}

文字が極端に大きくなると、横並びでは収まりません。DynamicTypeSize は大きさを比較でき、accessibility で始まる5つのサイズは、それ以外よりはるかに大きいとされています。isAccessibilitySize を見て配置を切り替えます。

struct BatteryLabel: View {
    @Environment(\.dynamicTypeSize) private var size
 
    var body: some View {
        if size.isAccessibilitySize {
            VStack { BatteryIcon(); Text("充電中") }
        } else {
            HStack { BatteryIcon(); Text("充電中") }
        }
    }
}

これはAppleのドキュメントにある BatteryLabel の例を、isAccessibilitySize を使う形に寄せた書き方です。公式の例は dynamicTypeSize >= .accessibility1 で比較しています。

見た目の確認には、dynamicTypeSize(_:) 修飾子をプレビューに付ける方法があります。ドキュメントは、ContentView().dynamicTypeSize(.xLarge) をプレビューで別の大きさを見るのに便利だと説明しています。

#Preview {
    ContentView()
        .dynamicTypeSize(.accessibility3)
}

逆に、大きさの上限を決める ...DynamicTypeSize.large のような範囲指定は、慎重に使います。ドキュメントは、大きさを制限するなら accessibilityShowsLargeContentViewer() で大きなコンテンツビューアーを出すことを検討するよう添えています。Claude Codeに「崩れるから上限を付ける」と直させると、問題を隠すだけになりがちです。上限を付ける直し方は、依頼文で禁止しておく運用が安全です。

手順3: UIテストの監査で完了条件を機械にする

ここまでの手順は、人がAccessibility Inspectorを開く前提でした。繰り返し洗えるようにするには、監査をUIテストに入れます。

Appleのドキュメントによれば、XCUIApplication に対して performAccessibilityAudit(for:_:) を呼ぶと、現在の画面をAccessibility Inspectorと同じ方法で監査します。問題があればテストは自動的に失敗します。

import XCTest
 
final class AccessibilityAuditTests: XCTestCase {
    func testSettingsScreenAudit() throws {
        let app = XCUIApplication()
        app.launch()
        app.buttons["設定"].tap()
        try app.performAccessibilityAudit()
    }
}

監査は画面単位なので、画面ごとにテストメソッドを作ります。画面へ移動する操作もテストに書く必要があります。

テストが赤の間は終わらない構成にするのが、Claude Codeの運用側の工夫です。公式のBest practicesは、完了条件を確認する方法を、依頼文に書く方式から Stop フックで強制する方式まで段階的に紹介しています。Stop フックは終了コード2でClaudeを作業に戻し、標準エラーに出した内容が理由として渡されます。

公式の例は tsc ですが、同じ形をテストコマンドに置き換えられます。次は設定の一例です。MyApp と実機名は自分の環境に合わせて書き換えます。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=<端末名>' -only-testing:MyAppUITests/AccessibilityAuditTests -quiet 1>&2 || exit 2"
          }
        ]
      }
    ]
  }
}

実行ログが長いときの絞り方は、xcodebuildのエラーをxcresultで絞る手順にまとめています。

同じ公式ページには、フックが8回連続でブロックし、その間にツール呼び出しがないと、Claude Codeが次のブロックを無視してターンを終える仕様も書かれています。上限は環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変えられます。直せない指摘が残るときは、堂々巡りの前に人が介入する前提で設計します。

もう一点、Appleのドキュメントが明記する限界があります。監査の指摘をすべて解消しても、完全にアクセシブルなアプリになるとは限りません。VoiceOverなど支援技術での実機確認は、別に必要です。UIテストが通ったことは「検出できる抜けが消えた」という意味にとどまります。

踏みやすい落とし穴

落とし穴

装飾画像までラベルを付けてしまう

「画像にはラベルを」とだけ頼むと、見た目のための画像まで読み上げ対象になります。依頼文に「装飾は Image(decorative:) か accessibilityHidden(true)」の分岐を必ず入れます。

落とし穴

ラベルに「ボタン」と書く

ボタンであることはtraitが伝えます。「再生ボタン」ではなく「再生」とするよう、依頼文にもCLAUDE.mdにも書いておきます。

落とし穴

Dynamic Type の対応を上限指定で済ませる

文字が大きくなると崩れる画面に、大きさの上限だけを付けると、見た目は直っても対応にはなっていません。配置を切り替える直し方を先に検討します。

落とし穴

監査は1画面ぶんしか見ない

Accessibility InspectorもUIテストの監査も、対象は現在表示中の画面です。シートやアラート、空の状態の画面は、別に開いて監査します。

CLAUDE.mdに残す規約

毎回の依頼文に基準を書くのは手間です。Best practicesは、例外のない決まりは CLAUDE.md でなくフックなどの設定に移すよう勧めています。一方、判断を含む決まりは CLAUDE.md が向いています。ラベルの文言の書き方は後者です。

## アクセシビリティ(SwiftUI)
- 文字を持たない操作部品には accessibilityLabel を付ける。
  文言は用途を表し、「ボタン」とは書かない
- 装飾の画像は Image(decorative:) か accessibilityHidden(true)
- フォントは標準のテキストスタイルを使う。
  .system(size:) を使うときは理由をコメントに残す
- 固定の高さで文字を囲まない。必要なら ScaledMetric を使う
- 画面を追加したら AccessibilityAuditTests に監査を足す

「例外のない決まり」の側には、前節の Stop フックが当たります。

まとめ

SwiftUIのアクセシビリティ抜けは、コードで見つかる抜けと、画面でしか見つからない抜けに分かれます。前者はgrepの候補をClaude Codeに判定させ、後者はAccessibility Inspectorの指摘を材料にします。

直した後の確認は、UIテストの performAccessibilityAudit を Stop フックに乗せると、人が見る前に機械が一度止めてくれます。ただし、監査が通っても支援技術での確認は代わりになりません。

プロジェクト全体の進め方はSwiftUI iOSアプリの開発手順に、Web版のアクセシビリティ検査はPlaywrightとaxeをPRで落とす構成にあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/common-workflows
  2. code.claude.com/docs/en/best-practices
  3. developer.apple.com/documentation/swiftui/view-accessibility
  4. developer.apple.com/documentation/swiftui/view/accessibilitylabel(_:)
  5. developer.apple.com/documentation/swiftui/image
  6. developer.apple.com/documentation/swiftui/dynamictypesize
  7. developer.apple.com/documentation/accessibility/performing-accessibility-audit…
この記事を共有:XはてブLinkedIn