VoiceOverで、最低限対応したいものについてまとめました。
アクセシビリティ要素の読み上げ内容を設定する
そもそもどんな感じで読み上げられるのか
要素は状態1
accessibilityLabel
accessibilityValue
状態2
accessibilityCustomActionの説明
accessibilityHint
のように読み上げられる。この時読み上げられるのは設定されているものだけ。
状態1, 2にはそれぞれaccessibilityTraits
や、要素の種類、状態によって適切なものが適切な位置に入る。
たとえばUIButtonで、isSelected = true
, title="ほげ"
, accessibilityValue="ふが"
, accessibilityHint="ぴよ"
のような盛り盛り設定値の場合には画像のように読み上げられる。
何も設定していないisAccessibilityElement=true
なUIViewはフォーカスされるが、設定値が全て空なので読み上げられない。
accessibilityLabel
良くあるのが、画像icon.png
を設定したボタンの読み上げがicon
のように画像名になってしまっているパターン。
これだと何のボタンなのかわからないので、適切な読み上げ文言を設定する必要がある。
この時に設定するのがaccessibilityLabel
。
button.accessibilityLabel = "カテゴリ"
他にも、読み上げ内容のないUIViewや、画面を見ないと意味がわからないような文言のボタンはaccessibilityLabel
を適切に設定する必要がある。
accessibilityValue
要素が何かしらの動的な値を持つ場合、accessibilityValue
を適宜設定する必要がある。
たとえば次のようなボタンを押すとラベルのカウントを1ずつ増やすようなものを考えると、ラベルの値はボタンを押すたびに変更されるので、accessibilityValue
も変更する必要がある。
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では通常のタップの代わりにダブルタップする)したときに何が起こりそうか予想ができる。
view1.accessibilityTraits = .button
view2.accessibilityTraits = [.button, .selected]
配列で複数のtraitを設定することができるので、その要素を説明できるようなtraitの組み合わせを設定すると良い。
UIButtonのようにUIKitのコンポーネントは適切なものが設定されている場合があるので毎度設定が必要というわけではない
たとえば、UIView等の非ButtonのViewにaddGestureRecognizer
してボタンのように扱っている箇所では、accessibilityTraits
を設定した方がいい。
設定しなければ、ユーザはそのViewがボタンのように振る舞う要素だということがわからないため、実質操作できる要素を一つ失うことになる。
accessibilityHint
accessibilityHint
には、accessibilityTraits
では説明しきれないような振る舞いに関する文言を具体的に設定する。
後述のaccessibilityCustomAction
を設定している場合などに設定しておくと、どうすれば何が起きるのかがわかってユーザに優しい。
view1.accessibilityHint = "ダブルタップでxxxxします"
アクセシビリティ要素のフォーカスの設定
要素をまとめる(isAccessibilityElement)
labelやtableViewCellのcontent等は特に設定しなければ個々にフォーカスされる。
そのままだとフォーカスの移動のためのスワイプ回数が増えてしまい、なかなか煩わしい。
まとまりのある要素の親ViewでisAccessibilityElement = true
とし、一つのaccessibilityElementにして、1フォーカスで全て読み上げられるようにすると、より自然な読み上げになり、操作回数の削減もできる。
特に、同様のViewが反復するような場合には是非設定したい。
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 = [
label1,
button1,
label2,
button2,
view,
] as [UIView]
背景の要素を選択できなくする
presentはしないが、モーダルのような挙動をするViewにaccessibilityViewIsModal
を設定すると、背景の要素をフォーカスできないようにできる。
たとえば次の画像のように、モーダルを表示
ボタンを押すとisHidden
がfalseとなり表示されるViewがあるときを考える。
モーダルViewを表示した状態でVoiceOverをオンにしてフォーカスを移動すると背景にあるモーダルを表示
ボタンにフォーカスしてしまう。
以下のようにaccessibilityViewIsModal
を設定することで、背景のボタンにフォーカスしないようになる。
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
を設定する。
profileView.accessibilityCustomActions = [
.init(name: "カスタムアクション1", actionHandler: {_ in
print("カスタムアクション1")
return true
}),
.init(name: "カスタムアクション2", actionHandler: {_ in
print("カスタムアクション2")
return true
}),
]
対象の要素をフォーカスした状態で上下スワイプすることでアクションを選択、ダブルタップで発火できるようになる。
UIAccessibilityElement
次の画像のように、複数の要素が存在するスクロール可能なタブがあったとする。
この時に何もアクセシビリティ対応していなければ、タブより下の要素にフォーカスするまでにタブの要素回左スワイプをしなければならない。
出典: 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)
とすると、フォーカスしている要素が出力される。