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


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

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

黒いStatusBar | 白いStatusBar
 --- | ---
<img width=320px src="https://i.imgur.com/xq3fVPC.png"> | <img width=320px src="https://i.imgur.com/WjxFMeo.png">

**このステータスバーの色を反転するのにめちゃめちゃ苦労しました！**
ので、記録を後世のために残したいと思います。

## 開発環境

- macOS High Sierra 10.13.6
- Xcode 10.1
- Swift 4.2

## そもそもの背景

iOS9までは、StatusBarの色変更はそこまで難しいことではありませんでした。
`UIApplication.shared.statusBarStyle` に `UIStatusBarStyle` を指定するだけでした。

```swift
UIApplication.shared.statusBarStyle = .default //黒
UIApplication.shared.statusBarStyle = .lightContent //白
```

**しかし！！！iOS9.0からDeprecated扱いになってしまいました。**
警告内容：


```swift
Setter for 'statusBarStyle' was deprecated in iOS 9.0: Use -[UIViewController preferredStatusBarStyle]
```

代わりに `preferredStatusBarStyle` を使えば良いとのこと。
使用例：

```swift
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に設定しておく必要があります。

![](https://i.imgur.com/JLJrB5D.png)

日本語に言い換えるならば「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の色を変更したい場合の例：

<img width=320px src="https://i.imgur.com/KXM4rnd.png">

-> NCの `preferredStatusBarStyle` がコール

<img width=320px src="https://i.imgur.com/WhRGFIm.png">

-> SVCの `preferredStatusBarStyle` がコール

されます。

### 3. preferredStatusBarStyleが呼ばれるタイミング

タイミングは大きく分けて2種類です。

- ViewControllerの表示時
- `setNeedsStatusBarAppearanceUpdate` 動作時

ViewControllerの表示時にも呼ばれるので、明示的にStatusBarの色を変えたい場合には
`setNeedsStatusBarAppearanceUpdate` を使うようにすると良いです。

---

以上のことを踏まえ、ケースごとに説明します。
すべて、SVCのStatusBarの色を変更したい場合です。（一部PVC）

## ケース1: SVCのみ

<img width=320px src="https://i.imgur.com/XZQ331d.png">

-> SVC `preferredStatusBarStyle` がコール

こんな感じのコードで設定できるようにしておくと便利でしょう。

```swift
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` で遷移してきた画面
などが対象のケースです。

<img width=320px src="https://i.imgur.com/KXM4rnd.png">

-> NCの `preferredStatusBarStyle` がコール

ややこしいのですが、この場合2種類の指定方法があります。

### ケース2-1: SVCで指定できるようにする

NCのStatusBarStyleを参照しようとしたら、NCの `preferredStatusBarStyle` ではなく、
NCの前面、つまりNCのChildViewControllerの `preferredStatusBarStyle`
を参照するようにする、という設定ができます！

一部のNCだけの設定にしたい場合は、

```swift
class SampleNavigationViewController: UINavigationController {

    open override var childForStatusBarStyle: UIViewController? {
        return self.visibleViewController
    }
}
```

とすれば良いですし、全NCで設定したい場合は、

```swift
extension UINavigationController {
    open override var childForStatusBarStyle: UIViewController? {
        return self.visibleViewController
    }
}
```

という風にNCのextensionに設定すると便利でしょう。

### ケース2-2: NCで指定

デフォルトではNCの `preferredStatusBarStyle` が呼ばれます。

一部のNCだけの設定にしたい場合は、

```swift
class SampleNavigationViewController: UINavigationController {

    open override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }
}
```

とすれば良いですし、全NCで設定したい場合は、

```swift
extension UINavigationController {
    open override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }
}
```

という風にNCのextensionに設定すると便利でしょう。

## ケース3: SVC on TBC

<img width=320px src="https://i.imgur.com/WhRGFIm.png">

-> SVCの `preferredStatusBarStyle` がコール

ケース1と同じ設定方法でOKな、シンプルなケースです。

## ケース4: SVC on TBC on NC

だんだん複雑になっていきます。
`navigationController.pushViewController` で遷移してきた
`TabBarController` 上の画面が対象のケースです。

<img width=320px src="https://i.imgur.com/eAjelTG.png">

### ケース4-1: NCの `preferredStatusBarStyle` がコール

一番奥にあるControllerはNCです。その場合は大事な法則2より、
デフォルトではNCの `preferredStatusBarStyle` がコールされます。

ケース2-2と同じ設定方法でOKです。

### ケース4-2: NCで `childForStatusBarStyle` の指定をする

ケース2-1のように、

```swift
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が一番奥の場合です。

<img width=320px src="https://i.imgur.com/izNysLy.png">

-> ケース2と全く同じ。

一番奥がTBCなので、その前面のNCが対象になります。
その時点でケース2と全く同じ状態になります。

## ケース6: PVC on SVC on TBC on NC

ここにきてまさかの新キャラPVC登場です。

表記 | 内容
--- | ---
PVC | PopupViewController

ケース4の状態から、 `tabBarController?.present` で
PopupViewControllerが表示された状態です。

<img width=320px src="https://i.imgur.com/irWs9p3.png">

### ケース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が表示された状態です。

<img width=320px src="https://i.imgur.com/105fA86.png">

-> ケース5同様、ケース2と全く同じ。

つまりつまり！
**PVCの `preferredStatusBarStyle` がコールされることはありません！**

## PVCでStatusBarの色を操るためには

ケース6-2とケース7においては、
PVCからStatusBarの色を変化できないということになります。
さすがにそれは不便ですよね。

その解決策を、今の僕の知識で思いついたパターンを記載します。

### Protocol循環参照

SVC<->PVC間で循環参照できるようにするのです。
※NCの `childForStatusBarStyle` を指定して、SVCの`preferredStatusBarStyle` がコールされる状態にすることが前提。

SVCはステータスバー黒、PVCのステータスバー白にする場合のコードが下記。
クラス参照用のProtocol例：

```swift
protocol SampleDelegate: class {
    func setStatusBarStyle(style: UIStatusBarStyle)
}
```

SVC例：

```swift
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の例：

```swift
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変数）に持っておく形です。
シングルトンの例：

```swift
final class StatusBarSingleton: NSObject {
    static let shared = StatusBarSingleton()
    var statusBarStyle: UIStatusBarStyle = .default
}
```

常にNCの `preferredStatusBarStyle` がコールされる状態なら、

```swift
extension UINavigationController {
    open override var preferredStatusBarStyle: UIStatusBarStyle {
        return StatusBarSingleton.shared.statusBarStyle
    }
}
```

と指定しておけば、あとは対象の画面で、

```swift
StatusBarSingleton.shared.statusBarStyle = .default
setNeedsStatusBarAppearanceUpdate()
```

とすることでどの画面でもStatusBarの変更が可能です。
1つのアプリケーション内でStatusBarは常に1つしかないので、
シングルトンは効果的だと自分は思っています。

### viewWillAppearとviewDidAppear

ある特定の画面内でだけStatusBarを変更する場合などは、
上記のシングルトンを利用するなどして、

```swift
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