iOS Status Bar 的隱藏

自己一個(gè)人負(fù)責(zé)項(xiàng)目的好處是:代碼想怎么寫就怎么寫,然而壞處是:代碼要多渣有多渣,踩坑也是必須的

iOS開發(fā)也已有一年半,踩過的坑實(shí)在是太多了,之前只是簡單的記錄下筆記,現(xiàn)在開始整理總結(jié)成文章,希望形成一套自己的理論體系。

-------------------------------------- 我是一條華麗的分隔線 --------------------------------------

之前一直以為,隱藏 status bar 只是一句簡單的語句調(diào)用就搞定了,但在做公司的項(xiàng)目的過程中,發(fā)現(xiàn)居然隱藏不了,納悶了許久后算是明白了點(diǎn),沒想到也是蠻多坑的。

以下內(nèi)容均是基于 iOS 9 及其以上的,如有出入,請(qǐng)善用搜索引擎

Status Bar 的正常隱藏

view-controllers 控制 status bar 的隱藏

在iOS 9中,status bar 的隱藏默認(rèn)是通過 view-controlls 控制的,即每個(gè)控制器決定是否隱藏 status bar 。

只需在 controller 中重載 prefersStatusBarHidden 函數(shù)

override func prefersStatusBarHidden() -> Bool {
    return true
}
全局控制 status bar 的隱藏

如果想要全局控制,只需兩步:

  • 在Info.plist中,添加屬性 View controller-based status bar appearanceNO

  • 添加如下代碼

    UIApplication.sharedApplication().statusBarHidden = true
    

如果想改成每個(gè)控制器自行控制,將 View controller-based status bar appearanceYES即可

Status Bar 隱藏不了的情況

但是,有時(shí)候也會(huì)有意外發(fā)生,上述方法并不能如愿隱藏 status bar ,那就是 ParentViewController 中添加一個(gè)全屏的 ChildViewController ,此時(shí)想用 ChildViewController 來控制狀態(tài)欄時(shí)** ,就會(huì)失效,即使 ChildViewController 中的prefersStatusBarHidden方法返回的是YES,也無法隱藏 status bar 。

解決辦法是:重載 childViewControllerForStatusBarHidden方法

蘋果官方文檔 UIViewController Class Reference 是這樣解釋的:

Called when the system needs the view controller to use for determining status bar hidden/unhidden state.

Return Value:

The view controller whose status bar hidden/unhidden status should be used. Default return value is nil.

Discussion:

If your container view controller derives derives the hidden state of the status bar from one of its child view controllers, implement this method to specify which child view controller you want to control the hidden/unhidden state. If you return nil or do not override this method, the status bar hidden/unhidden state for self is used.

If you change the return value from this method, call the setNeedsStatusBarAppearanceUpdate method.

大概意思就是,如果你想要讓你的 container view controllerchild view controller 控制 status bar 的隱藏狀態(tài)的話,就重載該方法,決定使用哪個(gè) child view controller 來控制 隱藏/非隱藏 的狀態(tài)。如果返回 nil 或不重載該方法,就用它自己來控制 status bar 的狀態(tài)??梢酝ㄟ^調(diào)用 setNeedsStatusBarAppearanceUpdate 方法來改變?cè)摲椒ǚ祷氐闹?,即再調(diào)用該方法一次。

class StatusBarHiddenParentController: UIViewController {
    
    var childController: StatusBarHiddenChildController?

    override func viewDidLoad() {
        super.viewDidLoad()

        childController = StatusBarHiddenChildController.fromStoryboard("Main")
        addChildViewController(childController!)
        view.addSubview(childController!.view)
        setNeedsStatusBarAppearanceUpdate()
    }

    override func childViewControllerForStatusBarHidden() -> UIViewController? {
        return childController
    }
    
    override func prefersStatusBarHidden() -> Bool {
        return false
    }

}


class StatusBarHiddenChildController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func prefersStatusBarHidden() -> Bool {
        return true
    }

}

上述代碼最終能將狀態(tài)欄隱藏,即使在 StatusBarHiddenParentController 中的prefersStatusBarHidden返回的是NO

注意:因?yàn)?code>childViewControllerForStatusBarHidden會(huì)比viewDidLoad 先調(diào)用,所以在viewDidLoad中要調(diào)用setNeedsStatusBarAppearanceUpdate

有時(shí)候我們需要關(guān)閉控制器視圖,如下列三種情況:

  • 將控制器從navigationController的堆棧中pop出去
  • 將present出來的控制器dismiss掉
  • 將子控制器從父控制器中移除

pushpresent 兩種方式展示的 view controllerpopdismiss 時(shí)都能夠自動(dòng)還原 status bar 的狀態(tài)。但是,把子控制器從父控制器中移除時(shí),就會(huì)出現(xiàn)奇怪的問題, status bar 并不能自動(dòng)還原,因此還需要特別處理。

我們可以設(shè)置一個(gè)Bool變量 statusBarHidden 用來記錄目前 statusBar 是否隱藏,在 prefersStatusBarHidden 函數(shù)中返回 statusBarHidden值。在要移除子控制器的函數(shù)中做兩步操作:

  1. 先將 statusBarHidden 設(shè)置為 False, 并調(diào)用 setNeedsStatusBarAppearanceUpdate 刷新狀態(tài)欄
  2. 再將子控制器移除。
class StatusBarHiddenRemoveWayChildController: UIViewController {
    
    var statusBarHidden: Bool = true
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let removeButton = UIButton(frame: CGRectMake(30, 80, 100, 30))
        removeButton.setTitle("remove", forState: .Normal)
        removeButton.addTarget(self, action: "remove", forControlEvents: .TouchUpInside)
        view.addSubview(removeButton)
    }
    
    // 1. set `statusBarHidden` into `false`, and then refresh status bar
    // 2. remove from parent controller
    
    func remove() {
        statusBarHidden = false
        setNeedsStatusBarAppearanceUpdate()
        
        view.removeFromSuperview()
        removeFromParentViewController()
    }
    
    override func prefersStatusBarHidden() -> Bool {
        return statusBarHidden
    }
}

使用 Method Swizzling 隱藏 Status Bar

對(duì)于 status bar 隱藏不了的情況,除了上面介紹的方法,iOS 中還有一種強(qiáng)大的黑魔法 Method Swizzling,這里設(shè)計(jì)到了 Objective-C 語言的 runtime 特性,這是一個(gè)值得深入學(xué)習(xí)和研究的知識(shí)。

我們只需要在 ChildeViewController 中對(duì) ParentViewControllerprefersStatusBarHidden 方法進(jìn)行 Hook ,然后偷天換日,換成我們自己實(shí)現(xiàn)的方法,使其返回 true ,然后刷新狀態(tài)欄,就可以隱藏 status bar 了。

代碼如下:


class StatusBarHiddenSwizzlingChildController: UIViewController {

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        
        guard let parentViewController = parentViewController else {
            return
        }
        
        if parentViewController.respondsToSelector("setNeedsStatusBarAppearanceUpdate") {
            
            hookPrefersStatusBarHidden(parentViewController)
        }
    }
    
    func hookPrefersStatusBarHidden(parentViewController: UIViewController) {
        
        let originalSelector = Selector("prefersStatusBarHidden")
        let swizzledSelector = Selector("hook_prefersStatusBarHidden")
        
        let originalMethod = class_getInstanceMethod(parentViewController.dynamicType, originalSelector)
        let swizzledMethod = class_getInstanceMethod(self.dynamicType, swizzledSelector)
        
        let didAddMethod: Bool = class_addMethod(parentViewController.dynamicType,
            originalSelector,
            method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
        
        if didAddMethod {
            class_replaceMethod(self.dynamicType,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod))
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }
        
        dispatch_async(dispatch_get_main_queue()) { () -> Void in
        
            parentViewController.prefersStatusBarHidden()
            parentViewController.setNeedsStatusBarAppearanceUpdate()
        }
    }
    
    // must recover the hook when view will disappear
    
    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        
        guard let parentViewController = parentViewController else {
            return
        }
        
        if parentViewController.respondsToSelector("setNeedsStatusBarAppearanceUpdate") {
            
            hookPrefersStatusBarHidden(parentViewController)
        }
        
    }
    
    func hook_prefersStatusBarHidden() -> Bool {
        return true
    }
}

注意事項(xiàng):

  • 必須在 ChildViewControllerviewWillDisappear 中將 Hook 的方法還原回來,否則可能會(huì)出現(xiàn)奇怪的問題。
  • ParentViewController 中盡可能重載 prefersStatusBarHidden 方法,因?yàn)?Method Swizzling 有很多細(xì)節(jié)需要謹(jǐn)慎處理,如果 hookPrefersStatusBarHidden 寫的有bug,會(huì)導(dǎo)致奇怪的問題。

滾動(dòng)ScrollView或TableView時(shí)隱藏Status Bar和Navigation Bar

京東和淘寶客戶端的商品搜索結(jié)果頁面,在滾動(dòng)時(shí)可以隱藏 status barnavigation bar ,我自認(rèn)為這是一個(gè)很好的設(shè)計(jì),身為一個(gè)有追求的程序員,這個(gè)功能的實(shí)現(xiàn)那當(dāng)然也不能放過啦~~

NavigationController 有一個(gè)屬性 hidesBarsOnSwipe,可以實(shí)現(xiàn)輕掃時(shí)隱藏 navigation bar ,與之對(duì)應(yīng)的手勢(shì)是它的另一個(gè)屬性barHideOnSwipeGestureRecognizer,只要給這個(gè)手勢(shì)添加一個(gè)方法來控制 status bar ,就可以實(shí)現(xiàn)同時(shí)隱藏 status barnavigation bar 了。

class StatusBarAndNavigationBarHiddenOnSwipeController: UITableViewController {
    
    var hideStatusBar = false

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor.grayColor()
        
        navigationController?.barHideOnSwipeGestureRecognizer.addTarget(self, action: "swipe:")
        navigationController?.hidesBarsOnSwipe = true
    }
    
    override func prefersStatusBarHidden() -> Bool {
        return hideStatusBar
    }
    
    func swipe(recognizer: UISwipeGestureRecognizer) {
        
        hideStatusBar = navigationController?.navigationBar.frame.origin.y < 0
        
        UIView.animateWithDuration(0.2) { () -> Void in
            
            self.setNeedsStatusBarAppearanceUpdate()
        }
    }
    
    override func preferredStatusBarUpdateAnimation() -> UIStatusBarAnimation {
        return .Slide
    }
}

本文章所有示例代碼下載地址:Demo


擴(kuò)展閱讀:SWIZZLE

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容