携帯端末のステータスバーってありますよね、
次回やネットの接続状況、電池の残量などを表示してくれていて、
結構このステータスバーを見るためだけに携帯を開くなんてこと、自分はよくあります。
下図は背景が白でステータスバーが黒、
背景が黒でステータスバーが白いです。
黒いStatusBar | 白いStatusBar |
---|---|
このステータスバーの色を反転するのにめちゃめちゃ苦労しました!
ので、記録を後世のために残したいと思います。
開発環境
- macOS High Sierra 10.13.6
- Xcode 10.1
- Swift 4.2
そもそもの背景
iOS9までは、StatusBarの色変更はそこまで難しいことではありませんでした。
UIApplication.shared.statusBarStyle
に UIStatusBarStyle
を指定するだけでした。
UIApplication.shared.statusBarStyle = .default //黒
UIApplication.shared.statusBarStyle = .lightContent //白
しかし!!!iOS9.0からDeprecated扱いになってしまいました。
警告内容:
Setter for 'statusBarStyle' was deprecated in iOS 9.0: Use -[UIViewController preferredStatusBarStyle]
代わりに preferredStatusBarStyle
を使えば良いとのこと。
使用例:
final class SampleBarViewController: UIViewController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
こんな感じで、対象のViewController内で preferredStatusBarStyle
を指定してあげればOKです。
しかし、これだけではStatusBarの色は変わりません!!!
具体的なケースとともに説明します。
大事な法則
ケースを見る前に、3つのポイントを抑えておくと理解しやすくなります。
表記について以降下記の表現を使います。
表記 | 内容 |
---|---|
NC | UINavigationController |
TBC | UITabBarController |
SVC | SampleViewController |
1. Info.plistの「View controller-based status bar appearance」をYES
対象ターゲットのInfo.plistにて、
「View controller-based status bar appearance」をYESに設定しておく必要があります。
日本語に言い換えるならば「StatusBarをViewControllerごとに設定しますか?」
が一番ニュアンスが近いと思います。
色々なサイトで「NOにせよ」「YESに設定して」とか言われていて困惑しがちなので、
なぜYESにするべきなのかをちゃんと記載します。
これは言わば、 preferredStatusBarStyle
についての話なのです。
「View controller-based status bar appearance」がNOなら
画面ごとの preferredStatusBarStyle
を見ません。
設定されていようがいまいが、StatusBarは黒(.default)です。
しかし、この状態でも UIApplication.shared.statusBarStyle
を
指定すればStatusBarの色は変わります。
つまり、従来のStatusBar指定方法なら、NOにするのが正しかったんですね。
「View controller-based status bar appearance」がYESなら
先ほどと逆で、画面ごとの preferredStatusBarStyle
を見ます。
そのためiOS9以降ではYESにするのが正しいと言えるでしょう。
もちろん、常にStatusBarを黒にしたい場合はNOにすれば良いということです。
2. preferredStatusBarStyleが呼ばれるController
- NCの前面の画面A-> NC自体の
preferredStatusBarStyle
- TBCの前面の画面B-> 画面B自体の
preferredStatusBarStyle
上記の2つを認識してください。
なんでこの仕様になっているかは分かりません、Appleの人に聞いてくださいw
SVCのStatusBarの色を変更したい場合の例:
-> NCの preferredStatusBarStyle
がコール
-> SVCの preferredStatusBarStyle
がコール
されます。
3. preferredStatusBarStyleが呼ばれるタイミング
タイミングは大きく分けて2種類です。
- ViewControllerの表示時
-
setNeedsStatusBarAppearanceUpdate
動作時
ViewControllerの表示時にも呼ばれるので、明示的にStatusBarの色を変えたい場合には
setNeedsStatusBarAppearanceUpdate
を使うようにすると良いです。
以上のことを踏まえ、ケースごとに説明します。
すべて、SVCのStatusBarの色を変更したい場合です。(一部PVC)
ケース1: SVCのみ
-> SVC preferredStatusBarStyle
がコール
こんな感じのコードで設定できるようにしておくと便利でしょう。
final class SampleViewController: UIViewController {
@IBAction func ButtonTapped(_ sender: Any) {
setStatusBarStyle(style: .lightContent)
}
private var statusBarStyle: UIStatusBarStyle = .default
func setStatusBarStyle(style: UIStatusBarStyle) {
statusBarStyle = style
self.setNeedsStatusBarAppearanceUpdate()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return statusBarStyle
}
}
ケース2: SVC on NC
navigationController.pushViewController
で遷移してきた画面
などが対象のケースです。
-> NCの preferredStatusBarStyle
がコール
ややこしいのですが、この場合2種類の指定方法があります。
ケース2-1: SVCで指定できるようにする
NCのStatusBarStyleを参照しようとしたら、NCの preferredStatusBarStyle
ではなく、
NCの前面、つまりNCのChildViewControllerの preferredStatusBarStyle
を参照するようにする、という設定ができます!
一部のNCだけの設定にしたい場合は、
class SampleNavigationViewController: UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return self.visibleViewController
}
}
とすれば良いですし、全NCで設定したい場合は、
extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return self.visibleViewController
}
}
という風にNCのextensionに設定すると便利でしょう。
ケース2-2: NCで指定
デフォルトではNCの preferredStatusBarStyle
が呼ばれます。
一部のNCだけの設定にしたい場合は、
class SampleNavigationViewController: UINavigationController {
open override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
とすれば良いですし、全NCで設定したい場合は、
extension UINavigationController {
open override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
という風にNCのextensionに設定すると便利でしょう。
ケース3: SVC on TBC
-> SVCの preferredStatusBarStyle
がコール
ケース1と同じ設定方法でOKな、シンプルなケースです。
ケース4: SVC on TBC on NC
だんだん複雑になっていきます。
navigationController.pushViewController
で遷移してきた
TabBarController
上の画面が対象のケースです。
ケース4-1: NCの preferredStatusBarStyle
がコール
一番奥にあるControllerはNCです。その場合は大事な法則2より、
デフォルトではNCの preferredStatusBarStyle
がコールされます。
ケース2-2と同じ設定方法でOKです。
ケース4-2: NCで childForStatusBarStyle
の指定をする
ケース2-1のように、
extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return self.visibleViewController
}
}
と指定していた場合、どうなるでしょうか。
大事な法則から紐解いていきます。
- デフォルトではNCの
preferredStatusBarStyle
がコールされるはず -
childForStatusBarStyle
の指定をしているので、対象が前面になる - NCの前面はTBCなので、TBCの
preferredStatusBarStyle
がコールされ - るのではなく、大事な法則より、TBCの前面のSVCが対象になる
-> SVCの preferredStatusBarStyle
がコール
となるのです。つまり、NCのchildForStatusBarStyle
を指定している場合は、
ケース1と同じ設定方法でOKとなります。
ケース5: SVC on NC on TBC
今度はTBCが一番奥の場合です。
-> ケース2と全く同じ。
一番奥がTBCなので、その前面のNCが対象になります。
その時点でケース2と全く同じ状態になります。
ケース6: PVC on SVC on TBC on NC
ここにきてまさかの新キャラPVC登場です。
表記 | 内容 |
---|---|
PVC | PopupViewController |
ケース4の状態から、 tabBarController?.present
で
PopupViewControllerが表示された状態です。
ケース6-1: NCの preferredStatusBarStyle
がコール
ケース4-1と同じで、デフォルトならNCが対象になります。
ケース6-2: NCで childForStatusBarStyle
の指定をする
ケース4-2のようにNCで childForStatusBarStyle
の指定するとどうなるか。
結果は4-2と同じです。
-> SVC preferredStatusBarStyle
がコール
つまり、PVCの preferredStatusBarStyle
がコールされることはありません!
ケース7: PVC on SVC on NC on TBC
もう頭おかしくなりそうですね...
ケース5の状態から、 tabBarController?.present
で
PopupViewControllerが表示された状態です。
-> ケース5同様、ケース2と全く同じ。
つまりつまり!
PVCの preferredStatusBarStyle
がコールされることはありません!
PVCでStatusBarの色を操るためには
ケース6-2とケース7においては、
PVCからStatusBarの色を変化できないということになります。
さすがにそれは不便ですよね。
その解決策を、今の僕の知識で思いついたパターンを記載します。
Protocol循環参照
SVC<->PVC間で循環参照できるようにするのです。
※NCの childForStatusBarStyle
を指定して、SVCのpreferredStatusBarStyle
がコールされる状態にすることが前提。
SVCはステータスバー黒、PVCのステータスバー白にする場合のコードが下記。
クラス参照用のProtocol例:
protocol SampleDelegate: class {
func setStatusBarStyle(style: UIStatusBarStyle)
}
SVC例:
final class SampleViewController: UIViewController {
private var statusBarStyle: UIStatusBarStyle = .default
@IBAction func buttonTapped(_ sender: Any) {
setStatusBarStyle(style: .lightContent)
let viewController = PopupViewController(delegate: self)
tabBarController?.present(viewController, animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return statusBarStyle
}
}
extension SampleViewController: SampleDelegate {
func setStatusBarStyle(style: UIStatusBarStyle) {
statusBarStyle = style
self.setNeedsStatusBarAppearanceUpdate()
}
}
PVCの例:
final class PopupViewController: UIViewController {
private weak var delegate: SampleDelegate?
@IBAction func closeButtonTapped(_ sender: Any) {
delegate?.setStatusBarStyle(style: .default)
dismiss(animated: true, completion: nil)
}
init(delegate: SampleDelegate) {
self.delegate = delegate
super.init(nibName: "PopupViewController", bundle: nil)
modalTransitionStyle = .crossDissolve
modalPresentationStyle = .overCurrentContext
}
required init?(coder aDecoder: NSCoder) {
fatalError("Please initialize programatically.")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
メモリリークのしない循環参照を実現しておくことで、
SVCはPVC表示のステータスバーを白に、
PVCはSVC表示のステータスバーを黒に変更することができます。
デメリットは、NCの preferredStatusBarStyle
がコール状態では使えないですね。
シングルトン(あるいはStatic変数)
こちらは僕個人的におすすめの方法です。
StatusBayStyle
の値をどのクラスからも参照できる、
シングルトン(あるいはStatic変数)に持っておく形です。
シングルトンの例:
final class StatusBarSingleton: NSObject {
static let shared = StatusBarSingleton()
var statusBarStyle: UIStatusBarStyle = .default
}
常にNCの preferredStatusBarStyle
がコールされる状態なら、
extension UINavigationController {
open override var preferredStatusBarStyle: UIStatusBarStyle {
return StatusBarSingleton.shared.statusBarStyle
}
}
と指定しておけば、あとは対象の画面で、
StatusBarSingleton.shared.statusBarStyle = .default
setNeedsStatusBarAppearanceUpdate()
とすることでどの画面でもStatusBarの変更が可能です。
1つのアプリケーション内でStatusBarは常に1つしかないので、
シングルトンは効果的だと自分は思っています。
viewWillAppearとviewDidAppear
ある特定の画面内でだけStatusBarを変更する場合などは、
上記のシングルトンを利用するなどして、
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
StatusBarSingleton.shared.statusBarStyle = .lightContent
setNeedsStatusBarAppearanceUpdate()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
StatusBarSingleton.shared.statusBarStyle = .default
setNeedsStatusBarAppearanceUpdate()
}
とすれば、イベント毎ではなくスクリーン毎にStatusBarの色を変更することが容易になります。
まとめ
UIApplication.shared.statusBarStyle
って便利だったなと思わざるを得ません。
もし「StatusBarの色が全然変わらない...」という悩みを抱えるエンジニアがいれば、
参考にしてもらえたらと思います。
※ 参考:
- https://qiita.com/ux_design_tokyo/items/8e62977b7609e68755c7
- https://qiita.com/yimajo/items/7051af0919b5286aecfe
- stack overflow: preferredStatusBarStyle isn't called
https://stackoverflow.com/questions/19022210/preferredstatusbarstyle-isnt-called - developer help: setNeedsStatusBarAppearanceUpdate()
https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat