VoiceOver対応はどうやるのか

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

# アクセシビリティ要素の読み上げ内容を設定する
## そもそもどんな感じで読み上げられるのか
要素は`状態1` `accessibilityLabel` `accessibilityValue`  `状態2` `accessibilityCustomActionの説明` `accessibilityHint`のように読み上げられる。この時読み上げられるのは設定されているものだけ。

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

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

<img width="300" alt="IMG_0509.PNG (62.1 kB)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661548/ffe66c02-48e0-19b5-08e6-2b5bd8835cea.png">


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

## [accessibilityLabel](https://developer.apple.com/documentation/objectivec/nsobject/1615181-accessibilitylabel)
良くあるのが、画像`icon.png`を設定したボタンの読み上げが`icon`のように画像名になってしまっているパターン。
これだと何のボタンなのかわからないので、適切な読み上げ文言を設定する必要がある。
この時に設定するのが`accessibilityLabel`。
```accessibilityLabel.swift
button.accessibilityLabel = "カテゴリ"
```

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

## [accessibilityValue](https://developer.apple.com/documentation/uikit/uiaccessibilityelement/1619583-accessibilityvalue)
要素が何かしらの動的な値を持つ場合、`accessibilityValue`を適宜設定する必要がある。

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

<img width="300" alt="IMG_0511.PNG (42.5 kB)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661548/8346c2c1-2f3d-79b2-785e-3dd42c4b9f42.png">

```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](https://developer.apple.com/documentation/objectivec/nsobject/1615202-accessibilitytraits)
`accessibilityTraits`には要素の特性を設定する。
`accessibilityTraits`を適切に設定すると、ユーザは要素をダブルタップ（VoiceOverでは通常のタップの代わりにダブルタップする）したときに何が起こりそうか予想ができる。

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

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

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

## [accessibilityHint](https://developer.apple.com/documentation/objectivec/nsobject/1615093-accessibilityhint)
`accessibilityHint`には、`accessibilityTraits`では説明しきれないような振る舞いに関する文言を具体的に設定する。

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

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

# アクセシビリティ要素のフォーカスの設定
## 要素をまとめる（[isAccessibilityElement](https://developer.apple.com/documentation/objectivec/nsobject/1615141-isaccessibilityelement)）
labelやtableViewCellのcontent等は特に設定しなければ個々にフォーカスされる。
そのままだとフォーカスの移動のためのスワイプ回数が増えてしまい、なかなか煩わしい。

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

<img width="375" alt="IMG_7196A5DCDDE3-1.jpeg (66.3 kB)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661548/aa0b8251-a974-f6bf-7f51-f53f26d43c32.jpeg">


```swift
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](https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements)）
レイアウトによってはフォーカスが左上から右下に向かわず、ぐちゃぐちゃな順番でフォーカスされることがある。
これを防ぐために、親要素で明示的に`accessibilityElements`を設定すると、その順番でフォーカスされるようになる。
```accessibilityElements.swift
accessibilityElements = [
  label1,
  button1,
  label2,
  button2,
  view,
] as [UIView]
```

## 背景の要素を選択できなくする
presentはしないが、モーダルのような挙動をするViewに`accessibilityViewIsModal`を設定すると、背景の要素をフォーカスできないようにできる。
たとえば次の画像のように、`モーダルを表示`ボタンを押すと`isHidden`がfalseとなり表示されるViewがあるときを考える。
<img width="300" alt="IMG_0515.PNG (145.2 kB)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661548/1a74bd21-7711-67ba-8252-60318989e9f4.png"><img width="300" alt="IMG_0516.PNG (168.7 kB)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661548/ce6c2a64-b908-6490-e677-dee00572653f.png">


モーダルViewを表示した状態でVoiceOverをオンにしてフォーカスを移動すると背景にある`モーダルを表示`ボタンにフォーカスしてしまう。
<img width="300" alt="IMG_0517.PNG (195.4 kB)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661548/3f7cf2cc-400c-8398-2be4-7aa2d2ccce37.png">


以下のように`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](https://developer.apple.com/documentation/objectivec/nsobject/1615150-accessibilitycustomactions)
[要素をまとめる](https://qiita.com/akadateppei/items/52ac4d072f8840ee4fa8#%E8%A6%81%E7%B4%A0%E3%82%92%E3%81%BE%E3%81%A8%E3%82%81%E3%82%8Bisaccessibilityelement) のように親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
次の画像のように、複数の要素が存在するスクロール可能なタブがあったとする。
この時に何もアクセシビリティ対応していなければ、タブより下の要素にフォーカスするまでにタブの要素回左スワイプをしなければならない。

<img width="288" alt="image.png (64.0 kB)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661548/7cc6f09f-62b7-d8b5-38cf-e48af574c3f6.png">


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

この対応として、必要に応じて各[アクセシビリティ関連のメソッド](https://developer.apple.com/documentation/objectivec/nsobject/uiaccessibilityaction)をoverrideすると良い。
この辺りは[もう一歩進んだVoiceOver対応](https://developers.freee.co.jp/entry/one_step_ahead_of_voiceover_experience)が参考になった。

上記のタブ以外にも、スクラブジェスチャ（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)`とすると、フォーカスしている要素が出力される。