iOS11之后导航控制器中的控制器适配全集,以及如何不发版自动适配全面屏系列

内容均为原创, 如有任何疑问或者错误,请在文章下留言或者直接与我联系,一定及时回复: )

本文要讨论的问题

  1. 是什么影响了导航控制器的子控制器view布局变化?
  2. safeAreaInsets介绍以及如何利用它适配View布局。
  3. 适配全面屏的新思路
  4. safeAreaInsets是如何影响着scrollView及其子类(tableView)

为了更好的说明问题,本文的代码均在iphoneX模拟器中运行。

1.是什么影响了导航控制器的子控制器view布局变化?

当一个viewController在一个导航控制器中显示,有时候布局会被导航栏遮挡,控件的Y值需要从64(home键型号手机)或者88(全面屏手机)开始布局, 而有时候控件的Y值从0开始布局,也不会被导航栏遮挡。如果你对此也有疑惑,那么这篇文章或许可以帮到你。

我们新建一个UINavigationController将其作为keyWindow的rootViewController,并且里面包含了一个UIViewController,viewController中,我们添加一个y值为0开始布局的testView。

UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 0 ,[UIScreen mainScreen].bounds.size.width , 88)];
testV.backgroundColor = [UIColor redColor];
[self.view addSubview:testV];

会出现两种情况如图:

图1

图2

同样的布局,是什么导致了testView呈现形式不一样呢?

· 答案是navigationBar

iOS的导航控制器会根据导航栏是否会遮挡子控制器的view来决定是否需要将其挪到安全区域显示,什么是安全区域,我下面会讲。
细心的同学们发现图1的导航栏是透明的,那么既然是透明的,导航控制器会认为其是可视的,导航控制器中子控制器view会从状态栏顶部开始布局,注意!!!这里是状态栏顶部而不是导航栏顶部!!
图2的导航栏为不透明的,所以导航控制器会认为其为不可视的,导航控制器中子控制器view从导航栏底部开始布局。

· 什么时候系统会认为导航栏遮住了视图,需要将子控制器的view挪到安全区域展示呢?

以下两种情况,只要满足一个,导航控制器中子控制器view就会从导航栏底部开始布局。

// 将导航栏设置为不透明
self.navigationController.navigationBar.translucent = NO;
// 为导航栏设置背景图片
[self.navigationController.navigationBar setBackgroundImage:[UIImage imageNamed:@"test"] forBarMetrics:UIBarMetricsDefault];

2.safeAreaInsets介绍以及如何利用它适配View布局。

· 那么问题又来了,当我们自己写代码的时候,怎么判断屏幕上显示的view有没有被navigationBar,tabbar,或者是iphoneX系列的操作条给遮挡呢。

· 答案是safeAreaInsets,也就是苹果在iOS11,全面屏手机推出后的一个新的UIView的属性,这个UIEdgeInset类型的属性会告诉我们这个view的上下左右,各被navigationBar,tabbar,或者是iphoneX系列的操作条遮挡了多少,这些被遮挡以外的地方,就是我们所说的安全区域

举个例子:
导航栏没有隐藏且没有设置图片或者不透明,也就是说系统认为导航栏没有产生遮挡

@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UIView *testView;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 0 ,[UIScreen mainScreen].bounds.size.width , 100)];
    testV.backgroundColor = [UIColor redColor];
    [self.view addSubview:testV];
    self.testView = testV;
}
-(void)viewDidLayoutSubviews{
    NSLog(@"self.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.view.safeAreaInsets));
    NSLog(@"self.navigationController.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.navigationController.view.safeAreaInsets));
}

打印结果:

self.view.safeAreaInsets: {88, 0, 34, 0}
self.navigationController.view.safeAreaInsets: {44, 0, 34, 0}

· 通过打印结果我们可以发现,在导航栏没有隐藏且系统认为导航栏不产生遮挡效果的情况下,self.navigationController.view.safeAreaInsets.top = 44,这44就是状态栏的高度,所以我们可以看到,navigationBar是从状态栏的底部开始布局的。
· self.view.safeAreaInsets.top = 88由于不产生遮挡,导航控制内的view会从状态栏的顶部开始布局就像刚开始我们像图1那样,那么view就被遮挡了导航栏高度44 + 状态栏高度44,也就是88;

再举个例子:
导航栏不隐藏并且设置了不透明或者背景图片,也就是说系统认为导航栏产生了遮挡。

@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UIView *testView;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    //这里设置导航栏不透明,让子控制器的view从导航栏底部开始布局。
    self.navigationController.navigationBar.translucent = NO;
    UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 0 ,[UIScreen mainScreen].bounds.size.width , 100)];
    testV.backgroundColor = [UIColor redColor];
    [self.view addSubview:testV];
    self.testView = testV;
}
-(void)viewDidLayoutSubviews{
    NSLog(@"self.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.view.safeAreaInsets));
    NSLog(@"self.navigationController.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.navigationController.view.safeAreaInsets));
}

打印结果:

self.view.safeAreaInsets: {0, 0, 34, 0}
self.navigationController.view.safeAreaInsets: {44, 0, 34, 0}

通过打印结果,我们可以得知当子控制器view从导航栏底部开始布局的时候,不会产生导航栏和状态栏的遮挡,所以子控制器view的safeAreaInsets.top为0;导航控制器由于被状态栏遮挡,所以safeAreaInsets.top还是44。safeAreaInsets.bottom当然就是iphoneX系列的操作条了,高度为34。

3.适配全面屏的新思路

· 那么既然有了safeAreaInsets这个属性之后,我们是不可以直接通过这个属性来适配iOS11后的所有机型适配呢,这样以后不就可以发版适配新的机型了?(因为目前我们都是通过屏幕尺寸来适配全面屏系列的,如果发布了新的尺寸的全面屏机型,是需要重新判断是否为全面屏,然后发版解决适配问题的)

· 我们来看下苹果文档对于safeAreaInsets这个属性的解释:

If the view is not currently installed in a view hierarchy, or is not yet visible onscreen, the edge insets in this property are 0.

· 当一个view没有超出安全区域被遮挡或者view还没有显示在屏幕上的话,view的safeAreaInsets这个属性,是不会计算的,也就是说,返回的是{0,0,0,0}。所以说,在viewDidLoad方法中写布局的话,是取不到safeAreaInsets的值的。

· 那么什么时候获取到最新的safeAreaInsets呢?在safeAreaInsets更新的时候我们根据safeAreaInsets来调整iphoneX的适配,是不是就可以了呢?

· 我们再来看下苹果的文档

Declaration

  • (void)viewSafeAreaInsetsDidChange;
    Discussion
    Use this method to update your interface to accommodate the new safe area. UIKit updates the safe area in response to size changes to system bars or when you modify the additional safe area insets of your view controller. UIKit also calls this method immediately before your view appears onscreen.

· 当safeAreaInset发生变化时,控制器会调用这个方法告诉我们safeAreaInset的值更新了,那我们在这个方法中,对iphoneX进行适配,是不是就可以了呢?

· 来写个demo试一下

@interface ViewController ()<UITableViewDataSource>

/** <#注释#> */
@property (nonatomic, strong) UITableView *tb;

/** <#注释#> */
@property (nonatomic, strong) UIView *testView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];
    self.navigationController.navigationBar.translucent = NO;
    UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 0 ,[UIScreen mainScreen].bounds.size.width , 100)];
    testV.backgroundColor = [UIColor redColor];
    [self.view addSubview:testV];
    self.testView = testV;
}

-(void)viewSafeAreaInsetsDidChange{
    [super viewSafeAreaInsetsDidChange];
    self.testView.frame = CGRectMake(0,self.view.safeAreaInsets.top ,[UIScreen mainScreen].bounds.size.width , 100);
}

图3
· 我们可以看到,testView从导航栏底部,开始布局了,达到了我们的预期效果。

在实际开发中,我们可以在这个方法内,对iOS11之后的所有设备进行适配(这个方法和safeAreaInsets属性iOS11之后才有,我们只需要在此方法内重写需要适配的控件frame就行了)

举个例子:

@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UIView *testView;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    // 这里是按照home键机型写的代码 从导航栏底部开始布局 状态栏20 + 导航栏44 = 64
    UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 64 ,[UIScreen mainScreen].bounds.size.width , 100)];
    testV.backgroundColor = [UIColor redColor];
    [self.view addSubview:testV];
    self.testView = testV;
}

// 全面屏手机统一进入这个方法适配
-(void)viewSafeAreaInsetsDidChange{
    [super viewSafeAreaInsetsDidChange];
    self.testView.frame = CGRectMake(0,self.view.safeAreaInsets.top ,[UIScreen mainScreen].bounds.size.width , 100);
}

· 这个做法和现在目前主流的通过屏幕尺寸判断是否为全面屏手机的方法比确实是麻烦了一些,但是如果苹果发布了新的尺寸iphone,通过尺寸宏就没办法判断了。比如这次的iphoneXR和iphoneX MAX,就非常坑爹,需要将宏扩展后发版,才能解决适配问题。

3. safeAreaInsets是如何影响着scrollView及其子类(tableView)

有了前面1、2两个知识点的储备以后,tableView在navigationController中的适配会简单很多。我们还是直接用代码来说明问题,还是分两种情况:导航栏产生遮挡 和 不产生遮挡

导航栏不遮挡代码:

@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UITableView *tb;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    UITableView *tb = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    tb.dataSource = self;
    [self.view addSubview:tb];
    self.tb = tb;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return 60;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
    return cell;
}

-(void)viewDidLayoutSubviews{
    NSLog(@"self.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.view.safeAreaInsets));
    NSLog(@"self.navigationController.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.navigationController.view.safeAreaInsets));
    NSLog(@"self.tb.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.tb.safeAreaInsets));
    NSLog(@"self.tb.adjustedContentInset: %@",NSStringFromUIEdgeInsets(self.tb.adjustedContentInset));
    NSLog(@"self.tb.contentInset: %@",NSStringFromUIEdgeInsets(self.tb.contentInset));
}

打印结果:

self.view.safeAreaInsets: {88, 0, 34, 0}
self.navigationController.view.safeAreaInsets: {44, 0, 34, 0}
self.tb.safeAreaInsets: {88, 0, 34, 0}
self.tb.adjustedContentInset: {88, 0, 34, 0}
self.tb.contentInset: {0, 0, 0, 0}
图片4

从图4中我们可以看到,即便导航栏没有产生遮挡,tableview从状态栏顶部开始布局,contentInset为0,tablView的的上下inset还是多了88 和 34,这两个数字我相信大家已经很熟悉了,88为状态栏的高度加上导航栏的高度,34为底部操作条的高度。
基于上面的代码我们把tableView的contentInset属性改一下,再来看看效果

tb.contentInset = UIEdgeInsetsMake(100, 0, 100, 0);

打印结果

self.tb.safeAreaInsets: {88, 0, 34, 0}
self.tb.adjustedContentInset: {188, 0, 134, 0}
self.tb.contentInset: {100, 0, 100, 0}

图5

我们可以看到虽然contentInset我们只给了100,但是tableView的content顶部却偏移了188。
由此可见iOS11之后,决定tableView的inset不再是contentInset这个属性,而是adjustedContentInset这个属性。
通过打印结果,我们可以发现adjustedContentInset = contentInset + safeAreaInsets

导航栏遮挡代码:我们基于上面的代码,将导航栏设置成不透明的

self.navigationController.navigationBar.translucent = NO;
tb.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);

打印结果:

self.tb.safeAreaInsets: {0, 0, 34, 0}
self.tb.adjustedContentInset: {0, 0, 134, 0}
self.tb.contentInset: {0, 0, 0, 0}

由于导航栏设置成了不透明,导航控制器中的子控制器View的布局从navigationBar底部开始,所以tableView不存在遮挡的情况,tableView.safeAreaInsets.top也就理所当然的变成了0;
问题这个时候又来了,我们会发现tableView滚到底部的时候,就滚不下去了,下面差了88pt。产生这个问题的原因很简单,因为navigationBar产生了遮挡,子控制器view从导航栏底部布局,但是我们的tableView高度却是self.view.bounds,所以tableView超出了屏幕88pt(导航栏高度+状态栏的高度)。
我们上代码,看一下如何适配。

@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UITableView *tb;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.navigationController.navigationBar.translucent = NO;
    UITableView *tb = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    CGFloat navBarHeight = self.navigationController.navigationBar.frame.size.height;
    CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
    // 给tableView.contentInset加上导航栏高度+状态栏高度 补偿下移超出屏幕的部分
    tb.contentInset = UIEdgeInsetsMake(0, 0, navBarHeight + statusBarHeight , 0);
    tb.dataSource = self;
    [self.view addSubview:tb];
    self.tb = tb;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return 60;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
    return cell;
}

总结

1.导航控制器内的子控制器view的布局由navigationBar setBackgroundImage:navigationBar.translucent决定。
2.利用iOS11的新特性safeAreaInsets以及-(void)viewSafeAreaInsetsDidChange来完成iOS11之后机型的所有适配,免去了判断全面屏宏的设置,将来再出新的机型不用动任何代码也可以完美适配。

如果这篇文章的内容让你有所收获,请记得点赞哟
如有问题,请留言或者直接私信我。今天就先到这了 byebye~

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

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,967评论 3 119
  • 今得一好文,尤今的《包菜与洋葱》。原文道: 朋友悻悻然地说:“她当我是包菜,我把自己变成一个洋葱。” 莞尔之余,追...
    犀牛的草原阅读 455评论 0 0
  • 其实她一直都知道,远方是很远的远方,也是一个很大很大的世界,所以她一直想离开,想去更大更远的世界。 她没读书了,继...
    野傲阅读 257评论 0 0
  • ——一节体育的“处女”课纪实 不经意间,自2003年9月至今,我已经从事教育将近五个...
    我在圆梦路上阅读 878评论 0 3