178
129

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-28

携帯端末のステータスバーってありますよね、
次回やネットの接続状況、電池の残量などを表示してくれていて、
結構このステータスバーを見るためだけに携帯を開くなんてこと、自分はよくあります。

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

黒い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の色が全然変わらない...」という悩みを抱えるエンジニアがいれば、
参考にしてもらえたらと思います。

※ 参考:

178
129
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
178
129

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?