Swift 项目总结 06 - 基于控制器的全局状态栏管理

发现问题

全局管理和局部管理状态栏

iOS 7 以前,我们只有基于 UIApplication 单例类的全局状态栏管理:

extension UIApplication {
    // Setting the statusBarStyle does nothing if your application is using the default UIViewController-based status bar system.
    @available(iOS, introduced: 2.0, deprecated: 9.0, message: "Use -[UIViewController preferredStatusBarStyle]")
    open func setStatusBarStyle(_ statusBarStyle: UIStatusBarStyle, animated: Bool)
    
    // Setting statusBarHidden does nothing if your application is using the default UIViewController-based status bar system.
    @available(iOS, introduced: 3.2, deprecated: 9.0, message: "Use -[UIViewController prefersStatusBarHidden]")
    open func setStatusBarHidden(_ hidden: Bool, with animation: UIStatusBarAnimation)
}

我们使用起来大概这样:

// 设置状态栏样式
UIApplication.shared.statusBarStyle = .default
// 设置状态栏是否隐藏
UIApplication.shared.isStatusBarHidden = false
// 设置状态栏是否隐藏,变化过程是否需要动画
UIApplication.shared.setStatusBarHidden(false, with: .fade)

但在 iOS 7 以后,苹果推出了另外一套状态栏管理机制,即基于控制器的局部状态栏管理,从官方注释可以看出这种机制是苹果推荐使用的:

extension UIViewController {
    @available(iOS 7.0, *)
    open var preferredStatusBarStyle: UIStatusBarStyle { get } // Defaults to UIStatusBarStyleDefault

    @available(iOS 7.0, *)
    open var prefersStatusBarHidden: Bool { get } // Defaults to NO

    // Override to return the type of animation that should be used for status bar changes for this view controller. This currently only affects changes to prefersStatusBarHidden.
    @available(iOS 7.0, *)
    open var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { get } // Defaults to UIStatusBarAnimationFade

    // 手动触发状态栏状态更新
    @available(iOS 7.0, *)
    open func setNeedsStatusBarAppearanceUpdate()
}

我们使用起来大概这样:

class ViewController: UIViewController {
    // 状态栏是否隐藏
    override var prefersStatusBarHidden: Bool {
        return false
    }
    // 状态栏样式
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .default
    }
    // 状态栏隐藏动画
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        return .fade
    }
}

默认情况下,状态栏都是基于控制器的状态管理,这 2 种状态栏管理机制可以通过在 info.plist 修改配置进行选择

基于控制器的全局管理单例类

2 种管理状态栏的形式各有优缺点:

全局管理

  • 优点:管理方便,代码简洁
  • 缺点:状态是全局共享的,相互影响

局部管理

  • 优点:状态是分离到各个控制器,互不影响
  • 缺点:管理不方便,管理代码分散到各个控制器

我想结合了这 2 种管理机制的优点,开发一个基于控制器的全局管理单例类 StatusBarManager,即能像 UIApplication 那样简洁的管理状态栏,又能像 UIViewController 那样分离的管理状态栏。

分析问题

基于控制器的全局管理实现

首先我们就需要先把基于控制器的管理状态栏转变成单例类管理状态栏,这很简单,类似下面这样实现,具体内部实现下面会给出源码:

/// 自定义基类控制器,重载 prefersStatusBarHidden 等方法
class BasicViewController: UIViewController {
    
    override var prefersStatusBarHidden: Bool {
        return StatusBarManager.shared.isHidden
    }
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return StatusBarManager.shared.style
    }
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        return StatusBarManager.shared.animation
    }
}

分离的状态栏状态实现

实现分离的状态管理才是我们的重点,不然我们直接使用全局管理就行了,没必要这么麻烦,首先我们要理解什么是分离的状态管理,先看下图:

当我们显示前面视图时,后面视图的状态栏变化是不会影响到前面视图的状态栏状态,这就是状态栏状态的分离。

要实现这个功能,我联系到控制器导航使用的是 push 和 pop 操作,present 和 dismiss 操作也可以看成一个 push 和 pop 操作,我就想那我的状态栏状态是否也能通过 push 和 pop 进行管理呢?

看起来是可以,但我们要想到另外一个问题,那就是分页控制器用 push 和 pop 管理状态栏状态是不行的,因为是同层级控制器,那就需要有多分支的存储结构,不能用线性表结构,我想到了数据结构里的树结构!再仔细一想,UIView 视图层次不就是一个树结构吗?好,就决定是你了!!!

解决问题

树结构示意图:

import UIKit

/// 状态栏单一状态节点
class StatusBarState: NSObject {
    static let defaultKey: String = "StatusBarState.default.root.key"
    
    var isHidden: Bool = false
    var style: UIStatusBarStyle = .lightContent
    var animation: UIStatusBarAnimation = .fade
    var key: String = defaultKey
    // 子节点数组
    var subStates: [StatusBarState] = []
    // 父节点,为 nil 说明是根节点
    weak var superState: StatusBarState?
    // 下一个路径节点,为 nil 说明是叶子节点
    weak var nextState: StatusBarState?
    
    override var description: String {
        return "{ key=\(self.key) selected=\(String(describing: self.nextState?.key)) }"
    }
}

/// 全局状态栏状态管理单例类
class StatusBarManager {
    static let shared = StatusBarManager()
    // MARK: - 属性
    /// 状态键集合,用来判断树中是否有某个状态
    fileprivate var stateKeys: Set<String> = Set<String>()
    /// 根节点状态,从这个根节点可以遍历到整个状态树
    fileprivate var rootState: StatusBarState!
    /// 更新状态栏动画时间
    fileprivate var duration: TimeInterval = 0.1
    /// 当前状态
    fileprivate var currentState: StatusBarState!
    
    /// 以下3个计算属性都是取当前状态显示以及更新当前状态
    var isHidden: Bool {
        get {
            return currentState.isHidden
        }
        set {
            setState(for: currentState.key, isHidden: newValue)
        }
    }
    var style: UIStatusBarStyle {
        get {
            return currentState.style
        }
        set {
            setState(for: currentState.key, style: newValue)
        }
    }
    var animation: UIStatusBarAnimation {
        get {
            return currentState.animation
        }
        set {
            setState(for: currentState.key, animation: newValue)
        }
    }
    
    // MARK: - 方法
    /// 初始化根节点
    fileprivate init() {
        rootState = StatusBarState()
        currentState = rootState
        stateKeys.insert(rootState.key)
    }
    
    /// 为某个状态(root)添加子状态(key),当 root = nil 时,表示添加到根状态上
    @discardableResult
    func addSubState(with key: String, root: String? = nil) -> StatusBarState? {
        guard !stateKeys.contains(key) else { return nil }
        stateKeys.insert(key)
        
        let newState = StatusBarState()
        newState.key = key
        
        // 找到键为 root 的父状态
        var superState: StatusBarState! = rootState
        if let root = root {
            superState = findState(root)
        }
        newState.isHidden = superState.isHidden
        newState.style = superState.style
        newState.animation = superState.animation
        newState.superState = superState
        
        // 添加进父状态的子状态集合中,默认选中第一个
        superState.subStates.append(newState)
        if superState.nextState == nil {
            superState.nextState = newState
        }
        
        // 判断是否在当前状态上添加子状态,是的话,自动切换当前状态
        if currentState.key == superState.key {
            currentState = newState
            updateStatusBar()
        }
        
        printAllStates()
        return newState
    }
    
    /// 删除某个状态及其子状态树
    func removeState(with key: String) {
        guard stateKeys.contains(key) else { return }
        let state = findState(key)
        let isContainCurrentState = findStateInTree(state, key: currentState.key) != nil
        if state.subStates.count > 0 {
            removeSubStatesInTree(state)
        }
        // 是否有父状态,如果没有,说明要删除的是根状态,根节点是不能删除的,否则删除该节点并切换当前状态
        if let superState = state.superState {
            stateKeys.remove(state.key)
            if let index = superState.subStates.index(of: state) {
                superState.subStates.remove(at: index)
            }
            superState.nextState = superState.subStates.first
            if isContainCurrentState {
                if let selectedState = superState.nextState {
                    currentState = selectedState
                } else {
                    currentState = superState
                }
                updateStatusBar()
            }
            
        }
        printAllStates()
    }
    
    /// 更改某个状态(root)下要显示直接的子状态节点(key)
    func showState(for key: String, root: String? = nil) {
        guard stateKeys.contains(key) else { return }
        
        // 改变父状态 nextState 属性
        let rootState = findState(root)
        for subState in rootState.subStates {
            if subState.key == key {
                rootState.nextState = subState
                break
            }
        }
        // 找到切换后的当前状态
        let newCurrentState = findCurrentStateInTree(rootState)
        if newCurrentState != currentState {
            currentState = newCurrentState
            updateStatusBar()
        }
        printAllStates()
    }
    
    /// 删除某个状态下的子状态树
    func clearSubStates(with key: String, isUpdate: Bool = true) {
        guard stateKeys.contains(key) else { return }
        let state = findState(key)
        var needUpdate: Bool = false
        if findStateInTree(state, key: currentState.key) != nil {
            currentState = state
            needUpdate = true
        }
        if state.subStates.count > 0 {
            removeSubStatesInTree(state)
        }
        if needUpdate && isUpdate {
            updateStatusBar()
        }
        printAllStates()
    }
    
    /// 负责打印状态树结构
    func printAllStates(_ method: String = #function) {
        debugPrint("\(method): currentState = \(currentState.key)")
        printAllStatesInTree(rootState, deep: 0, method: method)
    }

    /// 更新栈中 key 对应的状态,key == nil 表示栈顶状态
    func setState(for key: String? = nil, isHidden: Bool? = nil, style: UIStatusBarStyle? = nil, animation: UIStatusBarAnimation? = nil) {
        var needUpdate: Bool = false
        let state = findState(key)
        if let isHidden = isHidden, state.isHidden != isHidden {
            needUpdate = true
            state.isHidden = isHidden
        }
        if let style = style, state.style != style {
            needUpdate = true
            state.style = style
        }
        if let animation = animation, state.animation != animation {
            needUpdate = true
            state.animation = animation
        }
        // key != nil 表示更新对应 key 的状态,需要判断该状态是否是当前状态
        if let key = key {
            guard let currentState = currentState, currentState.key == key else { return }
        }
        // 状态有变化才需要更新视图
        if needUpdate {
            updateStatusBar()
        }
    }
    
    /// 开始更新状态栏的状态
    fileprivate func updateStatusBar() {
        DispatchQueue.main.async { // 在主线程异步执行 避免同时索取同一属性
            // 如果状态栏需要动画(fade or slide),需要添加动画时间,才会有动画效果
            UIView.animate(withDuration: self.duration, animations: {
                UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
            })
        }
    }
    
    /// 从状态树中找到对应的节点状态,没找到就返回根节点
    fileprivate func findState(_ key: String? = nil) -> StatusBarState {
        if let key = key { // 查找
            if let findState = findStateInTree(rootState, key: key) {
                return findState
            }
        }
        return rootState
    }
    
    /// 从状态树中找到对应的节点状态的递归方法
    fileprivate func findStateInTree(_ state: StatusBarState, key: String) -> StatusBarState? {
        if state.key == key {
            return state
        }
        for subState in state.subStates {
            if let findState = findStateInTree(subState, key: key) {
                return findState
            }
        }
        return nil
    }
    
    /// 删除某个状态下的所有子状态的递归方法
    fileprivate func removeSubStatesInTree(_ state: StatusBarState) {
        state.subStates.forEach { (subState) in
            stateKeys.remove(subState.key)
            removeSubStatesInTree(subState)
        }
        state.subStates.removeAll()
    }
    
    /// 找到某个状态下的最底层状态
    fileprivate func findCurrentStateInTree(_ state: StatusBarState) -> StatusBarState? {
        if let nextState = state.nextState {
            return findCurrentStateInTree(nextState)
        }
        return state
    }
    
    /// 打印状态树结构的递归方法
    fileprivate func printAllStatesInTree(_ state: StatusBarState, deep: Int = 0, method: String) {
        debugPrint("\(method): \(deep) - state=\(state)")
        for subState in state.subStates {
            printAllStatesInTree(subState, deep: deep + 1, method: method)
        }
    }
}

创建 UIViewController+StatusBar 分类和基类控制器来辅助设置,简单管理状态栏:

/// UIViewController+StatusBar.swift
import UIKit

extension UIViewController {
    
    /// 控制器的状态栏唯一键
    var statusBarKey: String {
        return "\(self)"
    }
    
    /// 设置该控制器的状态栏状态
    func setStatusBar(isHidden: Bool? = nil, style: UIStatusBarStyle? = nil, animation: UIStatusBarAnimation? = nil) {
        StatusBarManager.shared.setState(for: statusBarKey, isHidden: isHidden, style: style, animation: animation)
    }

    /// 添加一个子状态
    func addSubStatusBar(for viewController: UIViewController) {
        let superKey = self.statusBarKey
        let subKey = viewController.statusBarKey
        StatusBarManager.shared.addSubState(with: subKey, root: superKey)
    }
    
    /// 批量添加子状态,树横向生长
    func addSubStatusBars(for viewControllers: [UIViewController]) {
        viewControllers.forEach { (viewController) in
            self.addSubStatusBar(for: viewController)
        }
    }
    
    /// 从整个状态树上删除当前状态
    func removeFromSuperStatusBar() {
        let key = self.statusBarKey
        StatusBarManager.shared.removeState(with: key)
    }
    
    /// 设置当前状态下的所有子状态
    func setSubStatusBars(for viewControllers: [UIViewController]?) {
        clearSubStatusBars()
        if let viewControllers = viewControllers {
            addSubStatusBars(for: viewControllers)
        }
    }
    
    /// 通过类似压栈的形式,压入一组状态,树纵向生长
    func pushStatusBars(for viewControllers: [UIViewController]) {
        var lastViewController: UIViewController? = self
        viewControllers.forEach { (viewController) in
            if let superController = lastViewController {
                superController.addSubStatusBar(for: viewController)
                lastViewController = viewController
            }
        }
    }
    
    /// 切换多个子状态的某个子状态
    func showStatusBar(for viewController: UIViewController?) {
        guard let viewController = viewController else { return }
        let superKey = self.statusBarKey
        let subKey = viewController.statusBarKey
        StatusBarManager.shared.showState(for: subKey, root: superKey)
    }
    
    /// 清除所有子状态
    func clearSubStatusBars(isUpdate: Bool = true) {
        StatusBarManager.shared.clearSubStates(with: self.statusBarKey, isUpdate: isUpdate)
    }
}
/// 保证所有控制器都重载了 prefersStatusBarHidden 的方法
class BasicViewController: UIViewController {
    
    override var prefersStatusBarHidden: Bool {
        return StatusBarManager.shared.isHidden
    }
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return StatusBarManager.shared.style
    }
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        return StatusBarManager.shared.animation
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    deinit {
         self.removeFromSuperStatusBar()
    }
    
    override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        self.addSubStatusBar(for: viewControllerToPresent)
        super.present(viewControllerToPresent, animated: flag, completion: completion)
    }
}

/// 保证所有控制器都重载了 prefersStatusBarHidden 的方法
class BasicNavigationController: UINavigationController {
    
    override var prefersStatusBarHidden: Bool {
        return StatusBarManager.shared.isHidden
    }
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return StatusBarManager.shared.style
    }
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        return StatusBarManager.shared.animation
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        pushStatusBars(for: viewControllers)
    }
    
    override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
        clearSubStatusBars(isUpdate: false)
        pushStatusBars(for: viewControllers)
        super.setViewControllers(viewControllers, animated: animated)
    }
    
    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        topViewController?.addSubStatusBar(for: viewController)
        super.pushViewController(viewController, animated: animated)
    }
}

/// 保证所有控制器都重载了 prefersStatusBarHidden 的方法
class BasicTabBarController: UITabBarController, UITabBarControllerDelegate {
    
    override var prefersStatusBarHidden: Bool {
        return StatusBarManager.shared.isHidden
    }
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return StatusBarManager.shared.style
    }
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        return StatusBarManager.shared.animation
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.setSubStatusBars(for: viewControllers)
        self.delegate = self
    }
    
    override func setViewControllers(_ viewControllers: [UIViewController]?, animated: Bool) {
        self.setSubStatusBars(for: viewControllers)
        super.setViewControllers(viewControllers, animated: animated)
    }
    
    func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
        showStatusBar(for: viewController)
    }
}

基于控制器的全局状态栏使用:

class ViewController: BasicViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setStatusBar(isHidden: false, style: .default)
    }
}

Demo 源码在这里:StatusBarManagerDemo

有什么问题可以在下方评论区提出,写得不好可以提出你的意见,我会合理采纳的,O(∩_∩)O哈哈~,求关注求赞

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,612评论 4 59
  • 年少时,我们总以为父母无坚不摧,直到撞破他们的无助,才明白,那是因为爱,他们才愿意扮演英雄。
    青栀恋阅读 380评论 0 1
  • 今天距离开始坚持摄影,已经232天了,也是第三十三周。 虽说没有全身心地投入进去,但是每天也都会留些时间想想拍些什...
    宫崎虫师阅读 228评论 2 7
  • 小胖要当爸爸了!真替他高兴。要知道,为了获得这个“职称”的晋升,可真是不容易呢。 小胖从小就得了肾病,三十岁出头的...
    欢乐V英雄阅读 386评论 2 4
  • 第一次接触韩剧是来自于很小的时候看的那部《天国的阶梯》,当年韩静书和车诚俊之间波折的爱情,真的是赚足了我的眼泪。那...
    陈远阅读 371评论 1 0