カスタムローターを使ったVoiceOverの効率性 – WWDC2020

Session概要

カスタムローターを統合し、あなたのアプリ内での複雑な状況でユーザがVoiceOverを使いナビゲーションを受けられるようになる方法をお見せします。複雑に入り組んだインタフェースであっても、カスタムローターがあればユーザは探りながら進むことができ、VoiceOver頼りの人に対してもローターでナビゲーションを改善できます。 https://developer.apple.com/videos/play/wwdc2020/10116/

VoiceOverローターとは


  • VoiceOver の音量や話す速さを変更したり、画面上で項目間を移動したりすることができ、その他の操作も可能
  • ダイヤルを回すように、画面上で指を回転するとローターが起動する
  • 上下にスワイプすることで、前の項目や次の項目に移動可能
  • 複雑な操作を簡易にすることができる
  • カスタムローター
    • VoiceOverローターの操作をカスタマイズできる機能

アプリにVoiceOverローターを追加する方法


  • accessibilityCustomRotorプロパティでカスタムローターを探す
  • UIAccessibilityCustomRotorを返すメソッドを作成する
    • ローカライズした名前と、クロージャで基本ロジックを実行する
  • カスタムローターAPIを活用するためには、クロージャを実装しカスタムローターをaccessibilityCustomRotorsに追加すること
mapView.accessibilityCustomRotors = [customRotor(for: .stores), customRotor(for: .parks)]

// Custom map rotors

func customRotor(for poiType: POI) -> UIAccessibilityCustomRotor {
    UIAccessibilityCustomRotor(name: poiType.rotorName) { [unowned self] predicate in
        let currentElement = predicate.currentItem.targetElement as? MKAnnotationView
        let annotations = self.annotationViews(for: poiType)
        let currentIndex = annotations.firstIndex { $0 == currentElement }
        let targetIndex: Int
        // 上下のフリップ
        switch predicate.searchDirection {
        case .previous:
            targetIndex = (currentIndex ?? 1) - 1
        case .next:
            targetIndex = (currentIndex ?? -1) + 1
        }
        guard 0..<annotations.count ~= targetIndex else { return nil } // Reached boundary
        return UIAccessibilityCustomRotorItemResult(targetElement: annotations[targetIndex],
                                                    targetRange: nil)
    }
}

カスタムローターをテキストで使う方法

  • テキストを自動で読み上げるラインローターを活用する
  • カスタムローターを利用することで、重要な部分に絞って情報を聞くことができる
  • 実装自体はVoiceOverローターと同様
  • accessibilityCustomRotorsを使用すればフィルタリングして特定の項目に絞れる

// Custom text rotor

func customRotor(for attribute: NSAttributedString.Key) -> UIAccessibilityCustomRotor {
    UIAccessibilityCustomRotor(name: attribute.rotorName) { [unowned self] predicate in
        var targetRange: UITextRange? // Goal: find the range of following `attribute`
        let beginningRange = ...
        guard let currentRange = ...  else { return nil }
        let searchRange: NSRange, searchOptions: NSAttributedString.EnumerationOptions
        switch predicate.searchDirection {   }
        self.attributedText.enumerateAttribute(
            attribute, in: searchRange, options: searchOptions) { value, range, stop in
            guard value != nil else { return }
            targetRange = self.textRange(from: range)
            stop.pointee = true
        }
        ...
        // targetRangeをnil値にして最初か最後にいることを示す
        return UIAccessibilityCustomRotorItemResult(targetElement: self,
                                                    targetRange: targetRange)
    }
}

VoiceOverの効率向上に必要なこと

  • インタフェースの視覚的に最も複雑な領域の特定
    • VoiceOverを使った場合と使わないのと同じぐらい簡単か確認する
    • 簡単でない場合は、画面が見えていない場合も同様なのでカスタムローターの利用を検討する
最新情報をチェックしよう!