iOS Status Bar 的隐藏

自己一个人负责项目的好处是:代码想怎么写就怎么写,然而坏处是:代码要多渣有多渣,踩坑也是必须的

iOS开发也已有一年半,踩过的坑实在是太多了,之前只是简单的记录下笔记,现在开始整理总结成文章,希望形成一套自己的理论体系。

-------------------------------------- 我是一条华丽的分隔线 --------------------------------------

之前一直以为,隐藏 status bar 只是一句简单的语句调用就搞定了,但在做公司的项目的过程中,发现居然隐藏不了,纳闷了许久后算是明白了点,没想到也是蛮多坑的。

以下内容均是基于 iOS 9 及其以上的,如有出入,请善用搜索引擎

Status Bar 的正常隐藏

view-controllers 控制 status bar 的隐藏

在iOS 9中,status bar 的隐藏默认是通过 view-controlls 控制的,即每个控制器决定是否隐藏 status bar

只需在 controller 中重载 prefersStatusBarHidden 函数

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

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

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

  • 添加如下代码

    UIApplication.sharedApplication().statusBarHidden = true
    

如果想改成每个控制器自行控制,将 View controller-based status bar appearanceYES即可

Status Bar 隐藏不了的情况

但是,有时候也会有意外发生,上述方法并不能如愿隐藏 status bar ,那就是 ParentViewController 中添加一个全屏的 ChildViewController ,此时想用 ChildViewController 来控制状态栏时** ,就会失效,即使 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 的隐藏状态的话,就重载该方法,决定使用哪个 child view controller 来控制 隐藏/非隐藏 的状态。如果返回 nil 或不重载该方法,就用它自己来控制 status bar 的状态。可以通过调用 setNeedsStatusBarAppearanceUpdate 方法来改变该方法返回的值,即再调用该方法一次。

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
    }

}

上述代码最终能将状态栏隐藏,即使在 StatusBarHiddenParentController 中的prefersStatusBarHidden返回的是NO

注意:因为childViewControllerForStatusBarHidden会比viewDidLoad 先调用,所以在viewDidLoad中要调用setNeedsStatusBarAppearanceUpdate

有时候我们需要关闭控制器视图,如下列三种情况:

  • 将控制器从navigationController的堆栈中pop出去
  • 将present出来的控制器dismiss掉
  • 将子控制器从父控制器中移除

pushpresent 两种方式展示的 view controllerpopdismiss 时都能够自动还原 status bar 的状态。但是,把子控制器从父控制器中移除时,就会出现奇怪的问题, status bar 并不能自动还原,因此还需要特别处理。

我们可以设置一个Bool变量 statusBarHidden 用来记录目前 statusBar 是否隐藏,在 prefersStatusBarHidden 函数中返回 statusBarHidden值。在要移除子控制器的函数中做两步操作:

  1. 先将 statusBarHidden 设置为 False, 并调用 setNeedsStatusBarAppearanceUpdate 刷新状态栏
  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

对于 status bar 隐藏不了的情况,除了上面介绍的方法,iOS 中还有一种强大的黑魔法 Method Swizzling,这里设计到了 Objective-C 语言的 runtime 特性,这是一个值得深入学习和研究的知识。

我们只需要在 ChildeViewController 中对 ParentViewControllerprefersStatusBarHidden 方法进行 Hook ,然后偷天换日,换成我们自己实现的方法,使其返回 true ,然后刷新状态栏,就可以隐藏 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
    }
}

注意事项:

  • 必须在 ChildViewControllerviewWillDisappear 中将 Hook 的方法还原回来,否则可能会出现奇怪的问题。
  • ParentViewController 中尽可能重载 prefersStatusBarHidden 方法,因为 Method Swizzling 有很多细节需要谨慎处理,如果 hookPrefersStatusBarHidden 写的有bug,会导致奇怪的问题。

滚动ScrollView或TableView时隐藏Status Bar和Navigation Bar

京东和淘宝客户端的商品搜索结果页面,在滚动时可以隐藏 status barnavigation bar ,我自认为这是一个很好的设计,身为一个有追求的程序员,这个功能的实现那当然也不能放过啦~~

NavigationController 有一个属性 hidesBarsOnSwipe,可以实现轻扫时隐藏 navigation bar ,与之对应的手势是它的另一个属性barHideOnSwipeGestureRecognizer,只要给这个手势添加一个方法来控制 status bar ,就可以实现同时隐藏 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


扩展阅读:SWIZZLE

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

推荐阅读更多精彩内容