もう一歩進んだVoiceOver対応

一歩進んだVoiceOver対応

この記事は freee Developers Advent Calendar 2018 、および Webじゃないアクセシビリティ Advent Calendar 2018 の9日目です。

こんにちは、freeeでiOSアプリ開発をしている @RyoAbe です。

今年春頃、はじめてVoiceOver対応を行った私が、よりよいUXを提供すべくiOS版クラウド会計freeeに改良を加え、そこで得られた知見をご紹介したいと思います。

qiita.com

😥 VoiceOver操作でUXが悪かった画面

freeeでは銀行口座やクレジットカードを連携することにより、利用履歴の明細を取り込んで帳簿付けを簡単にするという「自動で経理」という機能があり、アプリにとってもコア機能の一つになります。

iOS版の「自動で経理」の画面UIは、画面上にカルーセルUIで横並びに明細が配置され(UICollectionView)、画面下には明細ごとに入力するフォーム(UITableView)が配置されています。明細を左右にスワイプすることで画面下のフォームの内容が切り替わります。

iOS版「自動で経理」の画面UI

VoiceOverを使用して操作した動画がこちらになります。(VoiceOverでは左右スワイプしてフォーカスを移動、ダブルタップでボタンを選択します)

youtu.be

当たり前ではありますが、VoiceOverでは画面上の情報を音声を通じて正しく伝えることがとても重要になります。読み上げられないということがあれば、言わば画面上に存在しないも同然と言えます。

動画をご覧いただくと分かるかと思いますが、「下三角▼ボタン」や「×ボタン」にフォーカスが当たったときにファイル名が読み上げられており画面UIを正しく伝えられていないという基本的な問題もさる事ながら、 左右スワイプで明細は切り替わってはいるが、画面下のフォームに到達できないため、フォームを見逃してしまう という致命的な問題があります。 一番右端までスワイプすれば画面下のフォームに到達することは可能ですが、明細数が多い場合には非常にUXが悪いです。

(画面下のフォームをタップすることでフォーカスを移動させることも可能ですが、VoiceOverユーザは基本的には左右スワイプでフォーカスを移動させながら次の要素を見つけるため、やはりフォームに到達することは難しいです)

これでは画面下のフォームに気づくことは難しく、アプリの機能を十分に享受できません。 つまり、VoiceOver対応はUIの要素が読み上げられるだけでは不十分で、通常のアプリ開発と同様、どういったUXを提供するか考え、対応を入れる必要がある場合があります。

原因としては、VoiceOverのカーソルの移動は画面要素の左から右に、上から下に読み上げられるという仕組みになっているため、画面上に配置された UICollectionView にフォーカスがあたると、セル内の要素を全て読み上げなければ画面下のフォームに移動されないためです。

原因は分かっていたものの、どのように修正をすれば良いのか分からず、困っていました😥

🤩 WWDC 2018で解決策が紹介される

WWDCではここ数年、毎年アクセシビリティー関係のセッションがいくつかあります。 今年の WWDC 2018 でもいくつか発表がありましたが、その中の一つ「Deliver an Exceptional Accessibility Experience」内で、私が抱えていた問題の解決策が紹介されていました。

Deliver an Exceptional Accessibility Experience - WWDC 2018
Deliver an Exceptional Accessibility Experience - WWDC 2018 15:20 あたりから

セッションの中で題材として紹介されたアプリのUIも、画面上にカルーセルUI(UICollectionView)、画面下にセル(UITableView)が配置されており、「自動で経理」のUIととても似ています。事例として挙げられていた問題も、 「最後までスワイプしないと次の要素を見つけることができない」と話されており同じです。(こんなタイムリーな奇跡があるのかと思いましたw)

ズバリ解決方法はこう。

「明細は上下スワイプor3本指左右スワイプで切り替えられるようにし、基本操作の左右スワイプで明細から画面下のフォームへ移動できるようにする」

です。

ここで紹介された解決方法の ソースコードも公開されており、動画と合わせて参考にし、改良を入れました。

👨‍💻 いざ改良!

新たに作ったコンポーネントは、上下スワイプをハンドルしたり読み上げる内容を定義する UIAccessibilityElement(MyAccessibilityElementとします)と、 その UIAccessibilityElement を上位層で管理するUIView(MyContainerViewとします) の2つです。 それぞれのクラスの主要部分を抜粋して、解説していきます。

MyAccessibilityElement

まずはフォーカスがあたったときの読み上げ内容を accessibilityValue を override して設定します。 MyContainerView から読み上げるのための情報がつまったモデルを受け取って定義します。 (私の場合だと明細の情報、WWDCのセッションでは犬に関する情報がつまったモデルを使って定義)

override var accessibilityValue: String? {
    get {
        return "\(model.name), \(model.description)"
    }
    set {
        super.accessibilityValue = newValue
    }
}

次に上下スワイプや3指左右スワイプのジェスチャーが実行されたときの処理を実装します。 ジェスチャーが実行されたら、上位層の MyContainerView にセルを切り替えるよう要求します。

// 上スワイプのジェスチャーで呼ばれる
override func accessibilityIncrement() {
    _ = containerView?.accessibilityScrollForward()
}

// 下スワイプのジェスチャーで呼ばれる
override func accessibilityDecrement() {
    _ = containerView?.accessibilityScrollBackward()
}

// 3本指左右スワイプのジェスチャーで呼ばれる
override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {
    guard let containerView = containerView else {
        return false
    }
    switch direction {
    case .left:
        return containerView.accessibilityScrollForward()
    case .right:
        return containerView.accessibilityScrollBackward()
    default:
        return false
    }
}

MyContainerView

次に MyContainerView を作ります。

accessibilityElements を override して、 MyAccessibilityElement を生成して返します。これにより元々フォーカスが当たっていた UICollectionViewCell の各コンポーネントにフォーカスが当たらなり、MyAccessibilityElement 内で定義された accessibilityValue が読み上げられるようになります。

// MyAccessibilityElement を生成して返す
private var _accessibilityElements: [Any]?
override var accessibilityElements: [Any]? {
    set {
        _accessibilityElements = newValue
    }
    get {
        guard _accessibilityElements == nil else {
            return _accessibilityElements
        }
        guard let currentModel = currentModel else {
            return nil
        }

        let accessibilityElement: currentAccessibilityElement
        if let accessibilityElement = accessibilityElement {
            accessibilityElement = accessibilityElement
        } else {
            // 読み上げ対象の MyAccessibilityElement を生成
            accessibilityElement = MyAccessibilityElement(self, currentModel)
            // VoiceOverでフォーカスがあたったときの frame
            accessibilityElement.accessibilityFrameInContainerSpace = collectionView?.frame ?? .zero
            accessibilityElement = accessibilityElement
        }
        _accessibilityElements = [accessibilityElement]
        return _accessibilityElements
    }
}

次に上下スワイプされたときに UICollectionView のセルを切り替える処理を書きます。 これらのメソッドは MyAccessibilityElement からの実行されます。

// 次のセルに切り替える
func accessibilityScrollForward() -> Bool {
    guard let index = currentModelIndex, index < models.count - 1 else {
        return false
    }

    collectionView?.scrollToItem(
        at: IndexPath(row: index + 1, section: 0),
        at: .centeredHorizontally,
        animated: true
    )
    return true
}

// 前のセルに切り替える
func accessibilityScrollBackward() -> Bool {
    guard let index = currentModelIndex, index > 0 else {
        return false
    }

    collectionView?.scrollToItem(
        at: IndexPath(row: index - 1, section: 0),
        at: .centeredHorizontally,
        animated: true
    )
    return true
}

😆そして完成したのがこちら

youtu.be

上下スワイプでセルを切り替えられるように修正したのに加えて、

  • ファイル名が読み上げられていたボタンの正しく読み上げられるように
  • ページャーが 1/10 だったとすると いち すらっしゅ じゅう と読み上げられていたため、 全10明細中1明細 と読み上げられるように
  • 選択中の取引アクションを読み上げられるように

なども入れました。

(近日中にリリース予定。2018/12/9 時点ではまだリリースされておりません)

📱 同僚の全盲のエンジニアに使ってもらいました

昨日の伊原さんの記事 でも紹介されていた、全盲のエンジニアである中根さんに実際に触ってもらいました。

youtu.be

普段から中根さんは個人事業主として会計freeeを利用しており、VoiceOver対応された「自動で経理」を触って「早くリリースして下さい」と言ってくれました。 freee ではこうして実際にVoiceOverユーザに触っていただいてフィードバックをもらえる環境なので、エンジニアとしてとてもやりがいを感じます。

まとめ

私はこの対応を経て、通常のアプリ開発同様、VoiceOver対応は読み上げられるだけでは不十分で、UXを考える必要があるんだなという学びがありました。

特に日本ではVoiceOver対応されたアプリは多くはないですが、いつかアプリ開発においてVoiceOver対応が当たり前となり、その中でよりよいUXを提供しているアプリをVoiceOverユーザが選べるような世界になると良いなと、ふと思いました。

そんな状況が生まれるよう牽引すべく、今後もアプリ開発を頑張っていきたいと思います💪そして、WWDC 本当にありがとう🙏

明日は、

お楽しみに!