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~

推荐阅读更多精彩内容

  •    文章导读:文章较长,若你已对iOS11和iPhone X适配有基本了解,请直接阅读第三部分解决方案。拓展阅读...
    择势量投阅读 5,775评论 0 34
  • 今得一好文,尤今的《包菜与洋葱》。原文道: 朋友悻悻然地说:“她当我是包菜,我把自己变成一个洋葱。” 莞尔之余,追...
    犀牛的草原阅读 55评论 0 0
  • 其实她一直都知道,远方是很远的远方,也是一个很大很大的世界,所以她一直想离开,想去更大更远的世界。 她没读书了,继...
    野傲阅读 42评论 0 0
  • ——一节体育的“处女”课纪实 不经意间,自2003年9月至今,我已经从事教育将近五个...
    我在圆梦路上阅读 465评论 0 3