Swift 專案總結 06 基於控制器的全域性狀態列管理

執著丶執念發表於2018-06-02

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 修改配置進行選擇

Swift 專案總結 06   基於控制器的全域性狀態列管理

基於控制器的全域性管理單例類

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
    }
}
複製程式碼

分離的狀態列狀態實現

實現分離的狀態管理才是我們的重點,不然我們直接使用全域性管理就行了,沒必要這麼麻煩,首先我們要理解什麼是分離的狀態管理,先看下圖:

Swift 專案總結 06   基於控制器的全域性狀態列管理

當我們顯示前面檢視時,後面檢視的狀態列變化是不會影響到前面檢視的狀態列狀態,這就是狀態列狀態的分離。

要實現這個功能,我聯絡到控制器導航使用的是 push 和 pop 操作,present 和 dismiss 操作也可以看成一個 push 和 pop 操作,我就想那我的狀態列狀態是否也能通過 push 和 pop 進行管理呢?

看起來是可以,但我們要想到另外一個問題,那就是分頁控制器用 push 和 pop 管理狀態列狀態是不行的,因為是同層級控制器,那就需要有多分支的儲存結構,不能用線性表結構,我想到了資料結構裡的樹結構!再仔細一想,UIView 檢視層次不就是一個樹結構嗎?好,就決定是你了!!!

解決問題

樹結構示意圖:

Swift 專案總結 06   基於控制器的全域性狀態列管理

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哈哈~,求關注求贊

相關文章