Edited at

iOSのステータスバーを自在に扱う

携帯端末のステータスバーってありますよね、

次回やネットの接続状況、電池の残量などを表示してくれていて、

結構このステータスバーを見るためだけに携帯を開くなんてこと、自分はよくあります。

下図は背景が白でステータスバーが黒、

背景が黒でステータスバーが白いです。

黒いStatusBar
白いStatusBar


このステータスバーの色を反転するのにめちゃめちゃ苦労しました!

ので、記録を後世のために残したいと思います。


開発環境


  • macOS High Sierra 10.13.6

  • Xcode 10.1

  • Swift 4.2


そもそもの背景

iOS9までは、StatusBarの色変更はそこまで難しいことではありませんでした。

UIApplication.shared.statusBarStyleUIStatusBarStyle を指定するだけでした。

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
}
}

と指定していた場合、どうなるでしょうか。

大事な法則から紐解いていきます。


  1. デフォルトではNCの preferredStatusBarStyle がコールされるはず


  2. childForStatusBarStyle の指定をしているので、対象が前面になる

  3. NCの前面はTBCなので、TBCの preferredStatusBarStyle がコールされ

  4. るのではなく、大事な法則より、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の色が全然変わらない...」という悩みを抱えるエンジニアがいれば、

参考にしてもらえたらと思います。

※ 参考: