見出し画像

noteのiOSアプリで実装したアクセシビリティの全て(文字起こし) #iosdc #a

今年もiOSDCに登壇することができました。iOSDCには始まった2016年から参加し、2017年から毎年登壇できています。今年で5年目(6回目)の登壇です。
今回は最近会社で力を入れ始めたアクセシビリティの詳しい実装についてお話しました。
提出したCfPはこちら。

動画も字幕付きでYouTubeにあげていますので、iOSDCで観れなかった方、もう一度観たい方はよければみてください。(チャンネル登録、高評価もしてもらえると嬉しいです。)

文字起こし

noteのiOSアプリで実装したアクセシビリティのすべてというタイトルで発表します。

自己紹介

最初に自己紹介です。
note株式会社でiOSアプリエンジニアをしている植岡和哉と申します。
TwitterGitHubQiitanoteなどはすべてfromkkというアカウントで活動してるのでよかったらフォローしてください。

noteのオフィス画像

最初にnote株式会社について説明させていただきます。
note株式会社は2011年12月8日に設立されたデジタルコンテンツの企画、制作、配信、デジタルメディア運営のためのシステム開発サービス提供を事業とする会社です。

cakes

2012年9月にスタートしたcakesは多数のクリエイターや出版社と提携しているコンテンツ配信サイトです。読者は1週間150円で3万本以上の作品が読み放題で、クリエイターや出版社へ閲覧数に応じた収益が分配されます。

note

社名にもなっているnoteはメディアプラットフォームです。
クリエイターが文章やマンガ、写真、音声を投稿することができ、ユーザーはそのコンテンツを楽しんで応援できるWebサービスです。

ミッション 誰もが創作をはじめ、続けられるようにする。

私たちは誰もが創作を始め続けられるようにするを実現するためにクリエイターが創作を続けるための仕掛けをたくさん作っています。

アクセシビリティについて

まずアクセシビリティについてお話します。
ウェブアクセシビリティ基盤委員会によると

一般にアクセシビリティとはアクセスのしやすさ

を意味します。

転じて製品やサービスの利用のしやすさという意味でも使われるということです。
アクセシビリティは障害者や高齢者向けの対応と思われがちですが、全人類が情報に触れたり機能を体験できることを目指します。
特に画面を要するウェブサイトやスマートフォンアプリケーションにおいては、視覚障害を持つ人や視力が低下した人に不便を感じさせてしまうことが多いため、視覚的なサポートを行うことが中心となります。
例えば画面の読み上げ機能や画面やフォントの拡大機能をサポートするなどですね。
この発表でもAppleプラットフォームのアクセシビリティに関する機能であるVoiceOverを中心に紹介していきます。

ウェブアクセシビリティの4つの原則

ウェブアクセシビリティには知覚可能、操作可能、理解可能、堅牢性の4つの原則があります。

ウェブアクセシビリティの4つの原則

知覚というのは目が見えないなら、耳や触覚で、耳が聞こえないなら目で、情報の存在がわかる必要があるということです。
堅牢性というのはアクセスする媒体が変わっても、同様に情報にアクセスできるべきであるということです。
主にWebのアクセシビリティにおいて重要だと言われていますが、OSが変わっても同様の情報にアクセスできるべきであるというのはアプリでも意識したいですね。
詳細が気になる方はぜひ調べてみてください。

ユーザビリティ

またユーザビリティとの関係にも触れておきますユーザビリティは使いやすさというコンテキストでよく利用される言葉ですが、前提としてアクセシビリティが保たれた上で意識したいですね。

ユーザービリティとの関係

これはそもそもアクセスできなければ利用すらできないためです。
ユーザビリティーを向上させる前にどれだけのユーザーがその機能までたどり着けるのかを意識してみてください。

インクルーシブデザイン

また、近ごろはインクルーシブデザインという言葉も見聞きするようになりました。

インクルーシブデザイン

これはアクセシビリティやユーザビリティに加えて、住んでいる場所や年代性別などによる、違いや話したり聞いたり書いたり読んだりといった異なる言語など、これまでは排除されてしまっていた人々を巻き込みながら包括的にデザインする手法を言います。
今年のWWDCで素晴らしい発表がありましたのでまだ見てない方はぜひ見てみてください。

なぜnoteでアクセシビリティの向上に取り組み始めたのか

ようやく本題です。
なぜnoteでアクセシビリティの向上に取り組み始めたのか。
取り組み始めたきっかけとしては、もともとアクセシビリティの向上に興味のあった社員からアクセシビリティを向上したいという声が上がり始めました。
そこで社員の知人でアクセシビリティに詳しいという人がいるということで紹介してもらい、業務委託という形でジョインしてもらいました。
それとnoteの7周年記念イベントでアクセシビリティにも力を入れる旨の発表がありました。
更に、盲目の方からの問い合わせがあり、noteを利用しているが一部で利用できない機能があるということで、ユーザーインタビューを実施して見せてもらいました。
個人的に、このユーザーインタビューの録画を見たときに衝撃を受けました。
ユーザーインタビューはZoomで実施し普段どのようにnoteを利用しているのか、noteで困っていることがないかをフランクに話してもらいました。
noteで困っていることとして、iOSアプリで記事に見出し画像が設定できないという問題と、記事を公開する際にハッシュタグの設定がうまくいかないという問題を教えてもらいました。
これがどういうことなのか動画でお見せします。
こちらが記事に見出し画像が設定できない様子です。
https://youtu.be/rDX_tJ7t28M?t=269
次へボタンとタイトルにはフォーカスがあたるのですが間の見出し画像を設定する部分にはフォーカスが当たりません。
またこちらは記事を公開する際にハッシュタグの設定がうまくいかない様子です。
https://youtu.be/rDX_tJ7t28M?t=286
ハッシュタグを入力してウェブと同様にスペースを入力したりリターンキーを押してもハッシュタグが反映されませんでした。
これらの様子を見て思いました。

バグやん。

これまではアクセシビリティを向上させようと思っても、どう行動しているのか正直わかりませんでした。
しかし実際に動く様子を見ることでただの不具合だと捉えられるようになりました。
アクセシビリティ向上を掲げてもなかなかタスクは進みづらいと思います。
まずは1つ1つのバグ修正というタスクに分解すれば自ずと優先度が上がるのではないかと思いました。

VoiceOverの使い方

最初にVoiceOverの使い方を説明します。
いきなりVoiceOverを触ってみようと思っても最初は混乱してしまうためです。
https://youtu.be/rDX_tJ7t28M?t=343
設定アプリの中にアクセシビリティという項目があります。
そこにVoiceOverの項目がありますがこれは選択しないでください。
初めての方は戻し方がわからず混乱してしまうためです。
下の方にショートカットという項目があるのでこれを選択し、VoiceOverにチェックを入れます。
その後iPhoneのサイドボタンを3回連続でクリックします。
そうすることでVoiceOverが有効になります。
無効にするのも同じ動作をするだけでいいので、VoiceOverの無効の仕方がわからなくても混乱せずに済みます。

VoiceOverのジェスチャー

https://youtu.be/rDX_tJ7t28M?t=383
VoiceOverはスワイプジェスチャーを多用します。
3本指で左右にフリックするとページを遷移します。
一本指で左右にフリックすると前後の項目に遷移します。
画面を一本指でダブルタップすると項目を選択します。
ホーム画面に戻るにはiPhone X系の端末の場合は画面の下からスワイプアップして少し長めにとどまります。
さらに詳しい使い方を知りたい方はWWDC 2018の226番VoiceOver: App Testing Beyond The Visualsを見てみてください。

実際に発生した問題と解決方法

ここからnoteアプリで実際に発生した問題とその解決方法を説明します。
実際に発生した問題を3つ取り上げます。
1つ目はタップジェスチャーが設定されているviewが選択できないという問題です。
コードとしてはこのようなイメージです。

lazy var containerView: UIView = {
    let view = UIView(frame: view.bounds)
    view.isUserInteractionEnabled = true
    view.addGestureRecognizer(
        UITapGestureRecognizer(target: self, action: #selector(tap(sender:))))
    return view
}()

@objc private func tap(sender: UITapGestureRecognizer) {
    showTappedAlert()
}

ただのUIViewにaddGestureRecognizerにUITapGestureRecognizerを登録しているだけです。
VoiceOverを利用して実際に動かしている様子がこちらです。
https://youtu.be/rDX_tJ7t28M?t=461
真ん中のviewにフォーカスが当たらないことがわかります。
この問題の解決方法はシンプルでisAccessibilityElementtrueを設定するだけです。
ただのUIViewisAccessibilityElementがデフォルトでfalseになっているのでVoiceOverのフォーカスが当たりません。
こちらが先ほどの修正を加えたものです。
https://youtu.be/rDX_tJ7t28M?t=489
きちんとフォーカスが当たりタップイベントが発火していることが分かります。
いろんなアプリを見ていると同様の問題が発生しているものを見かけます。
ご自身で開発しているアプリでUITapGestureRecognizerを利用している場合は気にしてみてください。

続いて、画像のみのボタンで意図しない読み上げがされてしまう問題です。
コードのイメージとしてはこんな感じです。

lazy var button: UIButton = {
    let button = UIButton(type: .custom)
    let configuration = UIImage.SymbolConfiguration(
        font: UIFont.systemFont(ofSize: 280))
    let image = UIImage(systemName: "heart", withConfiguration: configuration)
    button.setImage(image, for: .normal)
    return button
}()

UIButtonに画像のみが設定されています。
これを実際に動かしてみた様子がこちらです。
https://youtu.be/rDX_tJ7t28M?t=531
スキ ボタン」と読み上げられるのを期待していたのですが「ラブ ボタン」と読み上げられてしまいました。
この問題の修正方法はaccessibilityLabelに読み上げてほしい文言を設定するだけです。

button.accessibilityLabel = "スキ"

こちらの修正内容を反映したものがこちらです。
https://youtu.be/rDX_tJ7t28M?t=548
正しく「スキ」と読み上げられていることがわかります。
こちらもいろんなアプリをVoiceOverを利用して見てみると同様の問題が発生しているアプリが多々ありました。
解決方法もとてもシンプルなのでご自身のアプリをVoiceOverで利用してどのように読み上げられるのかを気にしてみてください。

続いてハーフモーダルが閉じられない問題
を紹介します。
まずは現象を見てみましょう。ちなみにハーフモデルの表現にはFloatingPanelというライブラリを利用せていただいています。
https://youtu.be/rDX_tJ7t28M?t=580
1度表示したハーフモーダルを閉じるすべがなくなってしまったので、一旦VoiceOverをオフにしてモーダルを非表示にしてしまいました。
修正方法としては閉じるボタンを用意して、VoiceOverの設定に応じて「閉じるボタン」を表示するようにしました。

lazy var closeButton: UIButton = {
    let button = UIButton(type: .custom)
    button.setTitle("閉じる"for: .normal)
    button.addTarget(self, action: #selector(close(sender:)), for: .touchUpInside)
    return button
}()

@objc private func close(sender: Any) {
    dismiss(animated: true)
}

private var cancellable: AnyCancellable?
private func subscribeVoiceOver() {
    cancellable?.cancel()
    cancellable = NotificationCenter.default.publisher(
        forUIAccessibility.voiceOverStatusDidChangeNotification
    )
    .sink { [weak self_ in
        self?.handleVoiceOverStatus()
    }
}

private func handleVoiceOverStatus() {
    closeButton.isHidden = !UIAccessibility.isVoiceOverRunning
}

こちらの修正内容を反映したものがこちらです。
https://youtu.be/rDX_tJ7t28M?t=617
閉じるボタンが表示されてモーダルが閉じられるようになりました。
しかし、実はヒューマンインターフェースガイドラインにはモーダルには常に閉じるボタンを表示するべきという項目があり、この対応は正しくないのかもしれません。アンチパターンとして認識しておいていただくといいと思います。
ここまでは対応しないと詰んでしまったり、誤った情報をユーザーに知らせてしまうといったものでした。

更に体験を向上する施策

ここからは詰んでしまうわけではないがさらに体験を改善する施策についてお話しします。
ここでは六つ取り上げます。

最初にフォーカスが当たっている要素が何ができるのかを知らせる方法をご紹介します。まず、こちらをご覧ください。
https://youtu.be/rDX_tJ7t28M?t=666
テーブルビューのセルにフォーカスが当たって項目名が読み上げられましたが、実はセルをタップすると入力したタグが削除される仕様でした。
これをフォーカスが当たってる場合に読み上げられるようにして、ユーザーに知らせたいと思います。対応内容はこちらです。

cell.accessibilityHint = "ダブルタップで削除"

accessibilityHintにテキストを設定することで完了します。
設定したものがこちらです。
https://youtu.be/rDX_tJ7t28M?t=695
項目にフォーカスが当たり、読み上げの最後に先ほど設定したテキストが読み上げられることがわかります。
これによってユーザーに動作を迷わせることがなくなります。

続いて何か動作をしたら用意したテキストを読み上げる方法をご紹介します。
すでにこのようなコードがあったとします。

private func showErrorMessage(_ errorMessage: String) {
    errorLabel.text = errorMessage
}

これでアプリを動作させると次のような挙動になります。
https://youtu.be/rDX_tJ7t28M?t=729
入力できないテキストを入力しても音声ではわかりませんでした。
これを知らせるためにはUIAccessibilitypostメソッドに.announcementを設定してテキストを渡します。

private func showErrorMessage(_ errorMessage: String) {
    errorLabel.text = errorMessage
    UIAccessibility.post(notification: .announcement, argument: errorMessage)
}

これを設定したものがこちらです。
https://youtu.be/rDX_tJ7t28M?t=749
入力できないテキストを入力したことが理解できるようになりました。
アクションに対して理解してほしい機能がある場合はこういった施策もいいかもしれないですね。

続いて何か動作をした際にフォーカスされる場所を指定する方法をご紹介します。
どういうイメージかは次の動画を見てください。
https://youtu.be/rDX_tJ7t28M?t=773
次へボタンをタップすると次の要素にフォーカスが当たってほしいということがあります。これを実装していきたいと思います。
修正内容としてはUIAccessibilitypostメソッドに.layoutChangedを設定して遷移したいviewを渡します。この場合.layoutChangedは適切ではないかもしれませんが、他に適当なものがなかったので一旦こちらでしのいでいます。

@objc private func next(sender: Any) {
    UIAccessibility.post(notification: .layoutChanged, argument: messageLabel)
}

この内容を反映したものがこちらです。
https://youtu.be/rDX_tJ7t28M?t=804
次へボタンを選択した後でラベルにフォーカスが当たって読み上げられることが分かります。

続いて日付の読み上げを自然にする方法をご紹介します。
コードのイメージはこんな感じで日付を表示するラベルを用意します。

lazy var dateLabel: UILabel = {
    let label = UILabel()
    label.textColor = .label
    label.font = UIFont.preferredFont(forTextStyle: .headline)
    label.textAlignment = .center
    label.numberOfLines = 0
    label.lineBreakMode = .byWordWrapping
    return label
}()

private func showNow() {
    dateLabel.text = dateFormatter.string(from: now)
}

続いてdateFormatterをこのように作ってみましたdateFormatを明示的に指定するパターンです。

private lazy var dateFormatter: DateFormatter = {
    guard
        let dateFormat = DateFormatter.dateFormat(
            fromTemplate: "yyyy/MM/dd HH:mm", options: 0, locale: .current)
    else {
        fatalError("failed get date format from template")
    }
    var calendar = Calendar(identifier: .gregorian)
    let timeZone = TimeZone.current
    let locale = Locale.current
    calendar.locale = locale
    var dateFormatter = DateFormatter()
    dateFormatter.calendar = calendar
    dateFormatter.timeZone = timeZone
    dateFormatter.locale = locale
    dateFormatter.dateFormat = dateFormat
    return dateFormatter
}()

これでアプリを動かしてみた様子がこちらです。読み上げられる内容に注目してください。
https://youtu.be/rDX_tJ7t28M?t=837
日付けの区切り部分でスラッシュがそのまま読まれてしまってわかりづらくなっています。
この問題の修正内容はこちらです。

private func showNow() {
    dateLabel.text = dateFormatter.string(from: now)
    dateLabel.accessibilityLabel = accessibilityDateFormatter.string(from: now)
}

private lazy var accessibilityDateFormatter: DateFormatter = {
    var calendar = Calendar(identifier: .gregorian)
    let timeZone = TimeZone.current
    let locale = Locale.current
    calendar.locale = locale
    var dateFormatter = DateFormatter()
    dateFormatter.calendar = calendar
    dateFormatter.timeZone = timeZone
    dateFormatter.locale = locale
    dateFormatter.dateStyle = .long
    dateFormatter.timeStyle = .medium
    return dateFormatter
}()

dateFormatを指定せずにdateStyletimeStyleを指定したアクセシビリティ用のdateFormatterを別途用意して、accessibilityLabelに設定します。
こちらの内容を反映したものがこちらです。
https://youtu.be/rDX_tJ7t28M?t=866
日付部分がちゃんと聞きなじみのある読み方がされていることがわかります。

続いてリスト画面でスイッチのON/OFFの操作を自然にする方法をご紹介します。どういうことかはまずこちらの動画をご覧ください。
https://youtu.be/rDX_tJ7t28M?t=887
ラベルの読み上げとスイッチへのフォーカスがバラバラになってしまいちょっとした動作をしたいだけなのにスワイプ回数が多くなってしまっています。
こちらの修正内容がこちらです。

private func setUp() {
    isAccessibilityElement = true
}

override var accessibilityLabel: String? {
    get {
        titleLabel.text
    }
    set {}
}

override var accessibilityValue: String? {
    get {
        toggleSwitch.accessibilityValue
    }
    set {}
}

override var accessibilityTraits: UIAccessibilityTraits {
    get {
        toggleSwitch.accessibilityTraits
    }
    set {}
}

override func accessibilityActivate() -> Bool {
    toggleSwitch.isOn.toggle()
    return true
}

セルのアクセシビリティに関するプロパティを上書きするように修正しました。こちらの内容を反映したものがこちらです。
https://youtu.be/rDX_tJ7t28M?t=913
ラベルとスイッチの値の読み上げが自然になりました。ダブルタップでスイッチの値が変化することも伝えやすくなったと思います。

最後に見出しジャンプ機能への対応方法を紹介します。
見出しジャンプというのは見出しと本文があった場合に本文を飛ばして見出しだけに遷移したいという機能のことを言います。
見出しジャンプにはローターを使用します。
ローターは画面上を1本指で上や下にスワイプすることで利用することができる機能です。
画面上を2本指で回すように動かすことで設定の変更が可能です。
カスタム機能も作ることができるようです。

まずはこのようなコードを利用した画面を作成します。

private func makeHeadlineLabel(with title: String) -> UILabel {
    let label = UILabel()
    label.font = UIFont.preferredFont(forTextStyle: .headline)
    label.textColor = UIColor.label
    label.text = title
    label.numberOfLines = 0
    label.lineBreakMode = .byWordWrapping
    return label
}

動いている様子がこちらです
https://youtu.be/rDX_tJ7t28M?t=969
見出しが見つかりませんと言われてしまいました。こちらの対応内容を次に紹介します。

label.accessibilityTraits = [.header]

accessibilityTraits.headerを追加します。
これで動作させると次のようになります。
https://youtu.be/rDX_tJ7t28M?t=990
きちんと上下のスワイプで見出しだけにフォーカスが当たっていることがわかります。
accessbitilityTraitsにはheader以外にも様々な値があります。
適切なものを選択することでフォーカスが当たっている要素の特性を説明することができます。
特に.button.link.selected.notEnabledなどは個人的によく利用する気がします。

accessibilityTraitsの一覧

アクセシビリティの設定を確認するためのTips

アクセシビリティの設定を確認するための簡単なTipsを紹介します。
アクセシビリティ関連のプロパティがきちんと設定されているかを確認するためにはVoiceOverを利用する必要がありますが、読み上げに時間がかかってしまいます。
そこで音声コントロールを有効にしてオーバーレイの設定を項目名にすることで設定済みのラベルを可視化することができます。
VoiceOver実際に使わなくてもラベルの確認ができて便利です。

アクセシビリティーに関するより詳細の情報を調べるにはXcodeに付属のAccessibility Inspectorを利用します。

Accessibility Inspector

他にもOSSでアクセシビリティに関するテストツールがいくつか出ています。テストしたい用途に合わせて利用してみてください。

アクセシビリティに関するテスト

クリエイターの声

noteのアクセシビリティを改善したことでいくつか記事を書いていただきました。こういった声を挙げていただけることで本当に対応してよかったなと感じました。

今後の展望

今後の展望について簡単にお話します。
noteのウェブ・アプリのアクセシビリティの向上活動はまだまだ始まったばかりで不完全です。
アクセシビリティの向上は突き詰めようと思うとどこまでもできてしまうため、ゴールを目指して実施せず、それよりも定常の開発・品質管理の中に組み込んで開発を進めて行きたいと思っています。
また、定期的にユーザーインタビューを実施したりアンケートを取ったりして、利用者の声を拾い上げていく仕組みづくりをしていきたいです。
やはり実際に利用している人が困っているという点を改善するのが大事だと実感したためです。

まとめ

まとめです。
アクセシビリティを向上するぞと意気込まずにバグを修正するという意識に変わったことで取り組みが進みました。
NSObjectが持っているアクセシビリティ関連プロパティから少しずつ対応していきましょう。
また用途に合わせてUIAccessibilityAccessibility Frameworkの機能を利用して体験を向上させていけば良さそうということが分かりました。
アクセシブルなアプリが増えることで困っている人が少しでも減ればうれしいです。

情報収集
アクセシビリティに関する情報収集方法としてはまずはAppleの公式ページを見てみるのがいいと思います。

またアクセシビリティ関連の情報交換の場がSlackにあります。

#apple -appチャンネルでAppleプラットフォームのアクセシビリティに関する情報交換が行われています。
興味のある方はジョインしてみてください。

他にもOrangeという会社のガイドラインがとてもよくできているので参考になりました。

また、アクセシビリティのまとめサイトなんかもあったりします。

note 社の記事

note社で実施したアクセシビリティに関して発表された記事もいくつかありますので興味のある方は読んでみてください。

サンプルアプリ

今回紹介したアプリの挙動が確認できるコードをGitHubに掲載しています。

PR

最後に宣伝ですnote社では絶賛エンジニアを募集中です。
iOSとAndroidエンジニアモバイルアプリのPdMも特に募集しているので興味のある方がいましたらぜひご覧ください。

以上です。ご清聴ありがとうございました。

この記事が気に入ったらサポートをしてみませんか?