こんにちは。Yahoo!乗換案内のiOS アプリの開発を担当している田中(@tattn)です。
iOS 13で目に優しいダークモードが使えるようになりましたね。
しかし、OSの設定を変えるだけで、アプリが自動的に黒くなるわけではありません。アプリ側がダークモードに対応する必要があります。
Yahoo!乗換案内(以下、乗換案内)はiOS 13の公開日(日本時間)にダークモード対応版のアプリを公開しました!
乗換案内はユーザーが必要としそうなiOSの新機能を積極的に取り入れています。
今年、自分はAppleの年に一度の開発者イベントであるWWDCに参加しました。そして、現地でキャッチアップした新機能の中で、今回は主にダークモードをピックアップして対応することに決めました。
ダークモード対応を決めた理由としては、iOS 13の機能の中でもAppleがかなり力を入れていた機能であったのが大きいです。App Storeには特定のアプリが特集されるページがいくつかあります。OSの新機能や優れた機能を実装すると特集され、よりユーザーの目に留まりやすくなります。 また、普段使いするアプリとしてより愛着を持ってもらうために好みのテーマに変更できるようにしたいという意図もあります。
ダークモード対応する中でいくつもの課題を乗り越える必要がありましたので、この記事ではその過程を共有するとともに、対応時の注意点や効率よくダークモードに対応するための方法について、開発面にフォーカスして書いてみようと思います。
最後におまけとして、ダークモード以外でiOS 13対応で実施した内容も一部紹介します。
※ 記事中に登場するコードや定義名などは 乗換案内で実際に利用されているものではなくそれに近い実装です。
調査 / UIコンポーネントの色決定
iOS 13の公開日に合わせてアプリを公開するためには、まずはダークモード対応にどのくらい時間がかかるのかを調べる必要があります。
そこでまずエンジニアはどのような作業が必要でどのくらいの工数がかかるのかの調査を始めました。
並行してデザイナーはアプリ内に存在している画面とUIコンポーネントを洗い出し、それらのライトモード時の色とダークモード時の色をドキュメント化する作業を進めました。
※ ダークモード時の色の決定には多くの調査や検証が必要ですが、そのデザイン的な観点に関しては明日公開予定のYahoo!ニュースアプリの例で詳しく紹介予定です。
調査 / 洗い出しの完了後に、かかる時間の見積もりをしましたが、判断が非常に難しかったです。
実際に手を動かしてみないとわからない部分もあったため、いくつかの画面をダークモード対応してみて、それらの平均作業時間を元に画面数や色の微調整に必要な時間を算出、それに基づいて本格的な作業を開始しました。
色の設定
色のドキュメント化後、それらの色をXcodeのAsset Catalogで定義しました。
Asset Catalogで色を扱うためには、アプリのサポートOSバージョンをiOS 11以上にする必要があるため、乗換案内ではダークモード / iOS 13対応に伴い、iOS 10のサポートを終了しました。
乗換案内は1画面1Storyboardの構成になっています。そのため、画面ごとに作業を分担することにしました。今のチームの面白いところはデザイナーもXcodeをバリバリ触っているところです。なので、ダークモードの色に変更する対応もエンジニアと一緒にデザイナーが直接作業しています。そのほうがUX的な観点の問題に気付きやすく、スピーディに修正できます。
とにかくひたすら地道に色を設定していくことになるのですが、効率よく対応するためにいくつかやってみたことがありますので共有します。
enumによる色定義
Asset Catalogで作成した色をコードで参照する時は、以下のように文字列で参照するインターフェースを経由する必要があります。
UIColor(named: "MyColor")
しかし、文字列ではアプリを動かしてみないとタイピングミスに気付くことができません。
そのため、乗換案内では以下のような拡張とenumを作成して利用しています。
extension UIColor {
enum Name: String {
case background = "Background"
case currentTab = "CurrentTab"
}
convenience init(named: Name) {
// 色が取得できていないことがすぐに分かるように強制アンラップする
self.init(named: named.rawValue)!
}
}
このような実装があると以下のように簡単に扱え、タイピングミスが各所に埋め込まれることもなくなります。(補完も効きます)
UIColor(named: .currentTab)
また、ひたすら case
を列挙していくのは大変なため、以下のようなスクリプトを作成して自動生成させたり、SwiftGenやR.swiftなどを利用してコード生成するようにすると便利です。
乗換案内ではライブラリを使用せず、柔軟に対応するために自前でやっています。
Rubyでenumを自動生成する例
path = '<色のAsset Catalogへのパス>.xcassets'
Color = Struct.new(:case, :name)
colors = Dir.glob(File.join(path, "*.colorset")).map do |color_dir|
# 色の命名規則に合わせて case 名を調整する
# この例では先頭の文字を小文字にしたものを case 名にしている
color_name = File.basename(color_dir).gsub('.colorset', '')
case_name = color_name.sub(color_name[0], color_name[0].downcase)
Color.new(case_name, color_name)
end
puts <<"EOS"
extension UIColor {
enum Name: String {
#{colors.map{|color| " case #{color.case} = \"#{color.name}\""}.join("\n")}
}
convenience init(named: Name) {
self.init(named: named.rawValue)!
}
}
EOS
正規表現による一括置換
Xcode 11から linkColor
などのSystem Colorsが使えるようになりました。
しかし、画面数が多い既存のアプリで一つ一つ設定していくのはかなり大変です。そこで、正規表現を使って一括で置き換えていくアプローチを試してみました。
StoryboardやXibをXMLとして表示すると、以下のような色の要素が見つかります。(こちらは青色のリンクの色です)
<color key="titleColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
こちらをXcode 11で linkColor
に設定すると以下のように変更されます。
<color key="titleColor" systemColor="linkColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
違いを見ると systemColor
という属性が追加されているだけのようです。(※ 場合により色の数値も微妙に違うようです)
そこで以下のようなコマンドでプロジェクト内の色を一括で置き換えました。
grep -rlnE '<color key="titleColor" red="0.0" green="0.478.*" blue="1" alpha="1".*/>' ./ --exclude-dir={.git,fastlane,Carthage,Pods,Frameworks} --include={*.storyboard,*.xib} | xargs perl -i -pe 's/key="titleColor" red="0.0*" green="0.478.*" blue="1" alpha="1".*\/>/key="titleColor" systemColor="linkColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"\/>/g'
これ以外にもいくつかパターンがあったり、変更しないほうが良い色を変えてしまうケースもあったりしたため、差分をしっかり確認しながら活用しました。 これによって、明らかに色設定漏れが減りました。
※ Xcodeのバージョンやプロジェクトによって細かい調整が必要な部分のため、参考にする場合はご注意ください。乗換案内ではXcode 11 Betaの時に上記のようなスクリプトを使用しました。
対応中に発生した問題
対応中にいくつもの問題が発生しました。いくつかピックアップして共有します。
iOS 12以下でカラーアセットの色が取れなかった
現在は解消されましたが、Xcode 11のBetaやGM 1ではカラーアセットで設定した色が取得できず、UIColor(named:_)
が常に nil
を返す問題が発生していました。
iOS 13の公開直前までその状態だったため、ダークモード対応を諦めようかとも思いましたが、ギリギリで修正され、なんとか公開できてとても安心しました。
macOSの外観モードが影響してしまう
こちらもXcode 11.2で解消されましたが、Xcodeを動かしているmacOS側をダークモードにしている状態でStoryboard / Xibを編集するとiOS 12以下で一部ダークモードの色が表示される問題が起きていました。
こちらはmacOSをライトモードにした状態でStoryboard / Xibを再編集すると問題が改善されたため、その問題を認識してからひたすらに今まで設定した色を再設定する作業をしました。
リリース前に気が付くことができて本当に良かったです...
こちらの問題は別途、Qiitaの記事 に詳細を記載していますので、気になる方はチェックしてみてください。
iOS 12以下でAsset Catalogの色がViewに反映されない時がある
こちらは執筆時点でも起きているのですが、Asset Catalogで作成した色をコードでViewに設定すると、色が変わらない時があります。
以下のパターンなどで色が反映されないケースがあることを認識しています。
viewDidLoad
のタイミングviewWillAppear
のタイミング- 上記のタイミングで通信をし、通信後に表示を更新したタイミング
必ず発生するわけではないのですが、iOS 12以下で動作確認をした時に、「あれ?」となることが多々あります...
対処方法としては、DispatchQueue.main.async
を使って、色の設定タイミングを少し遅延させるとiOS 12以下でも正しく表示されるようになったため、今のところはそれで対応しています。
対応してみて
ここまで読んでいただいた方はおわかりかと思いますが、ダークモード対応はなかなか大変でした。設定漏れがないか、既存と色が変わってしまっていないかなどの確認が大変で、特にiOS 12以下もサポートする場合は、適宜iOS 12以下の端末での色の確認が必要です。
しかし、ダークモード対応後、ユーザーからは多くのポジティブなフィードバックが届いているため、対応してよかったと思っています。また、ログを見ると乗換案内のダークモードユーザーが日々増えており、iOS 13やサードパーティアプリのダークモード対応の浸透が進むにつれて、ユーザーがどのアプリを使うか選択する際の一つの決め手になっていく機能だと思っています。
ただ、色は人によって見え方が異なるため、ダークモードの色が見にくいと思うユーザーもいます。その場合は、アプリ内に端末の設定とは関係なくライトモードorダークモードに切り替えられるスイッチを用意しておくとアクセシビリティが上がりそうです。(UIWindow
の overrideUserInterfaceStyle
を変更することでその切替を実装できます)
また、Apple には不具合を報告できるBug Reportingというページがあるのですが、今回の開発中に多くのフィードバックを送り、その中のいくつかはすでに修正されました。
エンジニアとしては新しい機能に飛び込んでいって、フィードバック→改善のサイクルに参加できるのはなかなか面白かったです。
その後の運用について
ダークモードに対応するとその後の画面もダークモードに対応していく必要があるため、デザイン / 実装 / テストの面で少しずつ工数がかさ増しされます。
その点に関しては対応前にそれぞれのメリット・デメリットをしっかり考えて決めたほうが良いです。それはチームの規模や体制、アプリの種類、競合アプリの状態などさまざまな要因で変わってくるため、一概にどちらが良いとは言えない部分かと思っています。
乗換案内に関しては、総合的に見て今のところ対応してよかったと感じています。
おまけ(その他の iOS 13 対応)
ダークモード以外にiOS 13で対応したことをいくつか紹介します。
pageSheet
モーダルへの移行
iOS 13からモーダルのデフォルト表示が pageSheet
というスタイルに変わりました。このモーダルは下にスワイプすると閉じることができます。 こちらは fullScreen
にモーダルのスタイルを変更することで iOS 12以下と同じ表示方法にできるのですが、乗換案内では pageSheet
を採用することにしました。
理由としては、乗換案内はユーザーの学習コストを減らすために標準のUIを採用する方針であり、そして今回のモーダルが以前から期待していたUXであったためです。
また、すでにiOS 12以下でもスワイプでモーダルを閉じることができる機能を実装していたのも一つの要因です。
注意点としては pageSheet
を採用すると、表示時や閉じた時に親のViewController側で viewWillAppear
や viewWillDisappear
などが呼ばれなくなります。
この部分で必要な処理を実行している場合は UIAdaptivePresentationControllerDelegate
や beginAppearanceTransition
/ endAppearanceTransition
を利用する必要がありますので気をつけましょう。
起動時のクラッシュ対応
アプリをApp Store Connectにアップロードし、Test Flightで確認をすると起動時にアプリがクラッシュしてしまう問題が発生しました。
クラッシュログを見ると UISearchDisplayControllerNoLongerSupported
というログが見つかりました。UISearchDisplayController
はiOS 8で非推奨になったため、利用していなかったつもりだったのですが、Storyboard上に使われていない UISearchDisplayController
が残っていました...
こちらを削除したところ問題なく起動するようになりました。 DeprecatedなAPIはコードだけでなくStoryboardやXibなどでも参照されていないかしっかり確認が必要です。
UIKitの機能をメインスレッドから呼び出されないように修正
もともとUIKitの機能はメインスレッドから呼ぶ必要がありました。
iOS 13ではそのチェックがより厳しくなり、今までは動いていた部分もクラッシュすることが増えました。
特に結果をクロージャで返すAPIのクロージャ内でUIKitのAPIを触っていると、iOS 13でクラッシュするようになることが多いです。(INVoiceShortcutCenter.shared.getAllVoiceShortcuts
など)
APIドキュメントをしっかりと確認し、必要に応じてDispatchQueue.main.async
などを正しく呼ぶように数カ所を修正しました。
まとめ
乗換案内のダークモード / iOS 13対応はなかなか大変でしたが、多くの良い反響があり、とてもやりがいがありました。
ダークモード対応すると、新規画面でも対応していく必要があるため、継続的なサポートが必要です。対応する場合は、チームと相談をして慎重に対応の是非を判断しましょう。
お知らせ
この記事の内容をもっと詳しく聞きたい方や直接質問したい方に朗報です。
Bonfire iOS #7というイベントを2020年1月7日(火)にヤフーのLODGEで開催します。 今回はヤフーを代表するiOSエンジニアが2019年に取り組んだ新サービスの裏側やダークモード対応の話などをする予定です。
https://yj-meetup.connpass.com/event/157369
BonfireはiOS以外にもAndroidやウェブ、デザインなどさまざまな領域をテーマに開催しています。
connpassの「Yahoo! JAPAN」グループのメンバーに入ると開催前にメールや通知が届いて便利です。
興味がありましたら、ぜひ参加してみてください!
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました