アクセシビリティに対応したリーディング体験を作り出す – WWDC2019

Session概要

テキストのスタイルとレイアウトは、優れたリーディング体験をもたらす大きな要素です。CoreTextやTextKitといったテクノロジーは、優れたテキストレイアウトを作成するために必要なツールとなります。
このセッションでは、アクセシビリティに対応したリーディングコンテンツプロトコルを導入し、自動ページめくり機能を追加し、音声出力をカスタマイズすることで、VoiceOver向けにも同様の優れたアクセス体験を作り出す方法について https://developer.apple.com/videos/play/wwdc2019/248/

VoiceOverについて

  • テキストなどの読み上げ機能
  • ショートカット設定
    • 設定 → アクセシビリティ → ショートカット → VoiceOver で可能
  • 画面内に読み上げ可能なコンテンツが見つからないとアラート音がなる
    • そのため、テキストコンテンツを読み上げ可能にする必要がある
    • UIAccessibilityReadingContentの使用する
  • 指でテキストコンテンツをタップすると、音声が流れテキストがハイライトされる

読み上げプロトコル

public protocol UIAccessibilityReadingContent {
 // 触った箇所の行番号を返す
 func accessibilityLineNumber(for point: CGPoint) -> Int
 // 行のコンテンツを返す
 func accessibilityContent(forLineNumber lineNumber: Int) -> String?
 // CGRectを返す
 func accessibilityFrame(forLineNumber lineNumber: Int) -> CGRect
 // コンテンツのページを返す
 func accessibilityPageContent() -> String?
}

プロトコルの組み込み方法

  • 例えば、各ページはSessionItemViewのインスタンスで表現する
  • まずページビューをアクセス可能な要素にするため、 isAccessibilityElementtrueに設定する
  • 次に読み上げプロトコルを組み込む
class SessionItemView: UIView {
 var imageView: UIImageView
 var titleLabel: UILabel
 var identifierLabel: UILabel
 var detailsTextView: UITextView

 enum Layout: Int {
   case image = 0
   case title = 1
   case identifier = 2
   case details = 3
 }
}
class SessionItemView: UIView {
 override init(frame: CGRect) {
   super.init(frame: frame)
   isAccessibilityElement = true
 }
}
extension SessionItemView: UIAccessibilityReadingContent {
  // VoiceOverが理解する表記のLayoutのrawValueを返す
 func accessibilityLineNumber(for point: CGPoint) -> Int {
   // 行番号を利用してページビューにhitTestを実行する
   guard let view = hitTest(point, with: nil) else { return NSNotFound }
   switch view {
   case imageView:
     return Layout.image.rawValue
   case titleLabel:
     return Layout.title.rawValue
   case identifierLabel:
     return Layout.identifier.rawValue
   case detailsTextView:
     if let line = detailsTextView.lineForTouch(atScreenPoint: point) {
       return Layout.details.rawValue + line
     }
   …
   }
}
extension SessionItemView: UIAccessibilityReadingContent {
  // 既知のSubViewに関連付け、テキストを返すアクセシビリティラベルを返す
 func accessibilityContent(forLineNumber lineNumber: Int) -> String? {
   switch Layout(rawValue: lineNumber)! {
   case .image:
     return imageView.accessibilityLabel
   case .title:
     return titleLabel.accessibilityLabel
   case .identifier:
     return identifierLabel.accessibilityLabel
   case .details:
     return detailsTextView.contentFor(line:lineNumber - Layout.details.rawValue)
   }
 }
}

extension SessionItemView: UIAccessibilityReadingContent {
 func accessibilityFrame(forLineNumber lineNumber: Int) -> CGRect {
   switch Layout(rawValue: lineNumber)! {
   case .image:
     return imageView.accessibilityFrame
   case .title:
     return titleLabel.accessibilityFrame
   case .identifier:
     return identifierLabel.accessibilityFrame
   case .details:
     return detailsTextView.screenFrameFor(line:lineNumber - Layout.details.rawValue)
   }
 }
}
extension SessionItemView: UIAccessibilityReadingContent {
  // 既知のSubViewからテキストを集め、1つの文字列として返す
 func accessibilityPageContent() -> String? {
   let sessionTitle = titleLabel.accessibilityLabel ?? ""
   let sessionIdentifier = identifierLabel.accessibilityLabel ?? ""
   let detailText = detailsTextView.accessibilityValue ?? ""
   return "\(sessionTitle), \(sessionIdentifier), \(detailText)"
 }
}

自動ページめくり

  • コマンドが呼び出されVoiceOverがすべてのテキストを読み終わると、ページをめくる必要が出てきます
  • この機能を実現するには、2つのAPIの組み込みが必要
class SessionItemView: UIView {
 override init(frame: CGRect) {
   super.init(frame: frame)
   isAccessibilityElement = true
   accessibilityTraits = .causesPageTurn
 }
}

class SessionItemView: UIView {
 override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {
   var didScroll: Bool?
   switch direction {
   // 前のページに戻る要求を出す実装
   case .previous, .left:
     didScroll = delegate?.turnPreviousPage(self) { didChangePage in
     if didChangePage {
       UIAccessibility.post(notification: .pageScrolled, argument: nil)
     }
   }
   ページを進める要求を出す
   case .next, .right:
     didScroll = delegate?.turnNextPage(self) { didChangePage in
     if didChangePage {
       UIAccessibility.post(notification: .pageScrolled, argument: nil)
     }
   }
   default:
     break
   }
   return didScroll ?? false
 }
}

読み上げ音声のカスタマイズ

NSAttributedString(string: "Arc de Triomphe", attributes: [.accessibilitySpeechLanguage:
“fr-FR”])

let label = NSMutableAttributedString(string: "Yosemite National Park")
let range = label.string.range(of: "Yosemite")!
label.addAttributes([.accessibilitySpeechIPANotation: "joʊˈsɛmɪti"], range:NSRange(range, in:
label.string))
最新情報をチェックしよう!