阅读 647

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哈哈~,求关注求赞

文章分类
iOS
文章标签