2
Help us understand the problem. What are the problem?

posted at

updated at

VoiceOver対応はどうやるのか

VoiceOverで、最低限対応したいものについてまとめました。

アクセシビリティ要素の読み上げ内容を設定する

そもそもどんな感じで読み上げられるのか

要素は状態1 accessibilityLabel accessibilityValue 状態2 accessibilityCustomActionの説明 accessibilityHintのように読み上げられる。この時読み上げられるのは設定されているものだけ。

状態1, 2にはそれぞれaccessibilityTraitsや、要素の種類、状態によって適切なものが適切な位置に入る。

たとえばUIButtonで、isSelected = true, title="ほげ", accessibilityValue="ふが", accessibilityHint="ぴよ"のような盛り盛り設定値の場合には画像のように読み上げられる。

IMG_0509.PNG (62.1 kB)

何も設定していないisAccessibilityElement=trueなUIViewはフォーカスされるが、設定値が全て空なので読み上げられない。

accessibilityLabel

良くあるのが、画像icon.pngを設定したボタンの読み上げがiconのように画像名になってしまっているパターン。
これだと何のボタンなのかわからないので、適切な読み上げ文言を設定する必要がある。
この時に設定するのがaccessibilityLabel

accessibilityLabel.swift
button.accessibilityLabel = "カテゴリ"

他にも、読み上げ内容のないUIViewや、画面を見ないと意味がわからないような文言のボタンはaccessibilityLabelを適切に設定する必要がある。

accessibilityValue

要素が何かしらの動的な値を持つ場合、accessibilityValueを適宜設定する必要がある。

たとえば次のようなボタンを押すとラベルのカウントを1ずつ増やすようなものを考えると、ラベルの値はボタンを押すたびに変更されるので、accessibilityValueも変更する必要がある。

IMG_0511.PNG (42.5 kB)
accessibilityValue.swift
class ViewController: UIViewController {
    private var count: Int = 0 {
        didSet {
            countLabel.text = String(count)
            // countの変更に追従してaccessibilityValueも変更する
            countLabel.accessibilityValue = String(count)
        }
    }

    @IBOutlet private var countLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        // accessibilityLabelは不変なのでviewDidLoadで一度設定すればOK
        countLabel.accessibilityLabel = "カウント"
        count = { count }()
    }

    @IBAction private func didTapIncrementButton(_ sender: UIButton) {
        count += 1
    }
}

accessibilityTraits

accessibilityTraitsには要素の特性を設定する。
accessibilityTraitsを適切に設定すると、ユーザは要素をダブルタップ(VoiceOverでは通常のタップの代わりにダブルタップする)したときに何が起こりそうか予想ができる。

accessibilityTraits.swift
view1.accessibilityTraits = .button
view2.accessibilityTraits = [.button, .selected]

配列で複数のtraitを設定することができるので、その要素を説明できるようなtraitの組み合わせを設定すると良い。
UIButtonのようにUIKitのコンポーネントは適切なものが設定されている場合があるので毎度設定が必要というわけではない

たとえば、UIView等の非ButtonのViewにaddGestureRecognizerしてボタンのように扱っている箇所では、accessibilityTraitsを設定した方がいい。
設定しなければ、ユーザはそのViewがボタンのように振る舞う要素だということがわからないため、実質操作できる要素を一つ失うことになる。

accessibilityHint

accessibilityHintには、accessibilityTraitsでは説明しきれないような振る舞いに関する文言を具体的に設定する。

後述のaccessibilityCustomActionを設定している場合などに設定しておくと、どうすれば何が起きるのかがわかってユーザに優しい。

accessibilityHint.swift
view1.accessibilityHint = "ダブルタップでxxxxします"

アクセシビリティ要素のフォーカスの設定

要素をまとめる(isAccessibilityElement

labelやtableViewCellのcontent等は特に設定しなければ個々にフォーカスされる。
そのままだとフォーカスの移動のためのスワイプ回数が増えてしまい、なかなか煩わしい。

まとまりのある要素の親ViewでisAccessibilityElement = trueとし、一つのaccessibilityElementにして、1フォーカスで全て読み上げられるようにすると、より自然な読み上げになり、操作回数の削減もできる。
特に、同様のViewが反復するような場合には是非設定したい。

IMG_7196A5DCDDE3-1.jpeg (66.3 kB)
class ViewController: UIViewController {
    struct Profile {
        let userName: String
        let userId: String
        let profileText: String
    }

    private var profile: Profile? {
        didSet {
            userNameLabel.text = profile?.userName
            userIdLabel.text = profile?.userId
            profileTextView.text = profile?.profileText

            if let profile = profile {
                // separatorを" ,"とすると自然に読み上げられる
                profileView.accessibilityValue = [
                    profile.userName,
                    profile.userId,
                    profile.profileText
                ].joined(separator: ", ")
            } else {
                profileView.accessibilityValue = nil
            }
        }
    }

    @IBOutlet private var profileView: UIView!
    @IBOutlet private var userNameLabel: UILabel!
    @IBOutlet private var userIdLabel: UILabel!
    @IBOutlet private var profileTextView: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()

        profileView.isAccessibilityElement = true
        profileView.accessibilityLabel = "プロフィール"

        profile = .init(userName: "ユーザーネーム",
                        userId: "userId",
                        profileText: "ユーザーのプロフィールを表示しちゃうわね")
    }
}

フォーカス順を設定する(accessibilityElements

レイアウトによってはフォーカスが左上から右下に向かわず、ぐちゃぐちゃな順番でフォーカスされることがある。
これを防ぐために、親要素で明示的にaccessibilityElementsを設定すると、その順番でフォーカスされるようになる。

accessibilityElements.swift
accessibilityElements = [
  label1,
  button1,
  label2,
  button2,
  view,
] as [UIView]

背景の要素を選択できなくする

presentはしないが、モーダルのような挙動をするViewにaccessibilityViewIsModalを設定すると、背景の要素をフォーカスできないようにできる。
たとえば次の画像のように、モーダルを表示ボタンを押すとisHiddenがfalseとなり表示されるViewがあるときを考える。
IMG_0515.PNG (145.2 kB)IMG_0516.PNG (168.7 kB)

モーダルViewを表示した状態でVoiceOverをオンにしてフォーカスを移動すると背景にあるモーダルを表示ボタンにフォーカスしてしまう。
IMG_0517.PNG (195.4 kB)

以下のようにaccessibilityViewIsModalを設定することで、背景のボタンにフォーカスしないようになる。

accessibilityViewIsModal.swift
class ViewController: UIViewController {
    @IBOutlet private var modalView: UIView!

    @IBAction private func didTapOpenModalButton(_ sender: UIButton) {
        modalView.isHidden = false
        modalView.accessibilityViewIsModal = true
    }

    @IBAction private func didTapCloseModalButton(_ sender: UIButton) {
        modalView.isHidden = true
        modalView.accessibilityViewIsModal = false
    }
}

accessibilityViewIsModalは同じ階層の兄弟要素へのフォーカスを制限するため、フォーカスしたいViewの親要素で設定する必要があることに注意。

カスタムアクション

accessibilityCustomActions

要素をまとめる のように親ViewでisAccessibilityElement = trueとして要素をまとめたときなどにおいて、子要素が複数の動作(=action)を持っていたり(複数ボタンがあるなど)、ボタン的な振る舞いの要素が複数のアクションを持っているような場合には、どのアクションをするか選択したい。

そんな時はaccessibilityCustomActionsを設定する。

accessibilityCustomActions.swift
profileView.accessibilityCustomActions = [
    .init(name: "カスタムアクション1", actionHandler: {_ in
        print("カスタムアクション1")
        return true
    }),
    .init(name: "カスタムアクション2", actionHandler: {_ in
        print("カスタムアクション2")
        return true
    }),
]

対象の要素をフォーカスした状態で上下スワイプすることでアクションを選択、ダブルタップで発火できるようになる。

UIAccessibilityElement

次の画像のように、複数の要素が存在するスクロール可能なタブがあったとする。
この時に何もアクセシビリティ対応していなければ、タブより下の要素にフォーカスするまでにタブの要素回左スワイプをしなければならない。

image.png (64.0 kB)

出典: https://cocoapods.org/pods/ACTabScrollView

この対応として、必要に応じて各アクセシビリティ関連のメソッドをoverrideすると良い。
この辺りはもう一歩進んだVoiceOver対応が参考になった。

上記のタブ以外にも、スクラブジェスチャ(Zを描くようなジェスチャ、popやdismissに使う)、magicTap(二本指でダブルタップ、重要な状態を切り替えるために使う)などの対応を入れておくと快適に操作できるようになる。

VoiceOver対応で便利なもの

VoiceOverの切り替え

設定 -> アクセシビリティ -> ショートカット にてVoiceOverを選択しておけばホームボタン/電源ボタン?のトリプルタップでVoiceOverの切り替えが可能になる。

開発中にアクセシビリティが適切か確認したい時にいちいち設定を開かなくていいので便利
特にVoiceOver操作に慣れない間は、一度VoiceOverをオンにしてしまうとオフにしたくてもできないみたいなことが起こり得るので、その回避にも。

読み上げの可視化

VoiceOverの読み上げはたまに聞き取れないことがあり、「なにいってんだ....?」となる。
これを避けるために、読み上げないようの可視化をすると良い。

設定 -> アクセシビリティ -> VoiceOver -> キャプションパネルをオンにするとスクリーン下に読み上げ文言の字幕が表示されるようになる。

Accessibility Inspector

上記VoiceOver切り替えや、読み上げの可視化のもう一つの方法としてAccessibility Inspectorがある。

xcode -> Open Developer Tool -> Accessibility Inspectorで起動できる
accessibility関連の設定値の可視化や、voiceover操作ができる。

どの要素の読み上げかわからないとき

稀に謎の読み上げがされることがある。

私の場合は右スワイプでフォーカス移動をしていると、フォーカスが消失してselect to enable accessibility for this contentと読み上げられることがあった。
フォーカスされないので、原因となる要素の発見ができなかった。

そこで、UIAccessibilityFocusedElementを利用すると、フォーカスしている要素を特定することができる。
xcodeのコンソールでpo UIAccessibilityFocusedElement(0)とすると、フォーカスしている要素が出力される。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
2
Help us understand the problem. What are the problem?