ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIとUIKitを使ったアクセシブルなアプリの作成
UIフレームワークの進歩により、リッチでアクセシブルな体験体験の作成がどれほど容易になるかを説明します。VoiceOverなどのテクノロジーが、アクセシビリティトレイトやアクションを通して、いかにアプリのインターフェイスとインタラクションしやすくなるかもご覧ください。みなさんのアクセシビリティ体験を洗練するSwiftUIの最新のアップデートや、みなさんのUIKit アプリのアクセシビリティ情報を最新に保つ方法をお話しします。
関連する章
- 0:00 - Welcome
- 1:30 - Explore the toggle trait
- 2:46 - Discover multi-platform accessibility announcements
- 3:58 - Assign priority to announcements
- 6:36 - Meet the zoom action
- 8:00 - Refine VoiceOver direct touch experiences
- 11:08 - Customize accessibility content shapes in SwiftUI
- 12:48 - Keep accessibility attributes up-to-date in UIKit using block-based setters
リソース
関連ビデオ
WWDC23
-
ダウンロード
♪ ♪
Allison:みなさん こんにちは 私はAllisonで アクセシビリティのエンジニアです 今日はみなさんのアプリを さらにアクセシブルにするための 画期的な新しい方法について説明します Appleではアクセシビリティは 作るものすべてに必須であり 私たちは誰しもがテクノロジーに アクセスできるべきだと考えています アプリをアクセシブルにするのは 非常に簡単であって欲しいと思います 昨年 私たちは数多くの 機能拡張を行い 誰もが最高のアプリ体験ができるよう 努めてきました 本セッションでは 人々が 新しい画期的な方法で アプリとインタラクトできるような APIを詳しく見ていきます その次に SwiftUIのアプリの コンテンツのために アクセシビリティビジュアルを 改善する方法について話します 最後には アクセシビリティの属性を UIKit内で最新に保つための より良い方法について学びます ではアクセシビリティの 機能拡張についてからはじめましょう 写真編集のアプリを手掛けています 私のアプリでは ライブラリやカメラからの写真に 素敵な画像修正を加えられます 様々なフィルターを適用したり 写真の色合いを変えたり アプリのピアノの鍵盤を使って 画像にあったカスタム音楽を 作ったりできます アプリに組み込める アクセシビリティエンハンスメントを 少し見ていく事にしましょう 私の写真アプリのフィルターのページに オンかオフの状態に切り替える カスタムボタンがあります
「Filter」のスイッチボタンで 画像フィルターのオンオフを切り替えます システムはこのカスタムUIの 正しいアクセシビリティのヒントや タイトルを知りませんが 私たちは他のシステムのトグルに 見合ったアクセシビリティ体験を 確実に提供したいわけです そこで新しいアクセシビリティの特性 isToggleが役立ちます フィルターボタンを表す構造体があります その構造体のボディに フィルターを切り替えられる ボタンを作成します ボタンの色はフィルター状態の 変数に基づいて更新されます そして isToggleの特性を accessibilityAddTraitsのmodifierの フィルターボタンに追加します isToggleは適切なアクセシビリティヒントと 「switch button」の説明を与えてくれます VoiceOver:「Filter Switch button」 「Double-tap to toggle setting」
Allison:新しいトグルの特性は UIKitでも利用可能です viewDidLoadメソッドで ボタンビューを設定します そしてボタンのaccessibility Traitsプロパティに .toggleButtonを含むよう設定します 写真付フィルターアプリで 写真ナビゲーションバーボタンに 新しいアナウンスメントを加えて 写真ビューがロード中である事を 知らせたいと思います Accessibility Notificationsが これを助けてくれる新しいAPIです Accessibility notificationsによる 統一されたマルチプラットフォーム方法で アプリでアシスティブテクノロジーを 使っている人に アナウンスメントを作成したり 情報を伝達したりできます Accessibility notificationsが 作成できるアプリは SwiftUIと UIKitとAppKitを実行するアプリです Accessibility Notificationでは アナウンスメントや レイアウトの変更や画面の変更 ページスクロールの通知を Swift独自の方法で送れます 写真ツールバーボタンが押される時は アナウンスメントを出したいですね VoiceOver:「Photo, Button」 「Photos Loading photos view」 Allison:ツールバーボタンの実行で アナウンスメントを出せます アナウンスメントの作成に使えるのは AccessibilityNotification.Announcementで 文字列パラメータを 「Loading Photos View」とします アプリでカメラのナビゲーション バーボタンが押された時も 3つのアナウンスメントを 作成したいと思います 最初のアナウンスメントの 「Opening Camera」と 3つ目の「Camera Active」が 最も重要なアナウンスメントです VoiceOverの現在のアナウンスメントの スピーチパターンを聞いてみましょう 2つ目の「Camera Loading」が 「Opening Camera」に被って いるのが分かります VoiceOver:「Camera button」 「Done Open--camera--camera active」 Allison:現在 SwiftUIやUIKitでは アナウンスメントの優先度も 設定できるので アシスティブテクノロジーによって 話されるアナウンスメントの 順番の重要性が設定できます これによって 必ず聞かせたい アナウンスメントと 時間がなければ無視する物とを コントロールできます この情報の重要性を特定するのに 使える3つの アナウンスメント優先度は highとdefaultとlowです Highの優先度を持つアナウンスメントは 他のスピーチに割り込めますが 一旦始まれば割り込みはされません Defaultの優先度を持つアナウンスメントは 既存のスピーチに割り込めますが 新たなスピーチが始まれば 割り込まれることがあります Lowの優先度を持つアナウンスメントが 話される順番としては 他のスピーチが完了していて 新たなアナウンスメントも 始まっていない場合です 写真アプリではアナウンスメントの 優先度を利用して 割り込みのある文字列を修正できます 属性文字列から作られた アナウンスメントが3つあります SwiftUIでは優先度の設定を行う所は accessibilitySpeechAnnouncement Priorityの文字列優先度です 2番目のアナウンスメント 「Camera Loading」は 重要性が最も低いので lowの優先度を与えます 最後のアナウンスメント 「Camera Active」が最も重要なので highの優先度を与えます 次に 属性文字列をAccessibility Notificationに渡します まず デフォルトの優先度の アナウンスメントを割り当て 次にlowの優先度で それからhighの優先度です ここでlowの優先度のアナウンスメントが デフォルト優先度に割り込まない一方 High優先度のアナウンスメントが defaultとlowのアナウンスメントに 割り込む様子を見てください VoiceOver:「Camera button」 「Done Opening camera--camera active」 Allison:UIKitでも同様の 順序づけが可能です アナウンスメントの優先度をNSAttributed Stringのキーバリューペアに設定します 使用するキーは UIAccessibilitySpeechAttribute AnnouncementPriorityで 値を適切なUIAccessibility Priorityに設定します そして属性をattributed string initializerに渡します アプリに戻ると 画像ビューでは 物理的に触れたりつまんだりして ズームインやズームアウトができます アシスティブテクノロジーを有効にすると この物理的な指の動作は 達成しにくくなります アクセシビリティズームアクションでは アシスティブテクノロジーが有効な時 UIエレメントでズームインや ズームアウトができるようになります 画像にズームアクションを加えます ZoomingImageView構造体の ボディに画像があります 最初にaccessibilityZoomActionの modifierを加えます ズームアクションの方向に基づいて コンテンツのズームインや ズームアウトを行い AccessibilityNotification. Announcementをポストします これらの変更でVoiceOverのズーム機能が どうなったか見てみましょう VoiceOver:「Zooming image view image」 「Zoom」 「2x zoom 3x zoom」 「4x zoom 3x zoom」 Allison:ズームのトレイトと アクションはUIKitでも追加できます まず 画像ビューを含む事になる ズームビューを作成します 次に supportsZoomトレイトを 画像トレイトと並べて ズームビューに追加します そしてaccessibilityZoomInAtPointと accessibilityZoomOutAtPointを実装し このそれぞれがbooleanを戻して ズームが成功か失敗かを示します このそれぞれのメソッドで ズームスケールを更新し ズームの変更を示す アナウンスメントをポストします この画像フィルターのアプリでは 小さなピアノの鍵盤を使って 画像に追加する短い音楽を作成できます 鍵盤を使って画像に合った カスタムトーンが作れます トーンを作ろうとした時の音に対して 現在のVoiceOver体験が どんな感じか見てみましょう
エレメントに触れるたびに VoiceOverが鍵盤ラベルを読み VoiceOverの アクティベーション音が出るのですが これでは鍵盤を素早く続けて 押すのが困難になります 典型的には VoiceOverは安全な 探求体験を与えてくれますが 時にはアプリを適切に使うためには アプリとの直接的インタラクションが 必要となるものです 余分なスピーチや音は無しで 直接ピアノの鍵盤に触れた方が このアプリでは はるかに有用です 今こそ このビューに ダイレクトタッチのトレイト allowsDirectInteractionを 取り入れる時です アクセシビリティダイレクトタッチエリアで VoiceOverのジェスチャーが アプリへと直接渡されていく画面の領域を 特定できるようになります デフォルトの状態ではVoiceOverが ダイレクトタッチエレメントの コンテンツの読み上げと起動の 両方を行います ですが このアプリの場合は 誰かがピアノの鍵盤を触った時には まずピアノの鍵盤エレメントを設定せずにも VoiceOverがサイレントになり すぐにピアノの音が聴けるのが 望ましいと考えました さらには allowsDirect Interactionトレイトには 新たにサポートされる2つの ダイレクトタッチオプションがあります まずは silentOnTouchを特定すれば 誰かがダイレクトタッチエリアを触ったら VoiceOverがサイレントになり アプリはオーディオフィードバックを 自ら送ることができます 2番目は requiresActivationを特定して タッチパススルーの前にVoiceOverが エレメントを起動するよう ダイレクトタッチエリアに 要求させるオプションです こちらがKeyboardKeyView用の コードスニペットです どの鍵盤も長方形で 特定の音を鳴らします 音が鳴るたびにVoiceOverが 発話する問題を修正するために ボタンのダイレクトタッチオプションを silent on touchに設定しました これでVoiceOverが 鍵盤ボタンにかかっていても VoiceOverの発話が邪魔する事なく 正しい音が流れます UIKitでも新しいダイレクト タッチオプションが追加できます 鍵盤ボタンをUIButtonとして作成できます 次にallowsDirectInteractionの accessibilityTraitsを追加します アクセシビリティ ダイレクトタッチオプションを UIKitで設定する時は このトレイトが要求されます 最後は accessibility DirectTouchOptionsに silentOnTouchのオプションを追加します アクセシビリティトグルトレイトや アナウンスメント優先度やズームトレイト そしてダイレクトタッチオプションで SwiftUIやUIKit アプリとのアシスティブ テクノロジーインタラクションが コントロールしやすくなります 次は SwiftUIのアクセシビリティ コンテンツの形状の種類についてです この種類はアクセシビリティ エレメントのパスを設定し 画面上でのアクセシビリティエレメントの 外見をコントロールします 以前はインタラクションコンテンツの 形状の種類が アクセシビリティの形状と ヒットテストの形状を変えていました 今ではヒットテストの形状に 影響を与える事なく アクセシビリティコンテンツの 形状だけを左右する Accessibility Content Shape kindがあります エレメントが 例えば丸のような カスタムの形状を必要とする時 計算されたアクセシビリティのカーソルが 画面の他のアイテムを遮るかもしれません この例では アクセシビリティパスは 正方形なので 赤い丸のコンテンツにマッチしていません Accessibility Content Shape kindが ビューに適用されると 基礎となる アクセシビリティジオメトリが modifierで指定された形状で更新されます これでエレメントのパスは 既存のSwiftUIの形状で 素早く更新できるようになります 丸の画像で丸いボタンを作りました フレームを設定して アクセシビリティラベルを 色に合わせて redにします 最後に アクセシビリティタイプと Circleを形状として ビューにコンテンツ形状のmodifierを 追加できます
これでアクセシビリティパスが 赤い丸ボタンの 丸い形状と一致します 最後は UIKitアクセシビリティに 間もなく追加される ブロックベースの属性セッターです 写真編集アプリでは 写真フィルターの有無を表す 画像ビューのアクセシビリティ値が 欲しいと思います 今では簡単な方法で ビューの基礎にある アクセシビリティ属性を 表示されたUIと常に一致させられます ブロックベースの属性セッターを使えば それが可能です 新しいアクセシビリティのブロックAPIで 属性が必要な時はいつでも 値を直接に保存するのではなく 評価されたクロージャが付与できます このクロージャはビューが アシスティブテクノロジーによって 参照されたり評価されるごとに 再評価されます ビューコントローラ用の viewDidLoad法によって 作成されたクロージャで 物事が単純化できます accessibilityValueBlockプロパティを zoomViewに設定して 画像のフィルターの有無に基づいて 常に値を更新しておきます クロージャはこの属性に 適切なタイプを戻さなければならず これはオプショナルな文字列です 循環サイクルにならないよう 弱い参照を使用していますね ブロックはクラスライフサイクルの 最初に加えるのがお勧めで 適切なアクセシビリティ属性情報で クラスを始められます これでアクセシビリティ属性は はるかに簡単に維持できます 誰かがVoiceOverカーソルを 新しいエレメントに動かすたびに VoiceOverは最初にクロージャのある 属性セットを探し クロージャを再評価します
カスタムのUIを作成する時は トグルのようなアクセシビリティトレイトや ダイレクトタッチオプションのような 機能を統合して あらゆる人に対しての ユーザビリティを増強しましょう
次に SwiftUIのカスタム形状で 自分のビューを考えてみましょう アクセシビリティの形状が UIにマッチしていないなら カスタムのアクセシビリティ形状を 導入することを考えましょう そして最後に 自分がどのように アクセシビリティ属性を 設定しているのかを評価し ブロックベースのセッターが アプリに適しているか判断しましょう Appleでは アクセシビリティは 人権であると信じています みなさんの力であらゆる人の生活をよりよくし 力を与えるテクノロジーを 作り出せるはずです これらの新しいAPIを使えば アシスティブテクノロジーに 頼る人々にとって アプリの使い易さがグッと上がります 是非すべてを活用して 素晴らしい アクセシブルなアプリを作ってください ご視聴ありがとうございました
-
-
1:54 - Add the accessibility toggle trait
import SwiftUI struct FilterButton: View { @State var filter: Bool = false var body: some View { Button(action: { filter.toggle() }) { Text("Filter") } .background(filter ? darkGreen : lightGreen) .accessibilityAddTraits(.isToggle) } }
-
2:31 - Add the accessibility toggle trait with UIKit
import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let filterButton = UIButton(type: .custom) setupButtonView() filterButton.accessibilityTraits = [.toggleButton] view.addSubview(filterButton) } }
-
3:43 - Post an accessibility notification
import SwiftUI struct ContentView: View { var body: some View { NavigationView { PhotoFilterView .toolbar { Button(action: { AccessibilityNotification.Announcement("Loading Photos View") .post() }) { Text("Photos") } } } } }
-
5:13 - Assign announcement priority
import SwiftUI struct ZoomingImageView: View { var defaultPriorityAnnouncement = AttributedString("Opening Camera") var lowPriorityAnnouncement: AttributedString { var lowPriorityString = AttributedString("Camera Loading") lowPriorityString.accessibilitySpeechAnnouncementPriority = .low return lowPriorityString } var highPriorityAnnouncement: AttributedString { var highPriorityString = AttributedString("Camera Active") highPriorityString.accessibilitySpeechAnnouncementPriority = .high return highPriorityString } // ... }
-
5:46 - Post announcements with priority set
import SwiftUI struct CameraButton: View { // ... var body: some View { Button(action: { // Open Camera Code AccessibilityNotification.Announcement(defaultPriorityAnnouncement).post() // Camera Loading Code AccessibilityNotification.Announcement(lowPriorityAnnouncement).post() // Camera Loaded Code AccessibilityNotification.Announcement(highPriorityAnnouncement).post() }) { Image("Camera") } } } }
-
6:15 - Assign announcement priority with UIKit
class ViewController: UIViewController { let defaultAnnouncement = NSAttributedString(string: "Opening Camera", attributes: [NSAttributedString.Key.UIAccessibilitySpeechAttributeAnnouncementPriority: UIAccessibilityPriority.default] ) let lowPriorityAnnouncement = NSAttributedString(string: "Camera Loading", attributes: [NSAttributedString.Key.UIAccessibilitySpeechAttributeAnnouncementPriority: UIAccessibilityPriority.low] ) let highPriorityAnnouncement = NSAttributedString(string: "Camera Active", attributes: [NSAttributedString.Key.UIAccessibilitySpeechAttributeAnnouncementPriority: UIAccessibilityPriority.high] ) // ... }
-
6:56 - Add the accessibility zoom action
struct ZoomingImageView: View { @State private var zoomValue = 1.0 @State var imageName: String? var body: some View { Image(imageName ?? "") .scaleEffect(zoomValue) .accessibilityZoomAction { action in let zoomQuantity = "\(Int(zoomValue)) x zoom" switch action.direction { case .zoomIn: zoomValue += 1.0 AccessibilityNotification.Announcement(zoomQuantity).post() case .zoomOut: zoomValue -= 1.0 AccessibilityNotification.Announcement(zoomQuantity).post() } } } }
-
7:18 - Add the accessibility zoom action with UIKit
import UIKit class ViewController: UIViewController { let zoomView = ZoomingImageView(frame: .zero) let imageView = UIImageView(image: UIImage(named: "tree")) override func viewDidLoad() { super.viewDidLoad() zoomView.isAccessibilityElement = true zoomView.accessibilityLabel = "Zooming Image View" zoomView.accessibilityTraits = [.image, .supportsZoom] zoomView.addSubview(imageView) view.addSubview(zoomView) } }
-
7:43 - Respond to accessibility zoom actions with UIKit
import UIKit class ZoomingImageView: UIScrollView { override func accessibilityZoomIn(at point: CGPoint) -> Bool { zoomScale += 1.0 let zoomQuantity = "\(Int(zoomValue)) x zoom" UIAccessibility.post(notification: .announcement, argument: zoomQuantity) return true } override func accessibilityZoomOut(at point: CGPoint) -> Bool { zoomScale -= 1.0 let zoomQuantity = "\(Int(zoomValue)) x zoom" UIAccessibility.post(notification: .announcement, argument: zoomQuantity) return true } }
-
10:10 - Use accessibility direct touch options
import SwiftUI struct KeyboardKeyView: View { var soundFile: String var body: some View { Rectangle() .fill(.white) .frame(width: 35, height: 80) .onTapGesture(count: 1) { playSound(sound: soundFile, type: "mp3") } .accessibilityDirectTouch(options: .silentOnTouch) } }
-
10:46 - Use accessibility direct touch options with UIKit
import UIKit class ViewController: UIViewController { let waveformButton = UIButton(type: .custom) override func viewDidLoad() { super.viewDidLoad() waveformButton.accessibilityTraits = .allowsDirectInteraction waveformButton.accessibilityDirectTouchOptions = .silentOnTouch waveformButton.addTarget(self, action: #selector(playTone), for: .touchUpInside) view.addSubview(waveformButton) } }
-
12:21 - Set the accessibility content shape
import SwiftUI struct ImageView: View { var body: some View { Image("circle-red") .resizable() .frame(width: 200, height: 200) .accessibilityLabel("Red") .contentShape(.accessibility, Circle()) } }
-
13:35 - Update accessibility values using block-based setters with UIKit
import UIKit class ViewController: UIViewController { var isFiltered = false override func viewDidLoad() { super.viewDidLoad() // Set up views zoomView.accessibilityValueBlock = { [weak self] in guard let self else { return nil } return isFiltered ? "Filtered" : "Not Filtered" } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。