iOS 11 safeArea详解 & iphoneX 适配

2017 年 10 月 11 日 CocoaChina fruitymoon


最近看了许多iPhone X适配的文章,发现很少有介绍safeArea的,就来随便写写


现在对于iPhone X的适配,有一种常见的做法是给导航栏或tabbar增加一个固定的距离,比如顶部增加44pt,底部增加34pt。这种写死距离的做法乍看上去挺简单,其实并不好,理由如下


  1. 不适合多机型的适配,如果以后出了一种带刘海的iPad,需要预留出来的距离就未必是现在写死的距离

  2. 不适合需要支持横竖屏的app,横屏顶部和底部都不需要增加距离,反而是左右各有44pt

  3. 不够动态。还是举个例子,假如有电话打进来了,导航栏应该会下移,这时候view可能还是会被挡住


这里我想探讨一下如何使用safeAreaLayoutGuide和safeAreaInsets,以一种动态的方式,一劳永逸地解决iPhone X甚至后续所有机型的适配问题。


safeAreaLayoutGuide


首先我们看看什么是safeAreaLayoutGuide



看起来复杂,其实很简单,我归纳一下有几点:


  1. 它是UIView的一个只读属性,意味着所有UIView对象都有并且是系统帮我们创建好的

  2. 它继承UILayoutGuide,有layoutFrame意味着它能代表一块区域

  3. 它代表的区域避开了诸如导航栏、tabbar或者其他有可能挡住你这个UIView对象显示的所有父view,意味着你的view对象只要相对另一个view的safeLayoutGuide做布局就不用担心她被奇奇怪怪的东西挡住

  4. 对于控制器的view的safeAreaLayoutGuide,他的区域同样避开了statusbar或其他有可能挡住view显示的东西,我们甚至可以用控制器的additionalSafeAreaInsets属性,来额外指定inset

  5. 如果view完全在父view的安全区域内,或者view不在视图层级或屏幕上,那么view的safeAreaLayoutGuide区域其实和view自身是一样大的


safeAreaLayoutGuide是一个相对抽象的概念,为了便于理解,我们可以把safeAreaLayoutGuide看成是一个“view”,这个“view”系统自动帮我们调整它的bounds,让它不会被各种奇奇怪怪的东西挡住,包括iPhone X的刘海区域和底部的一道杠区域,可以认为在这个“view”上一定能完整显示所有内容。


以下绿色部分就是当前控制器view的safeAreaLayoutGuide区域


iphone X竖屏safeAreaLayoutGuide的bounds.png


iPhone X横屏safeAreaLayoutGuide的bounds.png


截图来自https://developer.apple.com/videos/play/fall2017/801/


不过需要铭记的一点是这个“view”并不会显示在我们的视图层级上。


UILayoutGuides will not show up in the view hierarchy, but may be used as items in an NSLayoutConstraint and represent a rectangle in the layout engine.


在我看来,他最大的作用是作为参照物,让view可以相对某个view的safeAreaLayoutGuide做布局,从而保证view能正常、安全地显示(相对的那个view不一定要是父view)


在一种常见的使用场景里,以前我的某个view是相对于控制器的view做布局,现在是相对控制器view的safeAreaLayoutGuide做布局了


以前是这样写


[NSLayoutConstraint constraintWithItem:someView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.vc.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];


现在是这样


[NSLayoutConstraint constraintWithItem:someView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.vc.view.safeAreaLayoutGuide attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];


适配前后的效果


适配前.png


改成相对于view的safeAreaLayoutGuide后-竖屏.png


改成相对于view的safeAreaLayoutGuide后-横屏.png


可以看到,相同的布局下,横屏在没有statusbar时,距离顶部是0,左边是44,如果有statusbar,距离顶部就是20。反正不管怎么弄,只要我们相对safeAreaLayoutGuide做布局,我们的view就能够安全完整地显示出来


那么非iOS11怎么办?


非iOS11 还是只能对view做布局,就要写两套布局代码,稍后会介绍


这样是不是就足够应对所有情况了呢?


并不是


  1. 我们自定义的view有一边需要紧挨着屏幕边缘,比如我项目里是自定义的导航栏,它的顶部是挨着屏幕顶部的,那么导航栏就不能相对view的safeAreaLayoutGuide布局,否则顶部会空出来一截子

  2. frame布局


这时就轮到safeAreaInsets来发挥作用啦


safeAreaInsets


先看一下safeAreaInsets的官方解释



有没有觉得和safeAreaLayoutGuide很像?safeAreaLayoutGuide可能就是根据safeAreaInsets来调整自己的bounds的


iPhone X竖屏时占满整个屏幕的控制器的view的safeAreaInsets是(44,0,34,0),横屏是(0,44,21,44),inset后的区域正好是safeAreaLayoutGuide区域


既然如此,对于自定义的顶部导航栏来说,我们可以给导航栏的高度加上一个vc.view.safeAreaInsets.top,让他变高一点就可以了,这样在X上,竖屏时top = 44, 横屏时top = 0,导航栏的高度能响应改变


需要注意的是,无论safeAreaLayoutGuide还是safeAreaInsets都是iOS11才能使用的。


对于safeAreaInsets,我们可以把版本判断写在一个函数里


我们可以这样写


static inline UIEdgeInsets sgm_safeAreaInset(UIView *view) {

    if (@available(iOS 11.0, *)) {

        return view.safeAreaInsets;

    }

    return UIEdgeInsetsZero;

}


UIEdgeInsets safeAreaInsets = sgm_safeAreaInset(self.view);

CGFloat height = kDefaultTopViewHeight; // 导航栏原本的高度,通常是44.0

height += safeAreaInsets.top > 0 ? safeAreaInsets.top : 20.0; // 20.0是statusbar的高度


问题又来了,这段代码放在什么地方合适呢?前面官方文档提到过,如果view不在屏幕上或显示层级里,view的safeAreaInsets = UIEdgeInsetsZero,所以我们需要明确知道safeAreaInsets改变的时机


实际上系统已经提供了回调


对于UIViewController


-(void)viewSafeAreaInsetsDidChange NS_REQUIRES_SUPER API_AVAILABLE(ios(11.0), tvos(11.0));


对于UIView


-(void)safeAreaInsetsDidChange API_AVAILABLE(ios(11.0),tvos(11.0));


这里主要探讨controller的回调,view的回调是类似的。只要controller的view的safeAreaInsets改变,系统就会调用viewSafeAreaInsetsDidChange。自然而然,我们会想把以上代码放在这里,然而这里有个大坑,你会发现,当这个控制器以动画的方式push进来时,导航栏的高度也会动画地变高,产生了不必要的多余动画,这种体验很糟糕


那么究竟应该放在哪里?我们很有必要看一下新的viewController调用时序


以下是从“rootVC” push 到 “pushVC”控制台输出的调用时序以及对应控制器的view的safeAreaInsets


2017-10-04 16:59:59.594811+0800 XXX[15662:803767] Begin pushViewController to [<_TtCC8XXXTests27ContainerViewControllerTest20MockUIViewController: 0x7f9c07b643b0>]

viewDidLoad()---Optional("pushVC")---UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)

willMove(toParentViewController:)---Optional("pushVC")---UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)

viewWillDisappear---Optional("rootVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

viewWillAppear---Optional("pushVC")---UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)

viewSafeAreaInsetsDidChange()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

viewWillLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

viewDidLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

viewWillLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

viewDidLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

viewDidAppear---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

viewDidDisappear---Optional("rootVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

didMove(toParentViewController:)---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

2017-10-04 16:59:59.604563+0800 XXX[15662:803767] Did PushViewController [<_TtCC8XXXTests27ContainerViewControllerTest20MockUIViewController: 0x7f9c0790d170>]->[<_TtCC8XXXTests27ContainerViewControllerTest20MockUIViewController: 0x7f9c07b643b0>] time = [0.009772]


可以看到,viewSafeAreaInsetsDidChange调用时机很早,在viewWillAppear后,这是为什么出现多余动画的原因。并且“pushVC”的safeAreaInsets直到viewSafeAreaInsetsDidChange调用前,都是UIEdgeInsetsZero,之后才是正确的UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)


并且viewSafeAreaInsetsDidChange后面会调用两次viewDidLayoutSubviews,所以我们应该把改变高度或布局的代码都写在viewDidLayoutSubviews里,这样就不会有多余的动画效果了。需要注意viewDidLayoutSubviews可能会由别的操作频繁触发,所以如果调整safeArea布局的代码比较耗时,可以考虑加上一个状态标记,只在didChange后执行一次布局调整


最后的代码应该长这样


- (void)viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

    UIEdgeInsets safeAreaInsets = sgm_safeAreaInset(self.view);

    CGFloat height = 44.0; // 导航栏原本的高度,通常是44.0

    height += safeAreaInsets.top > 0 ? safeAreaInsets.top : 20.0; // 20.0是statusbar的高度,这里假设statusbar不消失

    if (_navigationbar && _navigationbar.height != height) {

        _navigationbar.height = height;

    }


适配前后的效果


适配前


适配后


这样对于frame布局和autolayout布局的各种情况,有了一个动态的适配方案,就是分别使用safeAreaLayoutGuide和safeAreaInsets来灵活处理布局,相比写死一个固定距离,当前和未来的各种机型都能一套代码适配,扩展性更好。我们项目目前也是采用这种做法,如果你的项目需要适配横竖屏或UI控件布局相对复杂,真的应该考虑使用safeArea


顺便提一下,VFL似乎已经废了,因为|只能表示父view的边缘,并没有一个符号来表示父view的safeAreaLayoutGuide的边缘,以前我们写的VFL代码,好多得改,改起来也特别麻烦,建议别再用VFL了


最后一个版本判断的问题,safeAreaInsets和safeAreaLayoutGuide都是iOS11的API,如果不做封装,直接在代码里写,势必会出现大量@available这种版本判断语句,代码里到处是@available,看起来很崩溃,破坏代码可读性。


因为我之前写了一个自动布局框架,这次就将safeAreaLayoutGuide和版本判断都顺便封装在里面了,个人觉得这套框架比NSLayoutAnchor好用,主要作用是简化布局代码书写,以下是生成一个NSLayoutConstraint的对比


// 需求是topLeftView的top等于self.view的safeAreaLayoutGuide的top

// 使用系统API

if (@available(iOS 11.0, *)) {

        [NSLayoutConstraint constraintWithItem:self.topLeftView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view.safeAreaLayoutGuide attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];

    } else {

        [NSLayoutConstraint constraintWithItem:self.topLeftView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];

    }

// 使用NSLayoutConstraint-SSLayout

self.topLeftView.top_attr = self.view.top_attr_safe


在业务代码里不会出现任何版本判断,大家有兴趣的话可以试一下,哈哈


传送门:http://www.jianshu.com/p/c044f3de564a


登录查看更多
0

相关内容

iPhone 由苹果公司(Apple, Inc.)首席执行官史蒂夫·乔布斯在 2007 年 1 月 9 日举行的 Macworld 宣布推出,2007 年 6 月 29 日在美国上市,将创新的移动电话、可触摸宽屏 iPod 以及具有桌面级电子邮件、网页浏览、搜索和地图功能的突破性因特网通信设备这三种产品完美地融。
【芝加哥大学】可变形的风格转移,Deformable Style Transfer
专知会员服务
31+阅读 · 2020年3月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
226+阅读 · 2020年3月22日
自回归模型:PixelCNN
专知会员服务
27+阅读 · 2020年3月21日
Gartner:2020年十大战略性技术趋势, 47页pdf
专知会员服务
78+阅读 · 2020年3月10日
姿势服装随心换-CVPR2019
专知会员服务
36+阅读 · 2020年1月26日
专知会员服务
237+阅读 · 2020年1月23日
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
iOS自定义带动画效果的模态框
CocoaChina
7+阅读 · 2019年3月3日
Android P正式发布,你需要尽快做适配了
前端之巅
3+阅读 · 2018年8月7日
强化学习——蒙特卡洛方法介绍
论智
12+阅读 · 2018年6月3日
已删除
生物探索
3+阅读 · 2018年2月10日
百度的广告和今日头条的广告
keso怎么看
8+阅读 · 2018年2月9日
免费云真机测试 | 让您的应用完美适配 Android Oreo
引力空间站
3+阅读 · 2018年2月2日
【直观详解】支持向量机SVM
机器学习研究会
18+阅读 · 2017年11月8日
Arxiv
20+阅读 · 2020年6月8日
Conceptualize and Infer User Needs in E-commerce
Arxiv
3+阅读 · 2019年10月8日
Arxiv
5+阅读 · 2019年4月8日
Arxiv
5+阅读 · 2019年2月28日
Arxiv
4+阅读 · 2018年5月4日
Arxiv
4+阅读 · 2018年3月14日
Arxiv
6+阅读 · 2018年2月7日
VIP会员
相关VIP内容
【芝加哥大学】可变形的风格转移,Deformable Style Transfer
专知会员服务
31+阅读 · 2020年3月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
226+阅读 · 2020年3月22日
自回归模型:PixelCNN
专知会员服务
27+阅读 · 2020年3月21日
Gartner:2020年十大战略性技术趋势, 47页pdf
专知会员服务
78+阅读 · 2020年3月10日
姿势服装随心换-CVPR2019
专知会员服务
36+阅读 · 2020年1月26日
专知会员服务
237+阅读 · 2020年1月23日
相关资讯
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
iOS自定义带动画效果的模态框
CocoaChina
7+阅读 · 2019年3月3日
Android P正式发布,你需要尽快做适配了
前端之巅
3+阅读 · 2018年8月7日
强化学习——蒙特卡洛方法介绍
论智
12+阅读 · 2018年6月3日
已删除
生物探索
3+阅读 · 2018年2月10日
百度的广告和今日头条的广告
keso怎么看
8+阅读 · 2018年2月9日
免费云真机测试 | 让您的应用完美适配 Android Oreo
引力空间站
3+阅读 · 2018年2月2日
【直观详解】支持向量机SVM
机器学习研究会
18+阅读 · 2017年11月8日
相关论文
Arxiv
20+阅读 · 2020年6月8日
Conceptualize and Infer User Needs in E-commerce
Arxiv
3+阅读 · 2019年10月8日
Arxiv
5+阅读 · 2019年4月8日
Arxiv
5+阅读 · 2019年2月28日
Arxiv
4+阅读 · 2018年5月4日
Arxiv
4+阅读 · 2018年3月14日
Arxiv
6+阅读 · 2018年2月7日
Top
微信扫码咨询专知VIP会员