SwiftUIには accessibility から始まるモディファイアが数十種類存在しますが、実はすべてが「ユーザーのため」の機能というわけではありません。本記事では、代表的な3つのAPIを取り上げながら、それぞれの役割の違いを整理します。
そもそもアクセシビリティAPIには2つの目的がある
大きく分けると、SwiftUIのアクセシビリティ関連APIは以下の2つの目的に分類できます。
| 目的 | 対象 | 例 |
|---|---|---|
| 本当のアクセシビリティ | VoiceOverなどを使うユーザー |
accessibilityLabel, accessibilityElement, accessibilityAddTraits
|
| テスト用の識別子 | 開発者(UIテストコード) | accessibilityIdentifier |
同じ「accessibility」という名前空間に入っている理由は、XCTestのUIテスト(XCUITest)が内部的にアクセシビリティAPI経由でUI要素を検出しているためです。VoiceOverが画面の要素を調べる仕組みと、テストコードが要素を調べる仕組みは、裏側で同じシステムを共有しています。
VoiceOver ─┐
├─→ Accessibility API ─→ UI要素の情報
XCUITest ──┘
この前提を踏まえて、それぞれのAPIを見ていきます。
1. accessibilityElement(children:) — 子要素の扱いを決める
複数の子ビューを持つ場合に、それらを支援技術(VoiceOverなど)に対してどう見せるかを制御するモディファイアです。引数には AccessibilityChildBehavior という値を渡します。
.ignore — 子要素を無視する
子ビューのアクセシビリティ情報をすべて無視し、新しく空の1つの要素を作ります。ラベルや値は自分で付け直す必要があります。
var body: some View {
VStack {
Button("Previous Page", action: goBack)
Text("\(pageNumber)")
Button("Next Page", action: goForward)
}
.accessibilityElement(children: .ignore)
.accessibilityValue("Page \(pageNumber) of \(pages.count)")
.accessibilityAdjustableAction { action in
if action == .increment {
goForward()
} else {
goBack()
}
}
}
複数の部品をまとめて1つの操作可能なUIとして扱いたい場合に向いています。
.contain — 子要素をグループ化する
子ビューのアクセシビリティ要素をそのまま保持しつつ、VoiceOverの「コンテナ(グループ)」として扱います。
var body: some View {
ScrollView {
VStack {
HStack {
ForEach(users) { user in
UserCell(user)
}
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Users")
VStack {
ForEach(messages) { message in
MessageCell(message)
}
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Messages")
}
}
}
個々の要素は読み上げ可能なまま、グループ単位でのナビゲーションが可能になります。
.combine — 子要素の情報を結合する
子ビューの情報を1つにまとめて、新しい1つの要素として合成します。.ignore と違い、既存の情報を活かして結合してくれるため、ラベルを書き直す手間が少なくなります。
struct UserCell: View {
var user: User
var body: some View {
VStack {
Image(user.image)
Text(user.name)
Button("Options", action: showOptions)
}
.accessibilityElement(children: .combine)
}
}
カード型のUIなど、複数の情報をまとめて1つの読み上げ内容にしたい場合に適しています。
3つの違いのまとめ
| behavior | 子の情報 | 結果 | 使う場面 |
|---|---|---|---|
.ignore |
捨てる | 空の要素→自分で属性を付ける | 独自の複合コントロールを作る時 |
.contain |
保持する | 子要素をグループ化 | セクション分けしたリストなど |
.combine |
自動で合成 | 1つの要素に自然にまとまる | カード型UIなど、まとめて読ませたい時 |
なお .contain と .combine には、ビューが持つアクセシビリティ要素が複数または0個の場合などに「新しい要素が作られる」、それ以外の場合は「既存要素の設定が変わるだけ」という細かい挙動の違いがあります。実際の動作はVoiceOverをオンにして確認するのが確実です。
2. accessibilityAddTraits(_:) — 要素の種類を伝える
UI要素が「何の種類のものか」をVoiceOverなどに伝えるためのモディファイアです。渡す値の型 AccessibilityTraits は SetAlgebra に準拠しており、複数のトレイトを集合として組み合わせられます。
Text("送信")
.accessibilityAddTraits(.isButton)
複数のトレイトを同時に付与することもできます。
Image(systemName: "play.circle")
.accessibilityLabel("再生")
.accessibilityAddTraits([.isButton, .playsSound])
主なトレイト
| トレイト | 意味 |
|---|---|
.isButton |
ボタンである |
.isHeader |
見出し(ナビゲーションバーのタイトルなど) |
.isSelected |
現在選択されている |
.isLink |
リンクである |
.isSearchField |
検索フィールドである |
.isImage |
画像である |
.isStaticText |
変更されない静的テキスト |
.playsSound |
操作時に音を鳴らす |
.isKeyboardKey |
キーボードのキーのように振る舞う |
.updatesFrequently |
値が頻繁に更新される(ストップウォッチなど) |
.startsMediaSession |
メディア再生を開始する(VoiceOverの音声を一時中断) |
.allowsDirectInteraction |
VoiceOverでの直接タッチ操作を許可 |
.causesPageTurn |
読み終わると自動でページ送りする |
.isModal |
モーダルである |
.isSummaryElement |
起動時に要約情報を提供する |
.isToggle(iOS 17+) |
トグルスイッチである |
.isTabBar(iOS 17+) |
タブバーである |
SetAlgebra に準拠しているメリット
Set<Int> などの標準の集合型と同じ操作感覚で扱えます。
var traits: AccessibilityTraits = [.isButton, .isSelected]
// 追加
let combined = traits.union([.playsSound])
// 含まれているか確認
traits.contains(.isButton) // true
// 削除
traits.remove(.isSelected)
これはSwiftにおける典型的な設計パターンで、複数のフラグを型安全に組み合わせたいときに SetAlgebra や OptionSet を使う、という発想に基づいています。
3. accessibilityIdentifier(_:) — テスト用の識別子
ここまでの2つとは目的が異なり、ユーザーには見えない・聞こえない、開発者のためのAPIです。
extension View {
nonisolated public func accessibilityIdentifier(_ identifier: String) -> ModifiedContent<Self, AccessibilityAttachmentModifier>
}
公式コメントにも明記されています。
Use this value for testing. It isn't visible to the user.
使用例
Button("送信", action: submit)
.accessibilityIdentifier("submitButton")
XCTestのUIテストから、この識別子を使って要素を特定できます。
func testSubmitButton() {
let app = XCUIApplication()
app.launch()
let button = app.buttons["submitButton"]
XCTAssertTrue(button.exists)
button.tap()
}
なぜ必要なのか
表示テキストだけで要素を識別しようとすると、以下のような問題が起きます。
- ローカライズで文言が変わるとテストが壊れる
- 同じラベルの要素が複数あると区別できない
accessibilityIdentifier は表示内容と独立した固定の識別子を割り振ることで、この問題を避けます。
型注釈から読み取れるSwiftUIの設計思想
戻り値の型 ModifiedContent<Self, AccessibilityAttachmentModifier> は、「元のビューに変更を加えた結果」を表す型です。SwiftUIのモディファイアは元のビューを直接書き換えるのではなく、常に「元のビュー+変更内容」を表す新しい構造体を返します。これは値型・不変性を重視するSwiftUIの基本的な設計思想の一例です。
また nonisolated は、Swiftの並行処理(Concurrency)に関するキーワードです。View の多くのメソッドは @MainActor に紐づけられていますが、単純な文字列の紐付けであるこのメソッドはメインスレッド限定にする必要がないため、nonisolated によって任意のActorから呼べるようにされています。
まとめ:目的による分類
| モディファイア | 目的 | ユーザーに伝わる? |
|---|---|---|
accessibilityElement(children:) |
子要素の構造をどう見せるか制御する | 伝わる(構造として) |
accessibilityAddTraits |
要素の種類・特性を伝える | 伝わる(トレイトとして) |
accessibilityIdentifier |
UIテストでの識別用 | 伝わらない |
同じ accessibility から始まる名前でも、実際には「VoiceOverユーザーへの情報提供」と「開発者のためのテスト補助」という異なる目的が混在しています。ドキュメントのコメントに "the user" や "VoiceOver" といった語があればユーザー向け、"testing" のような語があれば開発者向け、というのが見分ける際の目安になります。