背景色からpreferredStatusBarStyleを動的に設定する

※ 記事最下部に追記あり(2022/07/06 19:25)

開発環境

> xcodebuild -version
Xcode 14.0
Build version 14A5229c

Xcodeはbetaだが本記事の内容はiOS16未満のAPIを使用。

モチベーション

背景色からpreferredStatusBarStyleを動的に生成したい。
view controllerの背景色が白に近い時は .darkContent に、黒に近い時は .lightContent を設定したい。

作ったもの

動画

www.youtube.com

差分

.darkContent .lightContent

コード

import UIKit

final class ViewController: UIViewController {
    @IBAction func pushButton(_ sender: Any) {
        let secondViewController = storyboard?.instantiateViewController(
            identifier: "secondViewController"
        ) { coder in
            SecondViewController(
                coder: coder,
                backgroundColor: .systemIndigo
            )
        }
        if let secondViewController = secondViewController {
            secondViewController.modalPresentationStyle = .fullScreen
            self.present(secondViewController, animated: true)
        }
    }
}


final class SecondViewController: UIViewController {
    let backgroundColor: UIColor
    
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return backgroundColor.binarized() == .white ? .darkContent : .lightContent
    }
    
    init?(coder: NSCoder, backgroundColor: UIColor) {
        self.backgroundColor = backgroundColor
        super.init(coder: coder)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = backgroundColor
    }
}

private extension UIColor {
    enum BinarizationType {
        case black
        case white
    }
    
    func binarized() -> BinarizationType {
        var white: CGFloat = 0
        self.getWhite(&white, alpha: nil)
        return white >= 0.5 ? .white : .black
    }
}

以下ポイント。

preferredStatusBarStyleはview controllerの初期化時に指定する必要がある

let secondViewController = storyboard?.instantiateViewController(
    identifier: "secondViewController"
) { coder in
    SecondViewController(
        coder: coder,
        backgroundColor: .systemIndigo
    )
}

preferredStatusBarStyleはgetオンリーなプロパティのため、view controllerの初期化時に指定する必要がある。ということは、指定の判断材料である背景色もview controller初期化時に値が存在する必要がある。そこで、view controllerの初期化をinstantiateViewController(identifier:creator:)で行って背景色を指定する形を取った。

UIColorを二値化する

final class SecondViewController: UIViewController {
    let backgroundColor: UIColor
    
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return backgroundColor.binarized() == .white ? .darkContent : .lightContent
    }
    
    // 省略
}

private extension UIColor {
    enum BinarizationType {
        case black
        case white
    }
    
    func binarized() -> BinarizationType {
        var white: CGFloat = 0
        self.getWhite(&white, alpha: nil)
        return white >= 0.5 ? .white : .black
    }
}

getWhite(_:alpha:)を使ってUIColorのグレースケールを取得できるので、0.5をしきい値として白黒で返す処理を書いた。

所感

短時間で自分の希望通りの実行結果を得られるコードが書けた。UIKit力が上がっているのかもしれない。そうであることを願う。

追記(2022/07/06 19:25)

社の先輩に教えてもらった。 上記までの内容でなくともpreferredStatusBarStyleを変更できる。

import UIKit

final class ViewController: UIViewController {
    @IBAction func pushButton(_ sender: Any) {
        guard let secondViewController = storyboard?.instantiateViewController(withIdentifier: "secondViewController") as? SecondViewController else {
            return
        }
        secondViewController.backgroundColor = .systemIndigo
        secondViewController.modalPresentationStyle = .fullScreen
        self.present(secondViewController, animated: true)
    }
}


final class SecondViewController: UIViewController {
    var backgroundColor: UIColor? {
        didSet {
            guard let backgroundColor = backgroundColor else {
                return
            }
            view.backgroundColor = backgroundColor
            setNeedsStatusBarAppearanceUpdate()
        }
    }
    
    override var preferredStatusBarStyle: UIStatusBarStyle {
        guard let backgroundColor = backgroundColor else {
            return .default
        }
        return backgroundColor.binarized() == .white ? .darkContent : .lightContent
    }
}

以下、ポイント。

参考