Runtime 隐藏Status Bar背景

Stella981
• 阅读 640

这次的主题的 Runtime ,对于很多人来说,习惯了面向对象的编程语言之后再接触 C 语言一开始是拒绝的。但是当你真的用起来了,你会上瘾,因为这彻彻底底地满足了极客们的折腾心理,用代码操控一切的心理。

就拿我做大象公会的例子来说(对了,这是我在 Smartisan 的第一个项目,也是独立开发的一款App),你知道 Smartisan 一贯的软件设计风格都是拟物化的,真实模拟着现实世界的自然规律。大到一个动画小到一个按钮,无处不体现着这一设计之初就贯彻的理念。然而这对于一个 iOS 工程师来说,不得不说是一个噩耗。你也知道如今强纳肾主管的苹果设计团队已经走了一条不归路了,所有 UI 元素都拍扁了。我不是说扁平化不好,因为这和拟物化只是两种并行的设计风格,没有对错,只有喜好。但你要是一股脑地全部拍扁了,那就有问题了,该给用户明确交互反馈的地方还得拟物。我在第一次拿到产品递过来的需求文档时,就意识到了这将会是个「在平坦的路面上曲折前行的」、「公然叫板苹果设计理念」的累差事。

比如今天我要引出 Runtime 这个话题的引子,就是 —— Navigation Bar.

Runtime 隐藏Status Bar背景

iOS7 之后(基本上现在这个年代,如果还有产品经理顽固地打算 iOS6 起跳的,你就可以... 不对,你要试图说服他)苹果的 Navigation Bar 虽然并没有和 Status Bar 连在一起,但是因为 Status Bar 的背景会默认与 Navigation Bar 的 barTintColor 一致,所以从视觉上看上去会觉得好像连在一起了。对于 iOS7 之后 Navigation Bar 发生的故事,你可以去 这里 看看。

如果你给 Navigation Bar 设置了一个背景图,就像上面你看到的那样,就会发现图片并不会延伸到 Status Bar 上。

Runtime 隐藏Status Bar背景

代码也很简单:

[self.navigationBar setBackgroundImage:[UIImage imageNamed:@"title_bar"] forBarPosition:UIBarPositionTop barMetrics:UIBarMetricsDefault];

通常你可能会觉得这已经满足了设计要求,今天故事到这里就应该结束了。但是接下来我翻看需求文档的时候,美好的幻想就被击碎了。不信,你看:

Runtime 隐藏Status Bar背景

没错,一个很简单的 presentViewController 动画就让处女座产品经理抓狂了,这里请脑补弹幕,各种工程师和产品之间的有趣对话。对话的结果就是,工程师屈服。事实上,任何人看到这多出的黑条都会觉得不舒服,不是吗?

一开始我的方向并不是 runtime,毕竟这把刀锋利是没错,但也容易伤着自己,所以开发的时候原则就是能不用就不用,除非为了设计解耦的架构或者公开API解决不了的时候。我一开始想到的自然还是这三个方法:

- (UIStatusBarStyle)preferredStatusBarStyle NS_AVAILABLE_IOS(7_0);- (BOOL)prefersStatusBarHidden NS_AVAILABLE_IOS(7_0);-(UIStatusBarAnimation)preferredStatusBarUpdateAnimation NS_AVAILABLE_IOS(7_0);

同时在 Info.plist 中增加一个 key View controller-based status bar appearance 并设置为YES 。以确保每个独立的 ViewController 可以独立地控制 Status Bar 的状态。

结果自然是吓人的。

Runtime 隐藏Status Bar背景

接下来又是一轮疯狂的 Google 和 StackOverflow,结果自然是无果(如果屏幕前的你,对就是你,有自己的非 runtime 实现的workaround欢迎在评论中留言,我将不胜感激)。到这里,其实就已经满足了 runtime 的使用前提,就是公开API解决不了,何况解耦又是求之不得的,这下我才想到是时候拿出这把锋利的剑了,然后也就可以引出了今天要讨论的内容了。

runtime 的基础普及不是今天的主题,网上有很多相关的文章,比如 这篇 。还有很多,我不推荐了,自学能力强的人其实都能自己搜到。

我简单说下我的理解。翻开 <objc/runtime.h> 的头文件的第一眼,我才知道原来 NSObject 也是放在 <objc/objc.h>的,以前一直以为是Foundation框架中的。上下滑动一看,飞流直下2000行,有种想关屏幕的冲动...简单来说,概念上的建模主要就是以下两个结构体:

struct objc_class {  
    Class isa 

#if !__OBJC2__
    Class super_class  
    const char *name                             
    long version                                 
    long info                                   
    long instance_size                           
    struct objc_ivar_list *ivars                 
    struct objc_method_list **methodLists       
    struct objc_cache *cache                     
    struct objc_protocol_list *protocols         
#endif}struct objc_method {  
    SEL method_name                             
    char *method_types                          
    IMP method_imp                                
}

你可以理解为 class 的范围最大,其中包含这 mehod ,imp 最小,包含在 method 中。

Runtime 隐藏Status Bar背景

对于 SEL method IMP 的区别,从上面 objc_method 的结构体我们就可以看出,一个方法Method,其实包含一个

  • 方法名 SEL – 表示该方法的名称;

  • 一个types – 表示该方法参数的类型;

  • 一个 IMP – 指向该方法的具体实现的函数指针,说白了IMP就是实现方法。

还有一些常用的 runtime 函数,你先认个眼熟,因为都会在下面的demo中用到。(我之所以只写出函数名而不写出有哪些参数是有考量的,如果现在就写出一大堆参数,只会让你让你感到迷惑,达不到先认个眼熟的目的,何况Xcode都有自动补齐,你记住了方法名还怕不知道有哪些参数)

  • 添加方法 class_addMethod @note :如果类中不存在这个方法的实现,添加成功;存在这个方法的实现,添加不成功

  • 替换方法 class_replaceMethod @note :如果以name标识的method不存在,就会添加这个method(就好像调用了class_addMethod);如果以name标识的method存在,替换imp

  • 获取class中的某个method class_getInstanceMethod

  • 获取method中的某个imp method_getImplementation or + (IMP)instanceMethodForSelector:(SEL)aSelector;

  • 交换两个method中的imp method_exchangeImplementations

  • 关联对象 objc_setAssociatedObject 
    获取对象 objc_getAssociatedObject 
    移除对象 objc_removeAssociatedObjects (文档中说,此函数的主要目的是在“初试状态”时方便返回一个对象。你不应该用这个函数来移除对象的属性,因为可能会导致其他clients对其添加的属性也被移除了。规范的方法是:调用 objc_setAssociatedObject 方法并传入一个 nil 值来清除一个关联。)

    (PS:这属于 Method Swizzling 技术, 我通常喜欢直白地叫方法偷换技术。Method Swizzling 也是 OC HOOK的一个方案。)

接下来我们看实际应用。打开层级调试视图:

Runtime 隐藏Status Bar背景

我用粗体和编号表示了层级关系。UINavigatinBar 是最底层的父视图,其上只有一个子视图_UINavigationBarBackground 。_UINavigationBarBackground 上又有两个子视图,分别是_UIBarBackgroundTopCurtainView 和 _UIBarBackgroundCustomImageContainer ,而我们的目的是让那块黑色区域也就是 _UIBarBackgroundTopCurtainView 消失。

好了,讲到这里逻辑部分已经结束了。下面看看代码中注意的地方。

首先是 + (void)load。load方法会在类第一次加载的时候被调用。因为load调用的时间比较靠前,适合在这个方法里做方法交换。而且国外大神们都这么推荐,没理由不做。

+ (void)load{    //方法交换应该被保证,在程序中只会执行一次
    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        //首先动态添加方法。如果类中不存在这个方法的实现,添加成功
        BOOL notAdded = class_addMethod(self, @selector(layoutSubviews), [self instanceMethodForSelector:@selector(__layoutSubviews)], method_getTypeEncoding(class_getInstanceMethod(self, @selector(__layoutSubviews))));        //因为UINavigationBar已经包含了layoutSubviews的实现,所以不会被添加成功
        if (notAdded) {            //如果UINavigationBar没有layoutSubviews这个方法的实现,那么添加成功,将被交换方法的实现替换到这个并不存在的实现
            class_replaceMethod(self, @selector(__layoutSubviews), [self instanceMethodForSelector:@selector(layoutSubviews)], method_getTypeEncoding(class_getInstanceMethod(self, @selector(layoutSubviews))));
        }else{            //否则交换两个方法的实现
            method_exchangeImplementations(class_getInstanceMethod(self, @selector(layoutSubviews)), class_getInstanceMethod(self, @selector(__layoutSubviews)));
        }

    });

}

交换方法实现 method_exchangeImplementations—— 我的理解是相当于拦截一个方法,这执行这个方法之前或之后注入另一个方法的代码。比如在这里在系统自动调用 [self layoutSubviews] 之后,我注入了我自己的代码。类似的思想,比如你想在全局范围上,在任何一个 NSObject alloc 之后打印一次log,就可以利用交换方法实现的技术,只要写一条NSLog就行了。这就有点AOP思想了。在编程思想中,传统的做法是,改造每个业务方法,这样势必把代码弄得一团糟,而且以后再扩展还是更乱,而AOP的思想是引导你从另一个切面来看待问题,比如上面log的例子,不管加在哪,它其实都是属于日志系统这个角度的。 AOP允许你以一种统一的方式在运行时期在想要的地方插入这些逻辑。说到底。就是把不同功能的代码分离开,以便能够分离复杂度。让人在同一时间只用思考代码逻辑,或者琐碎事务。

然后我们在 Navigation Bar 调用 layoutSubviews 时注入逻辑代码。

- (void)__layoutSubviews{    //这不是递归,其实调用了[self layoutSubviews];
    [self __layoutSubviews];    if (self.ky_hideStatusBarBackgroungView){

        Class backgroundClass = NSClassFromString(@"_UINavigationBarBackground");
        Class statusBarBackgroundClass = NSClassFromString(@"_UIBarBackgroundTopCurtainView");        for (UIView * aSubview in self.subviews){            if ([aSubview isKindOfClass:backgroundClass]) {
                aSubview.backgroundColor = [UIColor clearColor];                for (UIView * aaSubview in aSubview.subviews){                    if ([aaSubview isKindOfClass:statusBarBackgroundClass]) {                        //aaSubview.hidden = YES;                                      
                        aaSubview.backgroundColor = [[UIColor blackColor]colorWithAlphaComponent:0.01];                        
                    }
                }
            }
        }
    }
}

上面的代码就解释两点:

  • 你一定要区分名字SEL和实现IMP.[obj selector] selector只是名字,真正执行的是IMP。

  • [UINavigation layoutSubviews] 会调用 - (void)__layoutSubviews 的实现,也就是花括号中的代码; 而 [self __layoutSubviews] 会调用[UINavigation layoutSubviews] 的实现。所以不会递归。

注:_aaSubview.hidden = YES;_ 在项目中我又发现如果直接 hidden 背景视图会带来一些副作用,比如点击状态栏 TableView 无法回滚到顶部。所以,改成 aaSubview.backgroundColor = [[UIColor blackColor]colorWithAlphaComponent:0.01] 就可以避免这一问题。因为我发现设置成clearColor 依然无法点击回到顶部,所以目前的解决方法只能是设置一个 0.01 透明度的颜色了。

事实上,除了隐藏 Status Bar 之外,你还可以改变 Status bar 的背景颜色:

if ([aaSubview isKindOfClass:statusBarBackgroundClass]) {  
     aaSubview.backgroundColor = [UIColor lightGrayColor];
}

Runtime 隐藏Status Bar背景

最后一点,添加关联对象。我在头文件中已经声明了一个变量: @property (nonatomic,assign) BOOL ky_hideStatusBarBackgroungView;

//添加关联对象-(void)setKy_hideStatusBarBackgroungView:(BOOL)yesOrno{
    objc_setAssociatedObject(self, @selector(ky_hideStatusBarBackgroungView), @(yesOrno), OBJC_ASSOCIATION_ASSIGN);

    [self setNeedsLayout];

}//获取关联对象-(BOOL)ky_hideStatusBarBackgroungView{    return objc_getAssociatedObject(self, _cmd);
}

关于这个const void *key,最好是常量、唯一,比如是 static char 类型的:static char kAssociatedObjectKey;使用时用 &kAssociatedObjectKey。当然更推荐是指针型的:static char *kAssociatedObjectKey; 使用的时候直接就可以用kAssociatedObjectKey。 然而可以用更简单的方式实现:用selector。因为selector也能保证唯一并且是常量,所以可以把方法的地址作为唯一的key,并用_cmd代表当前调用方法的地址,就像:

- (void)addAssociatedObject:(id)object{

    objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)getAssociatedObject{    return objc_getAssociatedObject(self, _cmd);
}

小知识:根据WWDC 2011, Session 322 (第36分钟左右)发布的内存销毁时间表,被关联的对象在生命周期内要比对象本身释放的晚很多。它们会在被NSObject -dealloc 调用的 object_dispose() 方法中释放。

使用的时候就是一行代码搞定:

 self.navigationController.navigationBar.ky_hideStatusBarBackgroungView = YES;

最后的结局是美好的。

Runtime 隐藏Status Bar背景

以上就是篇文章的所有内容。不过也觉得这样的导航栏设计也只有 Smartisan 才有了。


后面的故事

强迫症是没有底线的,比如导航条两端圆角之外的黑块看着实在不爽:

Runtime 隐藏Status Bar背景

好在,解决起来也很容易:

if ([aaSubview isKindOfClass:navBarBackgoundImageClass]) {  
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:aaSubview.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(4.5, 4.5)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = aaSubview.bounds;
    maskLayer.path = maskPath.CGPath;
    aaSubview.layer.mask = maskLayer;
}

^_^


PS:写到这里,我不禁开始想一个哲学问题,就是为什么我们会默认就会想到用Category和runtime,我得出的原因是大家都这么用所以我也想到这么用。但是为什么要这么用?我们完全可以不用单独写一个category,直接在原来的类中就可以处理,还免去了使用runtime。但是为了遵循解耦和AOP的思想,我们一定要尽可能遵循科学规范的程序思想。其实真正的大牛,并不是说他知道的API比你多所以比你牛,而是他可以用更抽象、更解耦、更科学、更易维护的程序设计思想写程序。这就是软件工程师的境界了,非一朝一夕可以做到,你我在这条路上都还要躬行好多年。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这