Session概要
テキストのスタイルとレイアウトは、優れたリーディング体験をもたらす大きな要素です。CoreTextやTextKitといったテクノロジーは、優れたテキストレイアウトを作成するために必要なツールとなります。このセッションでは、アクセシビリティに対応したリーディングコンテンツプロトコルを導入し、自動ページめくり機能を追加し、音声出力をカスタマイズすることで、VoiceOver向けにも同様の優れたアクセス体験を作り出す方法について https://developer.apple.com/videos/play/wwdc2019/248/
VoiceOverについて
- テキストなどの読み上げ機能
- ショートカット設定
- 設定 → アクセシビリティ → ショートカット → VoiceOver で可能
- 画面内に読み上げ可能なコンテンツが見つからないとアラート音がなる
- そのため、テキストコンテンツを読み上げ可能にする必要がある
- UIAccessibilityReadingContentの使用する
- 指でテキストコンテンツをタップすると、音声が流れテキストがハイライトされる
読み上げプロトコル
- 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のインスタンスで表現する
- まずページビューをアクセス可能な要素にするため、 isAccessibilityElementをtrueに設定する
- 次に読み上げプロトコルを組み込む
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の組み込みが必要
- Page ViewにcausePageTurnを含める
- 方向を決めるaccessibilityScrollを含める
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
}
}
読み上げ音声のカスタマイズ
- VoiceOverの話し方を変えられる機能
- 読み上げプロトコル内で代替メソッドを2つ使用する
- アクセシビリティ属性を付与し、読み上げ方の様々な特徴を設定します
- 例えば、他言語の文章を希望する場合は言語の指定子と共にaccessibilitySpeechLanguage属性を含めるとVoiceOverが最適な音声を選ぶ
- accessibilitySpeechIPANotationによるIPA(International Phonetic Alphabet)表記の指定
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))