ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
カスタムローターを使ったVoiceOverの効率性
カスタムローターを統合し、あなたのApp内での複雑な状況でユーザーがVoiceOverを使いナビゲーションを受けられるようになる方法をお見せします。複雑に入り組んだインターフェースであっても、カスタムローターがあればユーザーは探りながら進むことができ、VoiceOver頼りの人に対してもローターでナビゲーションを改善できます。 このセッションを最大限に活用するためには、アクセシビリティとiOS、iPadOSのVoiceOverアクセシビリティAPIの全般的な理念に親しんでおくことが望ましいです。概略については、"Making Apps More Accessible with Custom Actions"をご覧ください。
リソース
関連ビデオ
WWDC23
WWDC21
WWDC19
-
ダウンロード
こんにちは WWDCへようこそ
アレックス・ヴァルツァックです “カスタムローターを使った VoiceOverの効率性” VoiceOverにカスタムローターを 追加する方法を紹介します VoiceOverは 画面読み上げ機能です 画面が見えない人の デバイス操作に役立ちます 画面をタップして VoiceOverを起動すると 簡単なジェスチャで UIをナビゲートします
画面が見えない人が ローター使って 操作する方法を いくつか紹介します ダイヤルを回すように 画面上で指を回転すると ローターが起動します 上下にスワイプすることで 前の項目や 次の項目に移動できます カスタムローターをアプリケーションに 追加すれば ユーザー体験が変わります 複雑な操作法が簡略化され アプリケーション内の検索も 上下にフリックするように簡単です いくつかの例を紹介します 複雑な操作を カスタムローターで簡単にします まずは カスタムローターなしの ユーザー体験を見てください 地図アプリケーションで現在地とApple Store 近くの公園を表示します VoiceOverを起動すると UIのレイアウト方向に従って アプリケーションのビューが動くのが分かります アップル ベイストリート サンフランシスコ湾 ベイブリッジ アルカトラズ島
カスタムローターなしのVoiceOverで アプリケーションを使った例です ご覧ください カーソルがApple Storeと公園の間だけでなく 橋や他の地点の間も動きます VoiceOverユーザーは すべての項目を順番に移動しなくてはなりません 一方 画面を見ればアイコンとその色から 特定の項目に目が行きます VoiceOverユーザーが 心地よくアプリケーションを使うには? まずインターフェース上で 視覚的に注意を引く項目を把握します 今回はApple Storeと公園ですね
次に カテゴリー別に グループ分けします カテゴリー内で項目を探索する カスタムローターを作ります やってみましょう Apple Storeと公園のローターを 1つずつ実装します すると各ローターの項目を ユーザーからの距離で並べ替え
その距離をアクセシビリティに 含めることができます これで 一番近くのApple Storeが すぐに見つかります
一番近くの公園も同様です
画面を見て 自分に最も近い場所に 目が行くのと同じですね
2つのローターを 実装した例をご覧ください まずは完成後のApple Storeローターです Apple Stores
Apple チェスナットストリート 0.9マイル Apple ユニオンスクエア 1.8マイル Apple ストーンズタウン 4.6マイル 次に公園のローターです 公園
アラモ・スクエア 0.8マイル コロナ・ハイツ・パーク 1.5マイル ヒッピー・ヒル 1.5マイル ユーザーは各カスタムローターの 並べ替えた場所を移動するので Apple Storeでも公園でも 一番近い場所がすぐに見つかります カスタムローターはすべてのユーザーに 同様の使いやすさを提供します 特にこの地図のように複雑な操作に有効です アプリケーションにローター追加する方法は? accessibilityCustomRotorプロパティで カスタムローターを探します
今回はローターが2つ必要です Apple Store用と公園用に 2つのカスタムローターを設定します
各ローターに 同じ地図の注釈をフィルタリングするので 2のローターに 1つのメソッドを実装するだけです
新たなUIAccessibilityCustomRotorを返す メソッドを作成します カスタムローターは 最小限の追加作業で実装できます ローカライズした名前で UIAccessibilityCustomRotorを初期化し クロージャで基本ロジックを実行 するとVoiceOverが UIAccessibilityCustomRotorと返します
ブロック引数から現在のローターの項目を抽出し 移動可能なリストを用意します searchDirectionは上下のフリップで 前に行くか次に行くかを知らせます この情報を使って 可能性リストの インデックスを増減できます
最初か最後にいることを ユーザーに示すため nil値に戻し VoiceOverは目標物に焦点を当てたままです それ以外は 前後の項目を targetElementとして 新たなUIAccessibilityCustomRotorの 結果に戻して終了します 開発者としてカスタムローターAPIを 活用するために覚えるべきことは クロージャを実装し カスタムローターを accessibilityCustomRotorsに追加することです カスタムローターは アプリケーション内の要素グループに作用します アクセシビリティの改善法をもっと学ぶなら カスタムアクションの相互作用について このトークをチェックしてください 地図上で関連する要素を見つける機能は 既に説明しました 次はカスタムローターをテキストで使う方法です
場所をタップしてパンフレットを表示します 今回はゴールデンゲートパークの パンフレットです やることがいっぱいです テキストを自動で表示する ラインローターで 1文ずつ見ていきます ライン ゴールデンゲートパーク 訪問計画 オランダ風車 3月に満開のチューリップを見に行く 公園の東端にある背の高い建物 イースト・メドー ピクニック場はメンテナンスで閉鎖中 ペット連れの家族に最適な場所 一部のコンテンツは聞けましたが 最初の警報までにかかった時間は? イースト・メドーの警報を聞くまでに その前のすべてを聞く必要がありました まだ聞いてない警報もあります つまり すべての警報を聞くには すべての情報を慎重に聞く必要があります 実際はカスタムローターを実装して 警報のみを表示することができるので 警報のアイコンに気付く速さで 重要な情報が聞けます 警報ローターを見てみましょう 警報 イースト・メドー ピクニック場はメンテナンスで閉鎖中 オーク・ウッドランド山道 ぬかるんだ状態 オーシャン・ビーチ 濃霧と強風に注意 予想どおり 警報ローターは 警報のみを表示するので 公園に行く計画を 効率よく立てられます
textViewには customRotorが for alertsとして1つだけあります textViewの accessibilityCustomRotorsに 含むローターは1つだけです 新しいローターを返すメソッドを実装します 公園のようなロケーションのタイプの代わりに ローターに置きたい テキスト属性のタイプで渡します この場合 それは警報属性です このメソッドの構文は 地図の例と似ています ここでもローカライズした名前で UIAccessibilityCustomRotorを初期化し VoiceOverの移動先の項目を返す クロージャを実装します
すべての警報は textViewの 属性付きテキストで見つけることができます テキスト内のローター項目の範囲から 警報の範囲を特定しますが ユーザーの進みたい方向で異なります ちょうどこの箇所です カスタム警報属性を検索するテキストの範囲と ユーザーのジェスチャに基づいて 検索する方向を決めます 警報の一致で 停止できると分かります その時点で UIAccessibilityCustomRotorの結果を返し 新たなtargetRangeとして渡します またはtargetRangeを nil値にして 最初か最後にいることを示します 注意すべきは targetRangeは UITextRangeであり targetElementは UITextInputプロトコルに適合することです 取り込むには かなりの情報量ですがアプリ ケーションにカスタムローターを実装すると ローターの初期化に使ったブロックから 前後のカスタムローターの結果が得られます
accessibilityCustomRotorsを使えば フィルタリングして特定の項目に絞れます 前半はカスタムローターを使った 地図アプリケーションの改善法でした 次にテキストベースのコンテンツとの 関わり方と強化法を説明しました テキストコンテンツのアクセシビリティを さらに学ぶなら このセッションをご覧ください
カスタムローターや VoiceOverについて学んだ知識で アプリケーションのアクセシビリティを 検証してください 詳細はアプリケーションのテストに関する 過去のセッションをご覧ください VoiceOverの効率を上げるには 次のことが必要です まずインターフェースの 視覚的に最も複雑な領域の特定です VoiceOverを使った場合 使わないのと同じくらい簡単か確かめます 簡単でない場合は 画面が見えてないのと同じです そこでカスタムローターをお勧めします あなたがデザインしたアプリケーションが 誰にでもうまく機能するか確かめてください WWDC 2020をご覧いただき ありがとうございました
-
-
4:04 - mapView.accessibilityCustomRotors = [customRotor(for: .stores), customRotor(for: .parks)]
mapView.accessibilityCustomRotors = [customRotor(for: .stores), customRotor(for: .parks)]
-
4:31 - map rotor 1
// Custom map rotors func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in return UIAccessibilityCustomRotorItemResult( ) } }
-
4:56 - map rotor 2
// Custom map rotors func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in let currentElement = predicate.currentItem.targetElement as? MKAnnotationView let annotations = self.annotationViews(for: poiType) let currentIndex = annotations.firstIndex { $0 == currentElement } return UIAccessibilityCustomRotorItemResult( ) } }
-
5:04 - map rotor 3
// Custom map rotors func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in let currentElement = predicate.currentItem.targetElement as? MKAnnotationView let annotations = self.annotationViews(for: poiType) let currentIndex = annotations.firstIndex { $0 == currentElement } let targetIndex: Int switch predicate.searchDirection { case .previous: targetIndex = (currentIndex ?? 1) - 1 case .next: targetIndex = (currentIndex ?? -1) + 1 } return UIAccessibilityCustomRotorItemResult( ) } }
-
5:17 - Maps rotor 4
// Custom map rotors func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in let currentElement = predicate.currentItem.targetElement as? MKAnnotationView let annotations = self.annotationViews(for: poiType) let currentIndex = annotations.firstIndex { $0 == currentElement } let targetIndex: Int switch predicate.searchDirection { case .previous: targetIndex = (currentIndex ?? 1) - 1 case .next: targetIndex = (currentIndex ?? -1) + 1 } guard 0..<annotations.count ~= targetIndex else { return nil } // Reached boundary return UIAccessibilityCustomRotorItemResult(targetElement: annotations[targetIndex], targetRange: nil) } }
-
8:07 - Text rotor 1
// Custom text rotor func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in var targetRange: UITextRange? // Goal: find the range of following `attribute` let beginningRange = guard let currentRange = else { return nil } switch predicate.searchDirection { } return UIAccessibilityCustomRotorItemResult(targetElement: self, targetRange: targetRange) } }
-
8:20 - Text rotor 2
// Custom text rotor func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in var targetRange: UITextRange? // Goal: find the range of following `attribute` let beginningRange = self.textRange(from: self.beginningOfDocument, to: self.beginningOfDocument) guard let currentRange = predicate.currentItem.targetRange ?? beginningRange else { return nil } let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions switch predicate.searchDirection { } return UIAccessibilityCustomRotorItemResult(targetElement: self, targetRange: targetRange) } }
-
8:37 - Text rotor 3
// Custom text rotor func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in var targetRange: UITextRange? // Goal: find the range of following `attribute` let beginningRange = guard let currentRange = else { return nil } let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions switch predicate.searchDirection { case .previous: searchRange = self.rangeOfAttributedTextBefore(currentRange) searchOptions = [.reverse] case .next: searchRange = self.rangeOfAttributedTextAfter(currentRange) searchOptions = [] } return UIAccessibilityCustomRotorItemResult(targetElement: self, targetRange: targetRange) } }
-
9:06 - Text rotor 4 (end)
// Custom text rotor func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor { UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in var targetRange: UITextRange? // Goal: find the range of following `attribute` let beginningRange = guard let currentRange = else { return nil } let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions switch predicate.searchDirection { } self.attributedText.enumerateAttribute( attribute, in: searchRange, options: searchOptions) { value, range, stop in guard value != nil else { return } targetRange = self.textRange(from: range) stop.pointee = true } return UIAccessibilityCustomRotorItemResult(targetElement: self, targetRange: targetRange) } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。