「アクセシビリティやるぞ!夏祭り2」でテストしたアプリの制作ノート

公開

2016年9月28日(水)にヤフー株式会社さんのセミナールームで開催された「アクセシビリティやるぞ!夏祭り2 ~俺たちにテストさせろスペシャル~」のセッション2、「セッション2 俺たちにテストさせろ!スマホアプリのユーザーテスト生実況!」に登壇させていただきました。ご参加いただきましたみなさま、関係者のみなさま、ありがとうございました。

セッションで私が提供したブログリーダーアプリ(Junnama Online)は私がiOSアプリ開発を学ぶ過程で作り始めたもので、「これはシンプルなアプリだな」と思われた方も多いのではないかと思います。おそらく参考書(私は「本気ではじめるiPhoneアプリ作り Xcode 7.x+Swift 2.x対応 黒帯エンジニアがしっかり教える基本テクニック」を読みました)の内容を応用して作っていけば、初心者でもなんとか完成させることができる簡単なアプリです。また、HTMLでWebページを作る場合と似ていて、標準のコンポーネントを適切に利用すればそれだけでもかなりアクセシブルな状態になる印象もありました。ただ、それだけでは少し不足だなと考えさせられる点も多かったように思います。
Junnama Onlineアプリの画面例

そこで今日は、アプリ制作中にアクセシビリティについてどのような事を考えたか、またどのような実装をしたか、技術的な側面をご紹介したいと思います。

お気に入りに登録ボタン・未読にするボタンの工夫

記事詳細ページに配置したお気に入りに登録ボタン・未読にするボタンについてです。

画像のラベル

ボタンに画像を設定した際、画像だけ指定すると画像のファイル名が読み上げられてしまいますので、ラベルを付けることが必須です。Interface Builderで画像とTitleを一度に指定すると警告が表示されたので、readStatusImage.accessibilityLabel = "未読にする"のように、accessibilityLabelでラベルの設定を行いました。

Xcode付属のAccessibility Inspectorで確認すると、ラベルの内容が反映されていることが分かります。(ラベルを付けていないと画像ファイル名になります。) Accessibility Inspectorでボタンがどのように読み上げられるか確認している画面

選択状態の表現

選択状態をビジュアルだけでなくプログラム的に表現できるか?ということを考えました。

今回はUIButtonで実装していたので、addClipButton.selected = trueのようなコードでステートを変更するだけで画像は選択状態の画像に変わり、VoiceOverの読み上げも「選択中の」と読み上げられるようになりました。

独自UIコンポーネントの改良

画面上部のカテゴリが並んでいる部分は、iOSライブラリ管理ツール「CocoaPods」で公開されている「PageMenu」を利用しました。デザインに手を加えると「グノシー」や「SmartNews」のタブのようになります。
カテゴリを並べたUI

UIScrollViewを利用した実装になっているので完全に独自のUIコンポーネントではないですが、標準のUIでもないのでVoiceOverで読み上げさせると各アイテムのラベルしか読み上げられず、どのようなUIなのか?、今何が選択されているのか?、が伝わらないのではないかと考えました。

そこで、まずaccessibilityLabelを使用してタブであること、アイテムの総数、現在フォーカスしているアイテムが何項目目か、を示すことにしました。ちなみに、文中にカンマを入れるとVoiceOverが読み上げられる際、一呼吸置くようになります。

menuItemView.titleLabel!.accessibilityLabel = "\(controller.title!), タブ, 全\(controllerArray.count)項目中の\(Int(index) + 1)項目目"

次に、選択状態にあるタブは「accessibilityTraits」を使用して選択状態にあることを示しました。(ボタンではないのでselected = trueは使えません。)

menuItems[currentPageIndex].titleLabel!.accessibilityTraits = UIAccessibilityTraitSelected

以上の設定により、例えばカテゴリ「アクセシビリティ」が選択状態にある場合は「選択中の, アクセシビリティ, タブ, 全11項目中の3項目目」のように読み上げられるようになります。

このUIは操作が難しいかもしれないという懸念があったのですが、VoiceOverがオンになっている時は左右のフリックでフォーカスが前後に移動するようで(これは初めて知りました)、中根さんは特に戸惑うことなく操作されていました。

UITableViewCellの工夫

記事一覧を表示しているUITableViewCellについてです。

AccessibilityTraitsの設定

UITableViewCellは、通常セルの内容のみが読み上げられます。ここで例えばAccessoryの設定を「Disclosure Indicator」にするとビジュアル的には「>」のようなアイコンが付き、VoiceOverも「ボタン」と読み上げるようになります。ただ、Disclosure Indicatorについて調べると、「Table View Programming Guide for iOS」に次のような説明がありました。

You use the disclosure indicator when selecting a cell results in the display of another table view reflecting the next level in the data model hierarchy.

他のアプリを見ると、Accessoryの設定を「Disclosure Indicator」にしていると思われるものと、何も設定していないもの、どちらも見られました。今回はガイドに従ってAccessoryの設定をNoneにしました。ただこれだと何らかの操作ができるセルなのか否かが伝わらないので、accessibilityTraitsをbuttonに設定しました。 Interface BuilderでTraitsを設定した画面

VoiceOverでは操作可能なセルであることが分かるようになりましたが、やはりビジュアル的にも操作可能なセルであることが何かしらの方法で分かる方が良いかと考えています。

accessibilityLabelの設定

以前書いた記事「自作iOSアプリをVoiceOverで操作してみた」でも紹介したのですが、セルが自分の考えたとおりの順序で読み上げられないことがありました。また、今回は純正の「メール」アプリや「Reeder」アプリに倣って○の画像で未読状態を表現していたため、それをビジュアル以外でも伝わるようにする必要がありました。

以前にも紹介しましたが、Appleのサイトにある「iOSアクセシビリティ プログラミングガイド(PDF)」の「Table Viewのアクセシビリティの改善」の項に次のようにあります。

Table Viewの行ごとに複数の情報を表示する場合、情報を1つの理解しやすいラベルに集約することでVoiceOverユーザの体験を改善することができます。

テーブルに、それぞれ大量の情報を提供するセルが含まれている場合は、これらの中からの情報を組み合わせてLabel属性にすることを検討するべきです。こうすれば、VoiceOverユーザはセルの内容の意味を1つのジェスチャで得ることができ、個々の情報にそれぞれアクセスする必要がなくなります。

そこで、今回は「未読(未読の場合のみ), 記事タイトル, 公開日時」をaccessibilityLabelで設定しました。(余計なことかもしれませんが、時間が12:00のような表示形式だったので、聞いて分かりやすいように少し編集しました。)

var a11yLabel = ""
if didRead {
    a11yLabel = ""
} else {
    a11yLabel = "未読, "
}
a11yLabel += item.title + ", "
a11yLabel += dateText.stringByReplacingOccurrencesOfString("\\:(.*)", withString: "時$1分", options: NSStringCompareOptions.RegularExpressionSearch, range: nil)

cell.accessibilityLabel = a11yLabel

モーダル表示(UIPresentationController)の実装

旅行キュレーションメディア「RETRIP」のアプリをはじめ、いろいろなアプリ、またiOSの設定画面などで見られる実装ですが、コンテンツの上を半透明の黒で覆い、モーダルでコンテンツを表示する実装方法があります。

このカスタムモーダルはUIPresentationControllerで実装ができます。しかし、VoiceOverを有効にして画面を操作してみると、コンテンツを覆う半透明の黒のレイヤーを入れた場合になぜか隠しているつもりの後ろのコンテンツ(PresentingView)のテキストやボタンにフォーカスが当たってしまいます。(左右のフリックではなく手当たり次第に画面を触った場合。)App Storeで公開されている他のアプリでも同じ現象が見られました。
モーダルの外のコンテンツにフォーカスが当たっている様子

デベロッパープログラムのTechnical Support Incidentを利用してAppleに解決方法を尋ねたところ、accessibilityViewIsModalプロパティを操作すればよいことが分かりました。早速記述をしたところモーダルの外のコンテンツにはフォーカスしなくなりました。

ただし、accessibilityViewIsModalが効くのはiOS 10からだそうです。iOS 9以下では、accessibilityElementsHiddenプロパティを操作すればよいようです。

class CustomPresentationController: UIPresentationController {
    lazy var dimmingView :UIView = {
        let view = UIView(frame: self.containerView!.bounds)
        view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.5)
        view.alpha = 0.0
        return view
    }()

    override func presentationTransitionWillBegin() {
        guard
            let containerView = containerView,
            let presentedView = presentedView(),
            let presentingView = presentingViewController.view
        else {
            return
        }

        // Add the dimming view and the presented view to the heirarchy
        dimmingView.frame = containerView.bounds
        containerView.addSubview(dimmingView)
        containerView.addSubview(presentedView)

        // Fade in the dimming view alongside the transition
        if let transitionCoordinator = self.presentingViewController.transitionCoordinator() {
            transitionCoordinator.animateAlongsideTransition({(context: UIViewControllerTransitionCoordinatorContext!) -> Void in
                self.dimmingView.alpha = 1.0
            }, completion: { context in
                containerView.accessibilityViewIsModal = true
                presentingView.accessibilityElementsHidden = true    // iOS 9 workaround
                UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil)
            })
        }
    }

    override func dismissalTransitionWillBegin()  {
        guard
            let presentingView = presentingViewController.view
        else {
            return
        }
        
        // Fade out the dimming view alongside the transition
        if let transitionCoordinator = self.presentingViewController.transitionCoordinator() {
            transitionCoordinator.animateAlongsideTransition({(context: UIViewControllerTransitionCoordinatorContext!) -> Void in
                self.dimmingView.alpha  = 0.0
            }, completion: { context in
                presentingView.accessibilityElementsHidden = false    // iOS 9 workaround
                UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil)
            })
        }
    }

Dynamic Type機能のサポート

iOSの設定画面で文字サイズを変更することができます。ここで設定した情報が反映されるよう、Dynamic Type機能をサポートしました。ユーザーの方は自分の好みのテキストサイズで記事を読むことができます。
テキストサイズを変更した後の画面

先に紹介した「PageMenu」で実装したカテゴリ一覧の部分の文字サイズが変わらないのが目下の課題です。

おわりに

今までにWebサイトのアクセシビリティについて学んでいたので、今回のアプリでもその知識を活かして「ここは何か追加でコードを書かなければならないのでは...?」と直感的に思うことがありました。「iOSアクセシビリティ プログラミングガイド(PDF)」が用意されていたこともあり、iOSアプリのアクセシビリティ向上も私自身は取り組みやすかったと感じています。

動きがあるアプリ、また例えばセルの上でフリックするとさまざまな操作ができるような実装のアプリだとどのような実装が必要なのか気になるところですが、そのようなアプリを制作した際にはまたご紹介したいと思います。