ハイライト
Mac アプリのアクセシビリティが iPhone / iPad と決定的に違うのは、Mac の UI は情報密度が高く、ネストされたコンテナ階層が深い点です。Mac の VoiceOver はデフォルトでコンテナ単位のナビゲーションを行うため、accessibility 要素ツリーの構造が適切でないと、ユーザーは一歩進むたびにコンテナへの「ドリルイン」と「ドリルアウト」を繰り返すことになり、操作効率が極端に落ちてしまいます。
主要内容
こんな場面を想像してみてください。ユーザーがあなたの Mac テキストエディタを開き、右側の Format Inspector で「Bold」スイッチをひとつ切り替えたいとします。視覚に問題のないユーザーであれば、画面をひと目見てマウスを少し動かすだけで完了します。ところが VoiceOver ユーザーは、矢印キーで Title、Apply、Subtitle、Apply、Heading、Apply…と 22 個の accessibility 要素を順に通り抜けて、ようやく「Bold」にたどり着きます。Session 内で Nicholas はこの実際の音声を録音して再生していますが、聞いているだけでぞっとするほどです。
Mac の本質的な特徴は、キーボード・マウス操作中心であり、情報密度が高く、マルチタスクのウィンドウが多いことです。SwiftUI はデフォルトですべての view を独立した accessibility element として扱うため、Mac の VoiceOver がデフォルトでコンテナ単位にジャンプする挙動と相まって、アプリは 2 つの極端な状態に陥りがちです。要素がフラットすぎて 1 ステップごとに大量の要素を踏破しなければならないか、ネストが深すぎてコンテナ間で「focus in / focus out」を繰り返すか、のいずれかです。これに対して今年の Apple は 3 つの武器を用意しました。accessibilityElement(children:) でレイアウト構造を再構築する、rotor と accessibilityDefaultFocus でナビゲーションを高速化する、accessibilityAction で hover-only 操作にキーボード相当の経路を補う、というものです。Session 全体は 1 本のテキストエディタの demo を題材に、これら 3 つを順に磨き上げていく構成になっています。
詳細
ステップ 1: accessibilityElement(children:) でレイアウトを再構築する(04:15)。この modifier には 3 種類の挙動があります。contain は子 view をひとつのコンテナにまとめ、combine は子 view をひとつの要素に統合し、ignore は子 view を完全に隠します。Demo では 22 個の style preset 要素を、“Style Presets” という名前の group にまとめています。
// Contain style presets in accessibility container
struct FormattingInspectorView: View {
var body: some View {
Form {
VStack {
StylePresetView(type: .title)
StylePresetView(type: .heading)
StylePresetView(type: .subHeading)
StylePresetView(type: .body)
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Style Presets")
}
}
}
ポイント:
.accessibilityElement(children: .contain): VStack をコンテナ化します。子 view は引き続き独立した要素ですが、VoiceOver はデフォルトでは外側のコンテナをひとっ飛びにスキップできます。.accessibilityLabel("Style Presets"): コンテナに分かりやすい名前を与えます。ユーザーは「Style Presets, Group」と聞いた瞬間に、その中に何が入っているのか把握できます。- 修正後は要素総数が 22 から 15 に減り、「Bold」スイッチまで矢印キー数回でたどり着けるようになります。
ステップ 2: combine でペアになった要素を統合する(06:21)。各 style preset は HStack { Title, Button } という構造で、VoiceOver は「Title」と「Apply - button」を別々に 2 回読み上げてしまいます。.combine でこれらをひとつの要素に統合します。
struct StylePresetView: View {
let preset: StylePreset
var body: some View {
HStack {
PresetTitleView(preset: preset)
Button("Apply") { /* ... */ }
}
.accessibilityElement(children: .combine)
}
}
ポイント:
.combineは子 view の label、traits、actions をすべてひとつの element に統合します。- VoiceOver は一度に「Title, Apply, button」と読み上げ、操作の意味が明確になります。
- 要素数はさらに半減し、8 個から 4 個になります。
ステップ 3: rotor で「ブックマーク探し」をワンキージャンプにする(08:43)。視覚に問題のないユーザーは、サイドバーをひと目見ればどのページにブックマークが付いているか分かりますが、VoiceOver ユーザーは 1 ページずつ順に聞いていく必要があります。Rotor はキーボードショートカットでメニューを開き、条件に一致する要素だけを列挙する仕組みです。
struct PagesView: View {
@Binding var pages: [Page]
var body: some View {
List(pages) { page in
PageListItemView(page: page)
}
.accessibilityRotor("Bookmarks") {
ForEach(pages) { page in
if page.isBookmarked {
AccessibilityRotorEntry(page.title, id: page.id)
}
}
}
}
}
ポイント:
.accessibilityRotor("Bookmarks"): “Bookmarks” という名前の rotor を登録します。AccessibilityRotorEntry(page.title, id: page.id): 各エントリがジャンプ先に対応します。idは List 内で対応する行を特定できる値である必要があります。- VoiceOver で List にフォーカスした状態で rotor のショートカットを押すとメニューが開き、「Page 5 bookmarked」へ直接ジャンプできます。
ステップ 4: accessibilityDefaultFocus で初期フォーカスを提案する(09:33)。macOS / iOS 26 で追加された modifier で、新しいシーンが現れたときに、VoiceOver にどの view へフォーカスを当てるかを提案できます。
struct MyView: View {
@AccessibilityFocusState(for: .voiceOver) var focusedForVoiceOver
var body: some View {
FirstView()
SecondView()
.accessibilityDefaultFocus($focusedForVoiceOver, true)
ThirdView()
}
}
ポイント:
@AccessibilityFocusState(for: .voiceOver): VoiceOver のフォーカス状態だけを対象とし、キーボードフォーカスには影響しません。.accessibilityDefaultFocus($focusedForVoiceOver, true): シーンが現れたときに SecondView がフォーカスを得るよう提案します。- あくまで「提案」であり、VoiceOver はユーザー設定に応じて別の選択をする可能性があるため、アプリ側で確定的に振る舞いを仮定しないようにします。
ステップ 5: hover-only 操作に accessibilityAction を補う(10:28)。Demo のブックマークボタンは、ページのサムネイルにマウスをホバーしたときだけ表示されます。VoiceOver ユーザーにはホバーという操作がないため、このボタンには永遠にたどり着けません。
struct PageListItemView: View {
var page: Page
var body: some View {
VStack() {
ThumbnailView(page: page)
Text(page.title)
}
.onHover { /* ... */ }
.accessibilityAction(named: page.isBookmarked ? "Remove Bookmark" : "Bookmark") {
page.isBookmarked.toggle()
}
}
}
ポイント:
.accessibilityAction(named:): 名前付きのアクションを宣言します。VoiceOver / Voice Control / Switch Control のいずれからも呼び出せます。- 名前は現在の状態に応じて動的に変化(“Bookmark” / “Remove Bookmark”)するため、ユーザーは次に何が起きるかを正確に把握できます。
- このアクションはマウスホバーに依存しないため、キーボードや支援技術から直接アクセスできます。
重要ポイント
-
やること: プロジェクトの早い段階で、VoiceOver をリグレッションテストとして回す。
- なぜやる価値があるのか: Session の中で Nicholas は「最初から検証する」ことを繰り返し強調しています。問題は早く見つけるほど修正コストが小さくなります。ビジュアルレイアウトの段階で VoiceOver の読み上げを一度通すだけで、要素が多すぎる、順序がおかしい、コンテナが深くネストしすぎている、といった構造的な問題を即座に発見できます。
- 始め方: macOS では
Cmd + F5で VoiceOver を起動し、Ctrl + Option + 矢印キーで新しく書いた view を一通り操作します。「1 ページ全体を聞き終えるのに何回キーを押すか」をチームで観測可能な指標として共有しましょう。
-
やること:
accessibilityElement(children:)で密度の高い UI を階層化する。- なぜやる価値があるのか: Mac アプリの inspector、sidebar、toolbar には多数の並列コントロールが並ぶため、フラットな構造のままでは VoiceOver ユーザーは数十回のキー入力で踏破することになります。
containとaccessibilityLabelを数行追加するだけで、要素総数を半減させられます。 - 始め方: まず最外層の group に意味のある
.accessibilityLabelを伴った.containを付け、次に内側で意味的にペアになっている子 view(Title + Button、Icon + Label など)を.combineで統合します。ネスト階層は 3 階層以内に抑えるとよいでしょう。
- なぜやる価値があるのか: Mac アプリの inspector、sidebar、toolbar には多数の並列コントロールが並ぶため、フラットな構造のままでは VoiceOver ユーザーは数十回のキー入力で踏破することになります。
-
やること: 重要なコレクションには rotor を用意する。
- なぜやる価値があるのか: ブックマーク、エラー項目、ToDo、未読メッセージといった「長いリストの中に散らばる少数のサブセット」は、VoiceOver ユーザーが最も迷子になりやすい場所です。rotor は O(N) の走査を O(1) のメニュー選択に変えてくれます。
- 始め方: アプリの中で「ユーザーが直接ジャンプしたいことが多い」要素タイプを洗い出し、対応する List / ScrollView に
.accessibilityRotor("名前") { ForEach { AccessibilityRotorEntry(...) } }を追加し、VoiceOver でショートカットからメニューを開いて、正しい行へジャンプできるかを確認します。
-
やること: hover-only 操作とジェスチャー操作をすべて棚卸しし、
accessibilityActionを補う。- なぜやる価値があるのか: Mac には「マウスをホバーして初めて現れるボタン」が多数存在しますが、これらは VoiceOver、Switch Control、Voice Control ユーザーにとっては不可視のコントロールです。
.accessibilityAction(named:)をひとつ加えるだけで、すべての支援技術から到達可能になります。 - 始め方: プロジェクト内の
.onHoverと.gestureの呼び出し箇所を grep し、それぞれが状態変更やナビゲーションをトリガーしているかを確認します。該当する場合は同じ view に.accessibilityAction(named:)を追加し、名前は動詞句で書きましょう。
- なぜやる価値があるのか: Mac には「マウスをホバーして初めて現れるボタン」が多数存在しますが、これらは VoiceOver、Switch Control、Voice Control ユーザーにとっては不可視のコントロールです。
関連セッション
- Customize your app for Assistive Access — アプリを Assistive Access モード向けに削ぎ落とし、認知障害のあるユーザーでも扱えるようにする
- Evaluate your app for Accessibility Nutrition Labels — App Store の新しいアクセシビリティ栄養ラベル。リリース前にセルフチェックを
- Principles of inclusive app design — 障害への理解から始め、アプリ設計の原則を組み立て直す
- Catch up on accessibility in SwiftUI — 本セッションの前提となる回。accessibility element の基本モデルを整理
コメント
GitHub Issues · utterances