みなさんはアクセシビリティを考えてアプリを作っていますか?今回はアクセシビリティを考慮したアプリに関わる可能性があり、実装したことがなかったので調査しました。

アクセシビリティとは

Appleが発表しているアクセシビリティの機能として

  • 視覚 
    • 拡大鏡
    • フォントを太くする
    • VoiceOver
  • 聴覚
    • 光や振動での通知
    • サウンド認識でのドアホンや炎の音などの通知
    • ヘッドフォンの音量調整
  • 身体機能
    • 背面タップでのアクション実行
    • Siriとショートカットの連携
    • アイトラッキングでのコントロール

などといった様々な機能が存在します。アクセシビリティ機能は障がいのある方も含め、すべてのユーザーに高品質な体験を届けるための補助機能として存在しています。

その中でも視覚に障がいを持つ方を補助する際に特に大切な機能であるVoiceOverについて今回調べていきます。

VoiceOver

VoiceOverは、画面が見えなくてもAppのインターフェイスを体験できるよう、画面の表示内容の説明を読み上げる機能です。

https://developer.apple.com/jp/accessibility/

公式によるとUIを画面を見ずとも理解できるように説明してくれる機能のようです。つまり開発者側はVoiceOverで発声する文言を注意して決定していく必要があると言えますね。

VoiceOverを有効化、無効化する方法

これら5つの方法があるようです。試しに利用する際には無効化する方法を覚えておかないと普段の操作とはまるで違うので困ってしまいます。

操作方法

使う際の基本的な操作方法は以下のようになっています。

フォーカスの移動1本指でスワイプ操作
要素の選択1本指でタップ操作
要素の決定1本指でダブルタップ操作

読み上げ対象となるUIパーツ

基本的にUIKitに含まれているものであればUIAccessibilityプロトコルに準拠しているので次のインスタンスメソッドを利用して有効化することですぐに読み上げられます。

Storyboardを利用して配置したUIパーツは基本的にデフォルトでAccessibilityが有効になっています。(UIViewは無効がデフォルト)

またSwiftUIにて実装したものに関してもアクセシビリティ要素については自動で生成されるようです。

isAccessibilityElement

意図的にVoiceOverの対象に入れるような場合に明示的にYESを指定します。(UIViewのデフォルトはNO)

また親ViewでこのプロパティがYESの場合には子ViewはVoiceOverの読み上げ対象になりません。説明のためのGifを以下に示します。

作成したサンプルアプリでは親Viewとして赤と青の背景色で設定したUIViewを置き、その中にUILabelUIButtonなどを用意しています。

左のgifでは親Viewのみにフォーカスが当たっていて、中のUILabelやUIButtonにフォーカスが当たっていないことがわかるかと思います。(UILabelなどがVoiceOverに読んでもらえない)

右のgifでは親Viewにはフォーカスが当たらず、中のUILabelやUIButtonにフォーカスが当たっていることがわかるかと思います。(UILabelなどがVoiceOverに読んでもらえる)

下の二つのUISwitchでは、親UIViewisAccessibilityElementを以下のように変更しています。

@IBAction func toggleStoryBoardViewAccessibility(_ sender: UISwitch) {
  baseView.isAccessibilityElement = sender.isOn
}

このように親ViewがAccessibilityElementと認識されてしまうと内部の要素が読まれない、という状況になるため注意が必要です。

読み上げの内容

UIAccessibilityを利用してVoiceOverはユーザーにテキストを提示します。UIAccessibilityのプロパティの中でもaccessibilityLabelは読み上げ内容を、accessibilityHintは少し長い詳細な説明を設定できます。

accessibilityTraitでは要素がどのように動作するかが表されています。いくつか種類があるので詳細はドキュメントを参照すると良いでしょう。こちらも目的に応じて自作ビューでは適切に選択すると効果的です。https://developer.apple.com/documentation/uikit/uiaccessibilitytraits

読み上げの内容と順序は [Label][Value][Trait][Hint]となっています。VoiceOverをオンにしてアクセシビリティの以下の画面の読み上げ速度の調整バーを選択してみてください。

「読み上げ速度、50%、調整可能、値を調整するには1本指で上または下にスワイプします」

と読み上げるはずです。ユーザーはLabelを順々に聞いていき目的の項目に移動することになるので、素早く項目にたどり着くためにLabelでの端的な説明を求められることがこの読み上げ順序から分かると思います。

具体的な実装を示すため、先のサンプル画面下部のAccessibilityをトグルさせていたUISwitchを以下のコードで喋らせてみます。

private func setupAccessibility() {
    storyboardAccessibilitySwitch.isAccessibilityElement = true
    storyboardAccessibilitySwitch.accessibilityLabel = "赤色Viewのアクセシビリティ"
    storyboardAccessibilitySwitch.accessibilityTraits = .none
    storyboardAccessibilitySwitch.accessibilityHint = "赤色の親Viewのアクセシビリティ機能を制御します"
}

これを実装してUISwitchにカーソルを当てると

「赤色Viewのアクセシビリティ、1、赤色の親Viewのアクセシビリティ機能を制御します」

と喋るようになります。(traitについては今回.noneを指定しているので省略して読まれます)

読み上げの順序

基本は左から右、上から下に読み上げますが、要素の配置によっては逆に読んでしまうことがあります。またまとめて読んでほしい要素もあると思います。その場合UIViewのaccessibilityElementsをオーバーライドして要素をグループとして定義することであるまとまりごとで読んでくれるようになります。

例として先のサンプル画面下部のトグルスイッチを見てみましょう。(音が出ます)

「左から右、上から下に読み上げ」の原則から、何もしない場合

IB Accessibility -> SwiftUI Accessibility -> 左Switch -> 右スイッチ

と読まれてしまいます。

IB Accessibility -> 左Switch -> SwiftUI Accessibility -> 右スイッチ

のように項目名と内容が連続して読まれる方が自然ですよね。

このように対応したものが以下の動画です。(音が出ます) 違いをわかりやすくするために左のみ対応しました。

左はまとまってフォーカスされてラベル -> スイッチの順で喋っているのに対し、右はまずラベルにフォーカスが当たり、ユーザーが操作してスイッチにフォーカスしているのがわかりますね。

実装例として以下に示します。UILabelやUISwitchをラップしたToggleViewを定義しており、labeltoggleをまとめたibElementのようにまとめる項目ごとに作成し、accessibilityElementsをgetterで返すような実装をすることで実現しています。

class ToggleView: UIView {
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var toggle: UISwitch!
    
    private var _accessibilityElements:[Any]?
    
    override var accessibilityElements: [Any]? {
        set {
            _accessibilityElements = newValue
        }
        get {
            if let _accessibilityElements = _accessibilityElements {
                return _accessibilityElements
            }
            
            var elements = [UIAccessibilityElement]()
            let ibElement = UIAccessibilityElement(accessibilityContainer: self)
            //ここでUILabelやUIToggleの文言やTraitなどを拡張したViewでも引き継いでいる
            ibElement.accessibilityLabel = label.text 
            ibElement.accessibilityTraits = toggle.accessibilityTraits
       
       // 作成されるViewのフォーカスされるサイズを中身の要素のframeを結合したサイズに設定
            ibElement.accessibilityFrameInContainerSpace = label.frame.union(toggle.frame)
            elements.append(ibElement)
            _accessibilityElements = elements
            return _accessibilityElements
        }
    }
}

気をつけたいこととしてはUIAccessibilityElementArrayに追加した順に遷移していくので、複数のViewをまとめて対応する場合でも「左から右、上から下に読み上げ」を遵守することが挙げられます。日本語のアプリで縦書きでもないのに右から左にものが並んでいたら違和感がありますよね。

おまけ:VoiceOver 認識

iOS14、iPadOS14以降の特定のデバイスにおいて前述の対応をしていなくとも同様な読み上げをしてくれる機能があります。

デバイスベースでAIが認識して対応してくれるようです。

まとめ

事前に検討が必要な点について

  1. どういう要素をVoiceOver対応するのか、切り分けること
  2. 要素が読み上げるhintの内容検討(場合によってはローカライズも必要)
  3. エンジニア側で見て独自Viewを定義している箇所があれば洗い出しておく

感想

調べる間に、実装して動かしてみて浮き出てくる問題点がありそれをどうにか潰している先行記事をいくつも見かけたので、作って実際に触らないとわからない使い勝手の良さ悪さはあると感じました。自分では使わない機能ですが、必要な人が使う際にこの機能がちゃんとあることが重要だとVoice Over機能を調べてみて理解しました。

公式サンプルが単純な実装を説明してくれていて、初学者こそ公式ページを参照する重要性を感じました。

参考文献



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる