-
読書アプリのアクセシビリティの向上
VoiceOverや画面の読み上げなどを活用して、充実した読書体験を生み出す方法を学びましょう。直感的なテキスト選択、行間や段落間の分かりやすいナビゲーション、個々の要素や複数ページにわたるスムーズな読書体験を提供する方法を紹介します。
関連する章
- 0:01 - Introduction
- 1:26 - Characteristics
- 3:45 - Standard views
- 14:05 - Custom text
リソース
- accessibilityNextTextNavigationElement
- editCategory
- accessibilityLinkedGroup(id:in:)
- causesPageTurn
- UITextInput
- Accessibility for UIKit
関連ビデオ
WWDC19
-
このビデオを検索
こんにちは!
私はJoshといいます。 ソフトウェアエンジニアとして アクセシビリティチームに所属しています。 今日は、長文テキストや 読書アプリを Appleプラットフォーム上の すべての人にアクセシブルにする方法をお話しします。
長文コンテンツを読むことは、 UIのナビゲーションとは根本的に異なります。 テキストをスムーズに移動することが重要で、 コントロールのような UI要素間を移動するだけではありません。
Appleのフレームワークには、 アクセシブルなテキストが 念頭に置かれて組み込まれています。 しかし、開発者として さらに多くのことができます。 長文テキストでの アクセシビリティ体験を充実させ、拡張するために。
今日は、いくつかの ベストプラクティスとテクニックを紹介します。 長文コンテンツを構築する際に ぜひ参考にしてください。
まず、VoiceOverや 他の支援技術を使用している人にとって 優れた読書体験を構成する 特性についてお話しします。 次に、UIKit と SwiftUI の ビューを使用・拡張する方法を示します。 読書体験のために特別に設計された 豊富な API を使って。
最後に、アプリのカスタムテキストを アクセシブルにする方法をお伝えします。 VoiceOver、Speak Screen、 またはアクセシビリティリーダーに対応するために。
まずは、長文コンテンツを表示するアプリで 優れたアクセシブル体験を 作る方法について説明します。 今日は、お勧め情報と旅行のヒントを 共有できるアプリを作りたいと思います。 私のお気に入りの都市のひとつ、 シカゴについてです。
私のアプリにはページ化されたコンテンツがあり、 複数の段落とテキストがあります。 テキストは複数行にわたって 折り返されます。 支援技術を使用している 誰もが このアプリで素晴らしい体験を できることを確認したいと思います。 このセッションでは、 2つの人気のある Appleプラットフォームに組み込まれた 支援技術に焦点を当てます。 VoiceOver と Speak Screen です。 VoiceOver は Appleの 内蔵スクリーンリーダーで、 視覚障害者や 弱視の方向けに設計されています。 有効にすると、カーソルで ハイライトされているものが聞こえます。 朝。 見出し。 リンカーン公園で 朝のスタートを切り、 トレイルを散策しながら シカゴのスカイラインを楽しみました。
Speak Screen は、 ページ上のすべてのコンテンツを 上から下へ読み上げ、 読み上げ中にハイライトします。 オンにすると、2本の指で 下にドラッグすることで 画面上部から開始できます。 昼。 昼食時に、 シカゴ川沿いを歩きました。 川沿いの道から 街の壮大な建築を見渡せました。 お気に入りのビューは DuSable橋の中央からで、 川を真っすぐ下に 見渡すことができました。
これらの技術を念頭に置いて、 インタラクションを改善するための 3つの目標を設定しました。 それらの機能と私のアプリの間のです。 具体的には、アプリが 細かいテキストナビゲーションを提供することを 確認したいと思います。 VoiceOver と Speak Screen が テキストをスムーズに移動できるように。 また、継続的な読書体験を 開発していることも確認したいと思います。 支援技術を使用している人が 中断に遭遇しないように。 最後に、アプリが 包括的なテキスト選択を提供することを 確認したいと思います。
このビデオの残りの時間で これらの主要な目標に集中します。 そして、旅行アプリが それらすべてを満たすことを確認します。
Appleのフレームワークは 多くのテキストコンポーネントを提供しており、 そのままアクセシブルに 使えるものがあります。 それらが何であるか、何を提供するかに 焦点を当てます。 そして、追加機能で 拡張する方法も。 UIKit と SwiftUI はどちらも アクセシブルなテキストビューを提供し、行、単語、 文字のナビゲーションを VoiceOver と Speak Screen で可能にします。 アクセシブルなテキスト選択も 含まれています。 すでに UIAccessibilityReadingContent を ご存知かもしれません。 これは、フルページコンテンツを アクセシブルにする優れた方法です。 このプロトコルには 焦点を当てませんが、 今日話すことの上に 引き続き使用・採用できます。 詳細は「アクセシブルな読書体験の作成」を ご覧ください。 今日は UITextInput に焦点を当てます。 ネイティブテキストビューが使用する 高忠実度のプロトコルで、 カスタムビューでも 採用できます。
システム全体の標準テキストビューは UITextInput プロトコルを採用しています。 UIKit を使って iOS の UITextView を使用すると 豊かなテキスト体験が得られます。 最初から、 SwiftUI の TextEditor も同様です。 選択を有効にしたシンプルな SwiftUI Text ビューも使えます。 すべての Apple プラットフォームで これらの機能を享受できます。 macOS アプリを構築している方は、 AppKit の NSTextView を使用するか、 説明した SwiftUI ビューを使用することで 同じ利点が得られます。 アプリの制約が 許す場合は、 常にこれらのコンポーネントを 使用するようにしてください。
私のトラベルガイドアプリでは、 UITextView を選択しました。 アクセシブルなプロパティのために 各段落で使用しています。 私が設計したユニークなレイアウトでは 各段落に個別のテキストビューを 使用する必要がありました。 複数の段落を含む 1つのビューではなく。 まず、細かいテキストナビゲーションを提供するという 目標に対して どのくらい達成できているか 確認します。
VoiceOver には、 画面に指を触れたときに 読み上げるテキストの粒度を 選択できる設定があります。 私は行に設定しているので、 VoiceOver をオンにすると、 画面上の任意の行を タップして その行が読み上げられるのを 聞くことができます。 リンカーン公園で 朝のスタートを切り、 トレイルを散策しながら 眺めを楽しみました... VoiceOver は移動方法を 変更するオプションも提供しています。 ローターと呼ばれる機能を通じて。 アクティブなローターを変更できます。 2本指の回転ジェスチャーを使って モードを切り替えます。 では、そのジェスチャーを使って 行ローターに切り替えます。 1本指で下にスワイプして ページの次の行を見つけます。
行。
...シカゴのスカイラインの眺め。
次は、この段落の末尾から 移動してみます。 次の段落の最初の行へ。
現在、これらの各段落が 別々のビューであるため、 VoiceOver は段落内での 行ナビゲーションにとどまっています。 そのため、行によってページを 完全に探索することができず、 その音が鳴る理由です。
VoiceOver が段落間を シームレスに移動できるように、 iOS 18 ではテキストナビゲーション API が 導入されました。 接続したいテキスト要素ごとに、 VoiceOver がナビゲートすべき 次と前の アクセシブルなテキスト要素を 返します。
たとえば、2つの段落ビューがある場合、 段落1の accessibilityNextTextNavigationElement メソッドから 段落2を返すことができます。 また、段落2の accessibilityPreviousTextNavigationElement から 段落1を返します。
ここに、トラベルガイドアプリの ページコントローラーがあります。 セットアップ中に configureNavigationElements コードパスが実行されると、 該当する各方向に 適切なナビゲーション要素を設定します。
実装が完了しました。 VoiceOver はある段落の末尾を超えて 次の段落の最初の行に 移動できるようになりました。 公園を出る前に、無料の動物園に立ち寄り、 すべての動物を見てきました...
SwiftUI を使用している場合は、 iOS 27 から、 複数のテキスト要素をリンクするには accessibilityLinkedGroup モディファイアを使用すると 同じ効果が得られます。 たとえば、ここには 同等のページビューがあります。 2つの選択可能なテキスト要素があります。 両方を accessibilityLinkedGroup で リンクすることで、 同じ id と名前空間を使用して、 テキストナビゲーションの 動作が得られます。 Mac で AppKit を使用している場合は、 accessibilitySharedTextUIElements を 確認してください。 同様の結果が得られます。 VoiceOver がアプリのページを ナビゲートできるようになりました。 異なるテキスト粒度で、 予期しないギャップなしに。 しかし、アプリの 継続的な読書体験を できるだけスムーズにすることも 目標に設定しました。
ページ化されたコンテンツは、 その性質上、ページ間のスワイプが必要です。 支援技術がこのコンテンツと シームレスにインタラクションできることが目標で、 ページが邪魔にならないように。 VoiceOver と Speak Screen には 機能があります。 すべてのコンテンツを読むことができ、 最初から最後まで、 スワイプなしで。
現在のアプリ体験を Speak Screen で探ります。 全部読むために、2本の指で 画面上部から下にスワイプします。 昼。 昼食時に、 シカゴ川沿いを歩きました。 川沿いの道から 街の壮大な建築を見渡せました。 お気に入りのビューは DuSable橋の中央からで、 川を真っすぐ下に 見渡すことができました。 Speak Screen がページ下部に達したところで 読み上げを停止したことに 気づくでしょう。 ページ化されたコンテンツでは、 全部読むための最良の体験は すべてのページを通じて移動し、 適切なタイミングで進むことです。 オーディオブックのように。
ここにアプリのページビュー コントローラーがあります。 viewDidLoad オーバーライドで、 causesPageTurn トレイトをページの 最後の段落に適用できます。 UIKit と SwiftUI の両方で 利用可能です。 accessibilityScroll と組み合わせると、 Speak Screen と VoiceOver が ページを自動的にスクロールします。 末尾に達したときに。
そのトレイトを最後の段落に適用して、 Speak Screen を試してみます。
昼。 昼食時に、 シカゴ川沿いを歩きました。 川沿いの道から 街の壮大な建築を見渡せました。 お気に入りのビューは DuSable橋の中央からで、 川を真っすぐ下に 見渡すことができました。 夕方。 その日の締めくくりとして、 湖岸沿いを歩きました。 ランナーやサイクリストの グループと共に。 スカイラインの素晴らしい 眺めを楽しみました。 ミシガン湖の水面に そびえ立つスカイライン。
素晴らしい! Speak Screen が読み終わると 自動的に次のページにフォーカスが移動しました。 期待通りです。
先ほど述べたように、 最後に検証したい動作は VoiceOver でのテキスト選択の仕組みです。 アプリに、選択したコンテンツを後で参照するために 保存する機能を追加しました。 ツールバーのボタンを使って。 この機能が アクセシブルであることを確認する必要があります。
ここでは UITextView を使用しており、 すでにアクセシブルな選択が可能です。 TextEditor を使用しても 同じ体験が得られます。 または SwiftUI で選択を有効にしたテキストを 使用しても。 しかし、人々が この「おすすめを保存」機能を 発見できるようにしたいと思います。 選択したテキストに対して。 視覚的には、 ツールバーにこのボタンを追加して 現在の選択を保存できるようにしました。 しかし、編集ローターを通じて VoiceOver でさらに発見しやすくできます。 これを行うには、カスタムアクションを作成して VoiceOver の編集ローターに追加します。 アクションを構築するときに 編集カテゴリを指定することで。 私の場合は、 accessibilityCustomActions を 段落 UITextView サブクラスで オーバーライドします。 そして、「おすすめを保存」 カスタムアクションを追加します。 スーパー実装からの アクションと一緒に。 テキスト選択に関連した カスタムアクションがある場合は、 編集カテゴリを使用してください。 汎用アクションではなく。
では、VoiceOver をオンにして 試してみます。 テキストを選択するために、 テキスト選択ローターに切り替え、 単語編集モードに切り替えます。 右にスワイプして 選択範囲を広げます。
テキスト選択。 右にスワイプして選択を拡張。 左にスワイプして選択を縮小。 単語選択。 「お気に入りのビューは DuSable橋から...」が選択されました。
テキストが選択された状態で、 編集ローターに切り替えます。 「選択を保存」アクションを アクティブにして保存します。
行。 単語。 文字。 編集。
選択を保存。 テキストが保存されました。 素晴らしい! これで、システムテキストビューを使用した アクセシブルなアプリ体験が完成し、 API を採用することで新しいアクセシブルな 読書機能が解放されました。 要素間の行ナビゲーション、継続的な読書、 テキスト選択が 期待通りに機能しています。 そして最も良い点は、 VoiceOver と Speak Screen だけがこれらの変更の 恩恵を受けるわけではないことです。 iOS 26 以降、 アクセシビリティリーダーを開くことができます。 テキストコンテンツをより簡単に 消費するために表示するツールです。 リーダーコントロールを コントロールセンターに追加しました。 そのボタンを押すと、アプリのコンテンツが アクセシビリティリーダーで開きます。 ここで共有したようなアクセシブルな テキストプラクティスを実装することで コンテンツのリーダー体験も より良くなります。
これが、読書コンテンツのために 標準テキストビューをアクセシブルにする方法です。 常にそれらのビューを 最初に選ぶことをお勧めしますが、 すべての状況で 可能とは限りません。
カスタムテキストを使用する場合に 何をすべきかに焦点を当てます。 またはカスタムテキスト要素を アクセシブルにするために。
カスタムテキストの使用は、 専用の読書アプリでよく見られるパターンです。 高度なタイポグラフィをサポートするために、 開発者のアプリケーション間で コードを共有するために、 またはスキャンされたページを表示するために。 旅行中は、訪れる場所の 手書きのメモを取るのが好きです。 そのため、テキストビューをノートブックの スキャンページに置き換えることにしました。 より個人的な雰囲気を出すために。 残念ながら、これにより UITextView が提供していた アクセシビリティ動作が失われました。 UITextView が無料で提供していた動作が。 最も基本的なこと、 テキストの読み上げを含めて。 朝。 見出し。 画像。
このコンテンツをアクセシブルにする 最良の方法は UITextInput プロトコルを 使用することです。 任意のアクセシビリティ要素に 採用できます。 このプロトコルは、レンダリングされたテキストや 画像内のテキストを、 標準テキストビューに入っているかのように アクセシブルにできます。 UITextInput を完全に実装すると、 ネイティブテキストビューを使用した場合と 同じテキスト体験が得られます。 VoiceOver による 行ごとのタッチ探索、 VoiceOver ローターと Speak Screen による 細かいナビゲーション、 そしてテキスト選択。
このプロトコルを実装するには、 いくつかの問題を解決する必要があります。 テキストのジオメトリを 管理する必要があります。 指定された範囲の 選択矩形を計算するために、 たとえば selectionRects メソッドで。
支援技術がビュー内の範囲を クエリするとき、 そのテキストの部分だけを 返せる必要があります。 そして重要なのが、 トークナイザーを提供する必要があることです。 行、文、単語、文字ごとの ナビゲーションを管理するために。
これらはほんの一部です。 実装が必要なものは。 このプロトコルのすべての アクセシビリティの利点を得るには、 完全に実装するようにしてください。
アプリでは、このプロトコルを アクセシビリティ要素に実装しました。 テキストをアクセシブルにするために。 ここでは、このプロトコルの selectionRects メソッドを実装しました。 VoiceOver がどのように コンテンツを「ハイライト」するかを 決定します。 画像から手書きを扱っているので、 各行の既知の高さと幅を使用して 近似矩形を計算できます。 カスタム関数 selectionRectFromImage を使って 特定の範囲に対して。 この情報を使って 選択矩形の配列を構築します。 このメソッドから返します。
他のメソッドの実装も 完了します。 プロトコル内の残りのメソッドの。 textInRange の適切な サブストリングを取得するなど トークナイザーを提供するために。 私の場合は、 UITextInputStringTokenizer をサブクラス化しました。 UIKit が提供するものを カスタムトークナイザーを作成するために。 自分の実装で動作するように、 それを返します。
最後に、選択体験を 選択ハンドルとハイライトで 視覚的に完全にしたいと思います。 これを行うために、ページビューに UITextInteraction を追加しました。 選択が変わったときに 入力デリゲートを呼び出します。 システムがビジュアルを 更新するように。 これは UITextInput 実装の 必須部分ではありませんが、 標準テキストビューでの 期待に応えることで アプリを完成させます。
UITextInput は causesPageTurn と 組み合わせても非常に有効です。 ナビゲーション要素 API とも。 アプリの新しいバージョンでも それらを実装しました。
アプリの更新が完了しました。 UITextInput プロトコルの残りの部分を 丁寧に実装し、 スキャンされたテキストに対して 必要なすべての API を 実装したことを確認しました。 VoiceOver 体験を 確認してみましょう。
まず、行ごとにナビゲートします。 行。 リンカーン公園で 朝のスタートを切り、 眺めを楽しみながら シカゴのスカイラインを。 動物園にはたくさんの動物がいました。
テキスト選択ローターに切り替えて テキストを選択します。 右にスワイプします。 テキスト選択。 右にスワイプして選択を拡張。 左にスワイプして選択を縮小。 行選択。 「動物園にはたくさんの動物がいました」が選択されました。 行。 単語。 文字。 編集。 選択を保存。 テキストが保存されました。
最後に、全部読むを試せます。 朝。見出し。 リンカーン公園で 朝のスタートを切り、 シカゴのスカイラインの眺めを楽しみました。 動物園にはたくさんの動物がいました。 昼。見出し。 昼食時に、 シカゴ川沿いを歩きました。 DuSable橋からの眺めは 写真に最高でした! 素晴らしい! すべてシームレスに機能しています。 優れた読書体験を作るものと、 それを可能にする API について説明したので、 あなた自身のアプリを調べる時間です。
VoiceOver をオンにして アプリを監査し、全部読むジェスチャーを試してください。 行ローターを使ってナビゲートし、 テキストを選択します。 標準テキストビューを使用している場合は、 causesPageTurn の採用を検討してください。 そして、スムーズなクロスページ読書動作のために テキストナビゲーション要素 API を。 カスタムレンダリングテキストを使用している場合は、 UITextInput を採用してください。 この作業を行うことで 素晴らしい体験が実現します。 アプリをダウンロードするすべての人にとって。 楽しい読書を!
-
-
7:29 - Link text elements together with navigation APIs
// Link text elements together with navigation APIs import UIKit class TravelGuidePageController: UIViewController { var paragraphs: [TravelGuideParagraph] func configureNavigationElements() { for (index, paragraph) in paragraphs.enumerated() { if index + 1 < paragraphs.count { paragraph.accessibilityNextTextNavigationElement = paragraphs[index + 1] } if index - 1 >= 0 { paragraph.accessibilityPreviousTextNavigationElement = paragraphs[index - 1] } } } } -
7:59 - Link text elements together with a linked group
// Link text elements together with a linked group import SwiftUI struct PageView : View { @Namespace private var pageNamespace var paragraphs: [String var pageNumber: Int var body: some View { Text(paragraphs[0]) .textSelection(.enabled) .accessibilityLinkedGroup(id: pageNumber, in: pageNamespace) Text(paragraphs[1]) .textSelection(.enabled) .accessibilityLinkedGroup(id: pageNumber, in: pageNamespace) } } -
9:50 - Turn pages automatically after reading
// Turn pages automatically after reading import UIKit class TravelGuidePageController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.lastParagraphView.accessibilityTraits.insert(.causesPageTurn) } override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool { moveToPage(direction) var scrollString = "Page \(currentPage) of \(pages.count)" UIAccessibility.post(notification: .pageScrolled, argument: scrollString) return true } } -
11:45 - Add actions to the editor rotor
// Add actions to the editor rotor import UIKit class TravelGuideParagraph: UITextView { override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { get { let saveAction = UIAccessibilityCustomAction(name: "Save Recommendation") { _ in self.saveRecommendation() } saveAction.category = UIAccessibilityCustomAction.editCategory return (super.accessibilityCustomActions ?? []) + [saveAction] } set { } } private func saveRecommendation() -> Bool { ... return true } } -
16:10 - Adopt UITextInput
// Adopt UITextInput import UIKit class ScannedPage: UIView, UITextInput { override init(frame: CGRect) { super.init(frame: frame) let interaction = UITextInteraction(for: .nonEditable) interaction.textInput = self addInteraction(interaction) } func selectionRects(for range: UITextRange) -> [UITextSelectionRect] { var rects: [UITextSelectionRect] = [] let startLine = lineIndex(for: range.start) let endLine = lineIndex(for: range.end) for line in startLine...endLine { let rect = selectionRectFromImage(for: range, in: line) rects.append(rect) } return rects } func text(in range: UITextRange) -> String? { let nsRange = nsRange(from: range) guard let range = Range(nsRange, in: scannedText) else { return nil } return String(scannedText[range]) } var tokenizer: any UITextInputTokenizer { CustomHandwritingTokenizer(textInput: self) } weak var inputDelegate: UITextInputDelegate? var selectedTextRange: UITextRange? { // Update visuals when assistive technologies change selection willSet { inputDelegate?.selectionWillChange(self) } didSet { inputDelegate?.selectionDidChange(self) } } }
-
-
- 0:01 - Introduction
What makes reading apps an accessibility challenge distinct from UI navigation, and what the session covers — the characteristics of a great reading experience, extending UIKit and SwiftUI text views, and making custom text accessible.
- 1:26 - Characteristics
Reading apps present unique accessibility challenges distinct from standard UI navigation, requiring fluid movement through text for technologies like VoiceOver and Speak Screen. This session covers three goals — granular navigation, continuous reading, and text selection — using UIKit, SwiftUI, and AppKit APIs.
- 3:45 - Standard views
UITextView, SwiftUI's TextEditor and selectable Text, and NSTextView on macOS all adopt UITextInput automatically, providing line, word, and character navigation and accessible text selection. The accessibilityNextTextNavigationElement and accessibilityPreviousTextNavigationElement APIs (and the new accessibilityLinkedGroup for SwiftUI) connect separate text elements so VoiceOver can move between them seamlessly, while the causesPageTurn trait provides page turning automatically during read-all gestures.
- 14:05 - Custom text
When using custom or custom-rendered text — such as scanned images — adopting the full UITextInput protocol gives VoiceOver and Speak Screen the same granular navigation and selection capabilities as native text views. This requires implementing text geometry methods like selectionRects(for:), a tokenizer, and text range methods, and can be paired with UITextInteraction for visible selection handles.