先日、自作のiOS用音楽再生アプリ「うさプレイヤ」をアップデートし、アプリの背景色を黒背景に変更する機能を実装しました。この機能を実装する方法がネット上を調べてもあまり見当たらなかったのと、実際にやろうとしたら結構大変だったので情報共有としてまとめておきます。
注意事項
- 手探りでやったのでもっと良い方法があるかもしれません
- ここで紹介する方法は「うさプレイヤ」での夜間モードの実装に必要だったものなので、他のアプリでは不要だったり、または逆に必要だったりする作業があるかもしれません
- 思い出しながら書いているので書き忘れていることもあるかもしれません
- 使用SDK(Deployment Target)は
iOS 9.3
です。ただし、iOS10とiOS11上での動作を確認しています
夜間モードという名称について
これはわりとどうでもいい話ですが、この「アプリの背景色を黒背景に変更する機能」はTwitterなどでは夜間モードと呼ばれていますが、昼間でも使うでしょという思いから「うさプレイヤ」ではダークモードという名称にしました。ちなみに、Twitterの夜間モードは英語ではNight Shift
となっており夜勤という意味のようです。
この記事での夜間モードの仕様
実装方法にも関連するので、この記事での夜間モードの仕様を明確にしておきます。
- 夜間モードにすると背景色が暗い色になり、テキストは明るい色になる
- 夜間モードはアプリ内の設定画面からON/OFFを切り替えられる
- 夜間モードのON/OFF設定は保存される
やるべきこと
夜間モードを実装するためには以下のような作業が必要になります。
- 夜間モードのON/OFF設定を保存する(UserDefaultsなど)
- 夜間モードのON/OFF切り替え用のUI(UISwitchなど)を用意する
- 黒背景の実現
- 白背景の実現
- その他の対応
「夜間モードのON/OFF設定を保存する(UserDefaultsなど)」と「夜間モードのON/OFF切り替え用のUI(UISwitchなど)を用意する」は特筆すべきことはないので省略します。
黒背景の実現
背景色の変更自体はUIViewのbackgroundColorプロパティを変えるだけです。また黒背景に黒テキストだと文字が読めなくなってしまうので、テキストの文字色も明るい色に変える必要があります。UILabelならtextColorプロパティを変更します。
問題は「いつ」「どうやって」変更するのかということです。
いつ背景色を変えるのか
まず最初に考えるのは生成時(viewDidLoad)に変えることです。しかし、「夜間モード」はアプリ内でON/OFFが変更できるので、生成後にも背景色を変える必要があります。なので、「うさプレイヤ」ではUIViewが表示される直前(viewWillAppear
)に背景色を変更するようにしました。「夜間モード」のON/OFF切り替えは別画面で行うので、「夜間モード」が切り替わっても画面切り替え時にviewWillAppear
経由で背景色が反映されます。
ただし、この方法は画面の切替時に一括で反映させる方法なので、画面表示後に生成されるUIViewに関しては反映されません。そのようなUIViewは生成時に夜間モードを反映する処理を行う必要があります。例えばUITableViewのセルなどはスクロールするごとに生成されるので、UITableViewControllerのcellForRowAtIndexPath
メソッド内で該当のセルに対して夜間モード反映処理を行うようにします。
どうやって背景色を変えるのか
さきほど説明したように、背景色あるいはテキスト色の変更自体は基本的にUIViewのプロパティを変えるだけでできます。問題はUIViewへのアクセスです。「うさプレイヤ」ではStoryboardを多用していたため、UIViewへのアクセスを得るにはアウトレットによる参照(ポインタ)を持つ必要があります。ですが、UIView1つごとにアウトレットを用意するのはとてつもない手間です。そこで、「うさプレイヤ」ではUIViewControllerのルートのUIViewから親子関係を走査して、画面内のすべてのUIViewをたどるという方法を取ることにしました。
UIViewの親子関係を走査する
UIViewの親子関係を走査するには、UIViewのsubviewsプロパティを利用します。「うさプレイヤ」では、各UIViewへの処理をblockで行えるメソッド(traverseView)を用意しました。
static BOOL traverseViewImpl( UIView* view, BOOL(^visitBlock)(UIView*) ) // visitBlock の戻り値は 子をスキップするかどうか
{
if ( visitBlock( view ) ){ return YES; }
for ( UIView* subview in view.subviews )
{
traverseViewImpl( subview, visitBlock );
}
return NO;
}
@implementation Util
+(void)traverseView:(UIView*)view visitBlock:(BOOL(^)(UIView* view))visitBlock
{
traverseViewImpl(view, visitBlock);
}
@end
これをUIViewControllerから以下のようにして使います。UIViewControllerのviewプロパティが画面のルートViewになるので、そこを起点にして走査します。
@implementation MyViewController
-(void)viewWillAppear:(BOOL)animated
{
[Util traverseView:self.view visitBlock:^(UIView* view){
// ここで UIView ごとの処理
}];
}
@end
UIViewごとの夜間モード対応の処理を呼び出す仕組みを用意する
ここまでで画面表示時に画面内のすべてのUIViewへの処理を行うことができるようになりました。ここから実際にUIVIewごとに背景色やテキスト色を変更して夜間モードに対応させていくことになります。
UIViewごとの処理、つまりUITableViewなら背景色を変更し、UILabelならテキスト色を変更するというような処理をする方法として、「うさプレイヤ」ではObjectiveCのカテゴリ機能(SwiftではExtension)とオーバーライドを使うことにしました。カテゴリ機能でUIViewにメソッドを追加し、さらにUIViewの派生クラスにもカテゴリで同名のメソッドを追加することで、オーバーライドします。
具体的には、まずUIViewにカテゴリでsetupNightMode
のようなメソッドを追加します。そしてUITableViewなど夜間モードへの対応が必要なクラスにもカテゴリでsetupNightMode
のメソッドを追加します(オーバーライドになる)。これによりUIViewの走査時に各UIViewのsetupNightMode
を呼び出すことで、そのUIViewがUITableViewだった場合にはそのクラスで定義されたsetupNightMode
が呼び出されるようになります。
コード例
// UIViewのsetupNightMode。オーバーライドされていなければこれが呼ばれる。
@interface UIView (NightMode)
- (BOOL)setupNightMode;
@end
@implementation UIView (NightMode)
- (BOOL)setupNightMode // 子のsetupNightMode呼び出しをスキップするかどうか
{
// デフォルト(オーバーライドされていない場合)の夜間モードの処理を記述する
return NO;
}
@end
// UITableViewのsetupNightMode
/* 派生クラスのメソッドの宣言(@interface)は不要。@implementationだけでよい。
@interface UITableView (NightMode)
- (BOOL)setupNightMode;
@end
*/
@implementation UITableView (NightMode)
- (BOOL)setupNightMode // 子のsetupNightMode呼び出しをスキップするかどうか
{
// UITableView用の夜間モードの処理を記述する
return NO;
}
@end
このようにメソッドを用意しておけば、以下のようにUIViewを走査してsetupNightMode
を呼び出すだけでUIViewごとの夜間モード対応処理が呼ばれます。
@implementation MyViewController
-(void)viewWillAppear:(BOOL)animated
{
[Util traverseView:self.view visitBlock:^(UIView* view){
return [view setupNightMode];
}];
}
@end
UIViewごとの具体的な夜間モード対応方法
「うさプレイヤ」で夜間モードを実装するためにsetupNightModeをオーバーライドしたクラス一覧です。
- UIView
- UITableView
- UITableViewCell
- UILabel
- UITextView
- UITextField
- 背景色を変更しないためのオーバーライド(デフォルトだとUIViewのsetupNightModeで背景色が変更されてしまうため)
- UITableViewHeaderFooterView
- UISegmentedControl
- UIButton
- UINavigationBar
- UITabBar
UIViewのsetupNightMode実装
- 背景色を変更(透明でない場合のみ)
UIViewのsetupNightMode
の実装はsetupNightMode
がオーバーライドされていない場合の実装、つまりデフォルトの実装です。なのでUIView派生クラスはsetupNightMode
をオーバーライドしない限り、自動で背景色が変更されることになります。背景色を変更したくないクラスはsetupNightMode
をオーバーライドする必要があります。例えばUIButtonなどは背景色を変更しないためにsetupNightMode
をオーバーライドしています。
UITableViewのsetupNightMode実装
- UIViewと同じように背景色を変更
- separatorColorプロパティを明るい色に変更
- グループスタイル(
UITableViewStyleGrouped
)の場合は背景色をより暗くする(セルと区別がつくように)
UITableViewCellのsetupNightMode実装
- 背景色を変更(グループスタイルの場合)
- 背景色を透明に変更(グループスタイルでない場合)
- 選択時の背景色を変更
背景色を透明にしているのは、背景色を黒にしてもセル挿入のアニメーション時に背景色が真っ白になってしまうというバグのような挙動があったためその回避策です。グループスタイルの場合は普通に背景色を設定していますが、グループスタイルの場合はセルとTableViewの背景色に違いを付けるため背景色を透明にするわけにはいかないという事情があるからです。なので、グループスタイルでセル挿入アニメーションを行うと一瞬セルの背景色が真っ白になってしまう現象が発生すると思いますが、幸い、「うさプレイヤ」ではグループスタイルでセル挿入アニメーションが発生する場面がないのでなんとかなっています。
UILabelのsetupNightMode実装
- テキスト色を変更(アルファ値は維持)
- 背景色を変更(透明でない場合)
テキスト色を明るい色に変更します。このときアルファ値は変えないようにします。これは、「うさプレイヤ」では場所によって少しテキスト色を薄くしている箇所があり、それを維持するためです。このテキスト色を薄くしている箇所は、夜間モード実装前はRGB値で色を薄く、つまり黒を少し明るくさせていたのですが、夜間モード実装によりRGB値を一括で変更する必要がでてきたため、アルファ値で色を薄くさせるように変更しました。
UITextViewのsetupNightMode実装
- テキスト色を変更
- 背景色を変更(透明でない場合)
UITextFieldのsetupNightMode実装
- テキスト色を変更
- プレースホルダーのテキスト色を変更
- 背景色を変更(透明でない場合)
プレースホルダーのテキスト色を変更するためには、placeholder
プロパティではなくattributedPlaceholder
プロパティを使う必要があります。
プレースホルダーのテキスト色の変更例
NSDictionary *attributes = @{ NSForegroundColorAttributeName: color };
self.attributedPlaceholder = [[NSAttributedString alloc]initWithString:self.placeholder attributes:attributes];
白背景の実現
ここまで、黒背景を実装する方法を見てきましたが、夜間モードを実装するには夜間モードがOFFの場合の対応も必要です。つまりsetupNightMode
では夜間モードがOFFの場合には白背景を設定するようにしなければいけません。夜間モードのON/OFFで設定する色を変えること自体は簡単だと思いますが、問題はOFFのときに何色を設定するのかということです。OFFのときには各種UIViewのデフォルトの色を設定する必要がありますが、これは地道に画面をキャプチャするなりデバッグ出力なりでデフォルトの色を調べていく必要があります。背景色自体は真っ白でいいと思いますが、UITableViewのセパレータの色や、UITableViewがグループスタイルの場合の背景色など黒背景の設定時に色を変更したものはデフォルトの色を調べる必要があります。
UIKitの各種デフォルト色が載っているサイトも見つけましたが、これが正しいのかはわかりません(情報が古いっぽいですし)。参考まで。http://iphonedevwiki.net/index.php/UIColor
その他の対応
他にも対応が必要な箇所がいくつかあります。
- キーボードの色
- UIDatePickerのテキスト色
- 夜間モード切替時に色をスムーズに変化させる
キーボードの色
キーボードの色はUIKeyboardAppearanceLight
、UIKeyboardAppearanceDark
があるので夜間モードではUIKeyboardAppearanceDark
を利用した方が統一感が出てよいでしょう。「うさプレイヤ」ではキーボードの色の変更は以下のようなコードで行いました。
textField.keyboardAppearance = [Util isNightMode] ? UIKeyboardAppearanceDark : UIKeyboardAppearanceLight;
UIDatePickerのテキスト色
UIDatePickerの背景色が暗い場合、テキスト色を明るくしないと見にくくなります。「うさプレイヤ」ではUIDatePickerはモーダル画面でしか使っていないので、setupNightModeではなく生成時にテキスト色を変えています。
UIDatePickerのテキスト色を変える方法は、以下のとおりにします。
[datePicker setValue:color forKey:@"textColor"];
夜間モード切り替え時に色をスムーズに変化させる
これは必須ではありませんが、「うさプレイヤ」では夜間モード切り替え時は以下のようにしてアニメーションさせています。
[UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^ {
[Util traverseView:self.view visitBlock:^(UIView* view){
return [view setupNightMode];
}];
} completion:^(BOOL finished) {
}];
まとめ
作業手順としては以下のような感じでしょうか。
- 夜間モードの設定を保存/読み取るようにする(UserDefaultsなど)
- 夜間モード切り替え用のUIを用意(UISwitchなど)
- UIViewを走査する仕組みを用意
- UIViewと任意の派生クラスにカテゴリで夜間モード反映用のメソッドをカテゴリ(Extension)で追加
- 画面(UIViewController)ごとの
viewWillAppear
内に「全UIViewを走査して夜間モードを反映させる処理」を追加 - 画面表示後に生成されるUIView(UITableViewのセルなど)に対して生成時に「夜間モードを反映させる処理」を追加
- キーボードを利用している箇所は夜間モード時はUIKeyboardAppearanceDarkを使うようにする
- 実際の画面を確認しつつ4,5,6,7を繰り返して対応漏れが無いように実装していく
- 夜間モードOFF(白背景)も正常に表示されるか確認する
以上が「うさプレイヤ」での夜間モードの実装方法です。実装した感想としては「とても面倒くさい」という感じでした。ですが夜間モードは実際に使ってみるとかなり満足度が高かったので1、やってみる価値はありだと思います。
-
個人的な感想です ↩