iOS 11 safeArea详解及iphoneX 适配

最近看了许多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做布局,就要写两套布局代码,稍后会介绍

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

并不是

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

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈Xcode9 和iOS11适配和特性

    今天升级了Xcode9 刚才写了一篇 爱劈叉的齐刘海 现在说说新的东西把,有些简直不能再恶心了但有些简直不能再贴心 首先是跳转, 之前按住Command + 左键 就可以跳转了;然而今天我发现 除了这个: Jump to Definition(^⌘):跳转类头文件或定义 Show Quick Help(⌥):显示帮助文档 Edit All in Scope:编辑文档内所有匹配内容 在这里我要说,对于懒得不行的我,简直要吐,多了一步操作 效率降低很多的好吗? 那么好,你试试 Command + 右

  • 详解iOS11、iPhone X、Xcode9 适配指南

    更新iOS11后,发现有些地方需要做适配,整理后按照优先级分为以下三类: 单纯升级iOS11后造成的变化: Xcode9 打包后造成的变化: iPhoneX的适配 一.单纯升级iOS11后造成的变化 升级后,发现某个拥有tableView的界面错乱,组间距和contentInset错乱,因为iOS11中 UIViewController 的 automaticallyAdjustsScrollViewInsets 属性被废弃了,因此当tableView超出安全区域时,系统自动会调整SafeAre

  • 关于适配iOS11和iPhoneX的一些事

    前言 众所周知iOS11正式版终于来了,最近也把app适配了一下,其实也不是很麻烦,来看看我做的一些操作,话不多说了,来一起看看吧. 1.UITableView.UICollectionView的变化 tableView在iOS11默认使用Self-Sizing,tableView的estimatedRowHeight.estimatedSectionHeaderHeight. estimatedSectionFooterHeight三个高度估算属性由默认的0变成了UITableViewAuto

  • Xcode 9下适配iPhoneX导致iOS 10不兼容问题的解决方法

    前言 前久发现测试组提交来一个 bug,说有的布局在 iOS 11 上正常,在 iOS 10 下不正常.分别在 iOS 11 模拟器和 iOS 10.3 模拟器上跑了一下 app,发现果然如此,如下图所示: iOS 11 下点击"省市广播站",下级菜单中的按钮正常显示: iOS 10 下点击"省市广播站",下级菜单显示为空白: 解决方法 检查代码,发现下级菜单中的按钮是以手动布局方式动态添加到一个 scroll view 中的.添加时指定了按钮的框架,框架计算完全正

  • iOS11和iPhoneX适配的一些坑

    本文转载于:http://www.cocoachina.com/ios/20170921/20623.html 导航栏 导航栏高度的变化 iOS11之前导航栏默认高度为64pt(这里高度指statusBar + NavigationBar),iOS11之后如果设置了prefersLargeTitles = YES则为96pt,默认情况下还是64pt,但在iPhoneX上由于刘海的出现statusBar由以前的20pt变成了44pt,所以iPhoneX上高度变为88pt,如果项目里隐藏了导航栏加了

  • iPhoneX无导航栏页面适配问题解决方案

    原全屏适配在iPhoneX会由于安全区域的变化导致显示不全. 解决方案如下: 在self.view上添加一个view,剩下的视图基于该view布局,view的约束随self.view.safeAreaInsets的改变需要重置: - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.view.backgroundColor = [UIColor blac

  • iOS11&iPhoneX适配&Xcode9打包注意事项

    1,适配UITableView if#available(iOS11.0, *) { self.contentInsetAdjustmentBehavior= .never self.estimatedRowHeight=0 self.estimatedSectionHeaderHeight=0 self.estimatedSectionFooterHeight=0 }else{ } 2,适配UIScrollView if#available(iOS11.0, *) { scrollView?.

  • iPhoneX 各种适配记录笔记(超全面)

    前言 与以往的iPhone不同,这次iPhone X用上了时下流行的全面屏设计,屏幕的分辨率和比例都是苹果首次采用,而且还有个"别致的刘海",这就需要现有的APP为iPhone X重新作适配了. 所以iPhone X 的到来,惊艳的是果粉,苦逼的是程序猿.今天升级到Xcode9.0,运行项目,所谓的全屏 iPhone X,but 页面好像也没有全屏,于是根据之前的适配经验,总算初步解决了这个问题,记录如下,以备后需. App 页面适配适配前 适配前 问题:App 未全屏显示 解决办法

  • iOS 11 safeArea详解及iphoneX 适配

    最近看了许多iPhone X适配的文章,发现很少有介绍safeArea的,就来随便写写 现在对于iPhone X的适配,有一种常见的做法是给导航栏或tabbar增加一个固定的距离,比如顶部增加44pt,底部增加34pt.这种写死距离的做法乍看上去挺简单,其实并不好,理由如下 不适合多机型的适配,如果以后出了一种带刘海的iPad,需要预留出来的距离就未必是现在写死的距离 不适合需要支持横竖屏的app,横屏顶部不需要增加距离,反而是左右各有44pt,底部的距离也和竖屏不同 不够动态.还是举个例子,假

  • ios 11和iphone x的相关适配问题及解决方法

    有关iOS11 ,最大的变化就是增加了一个安全区域(safeArea)的概念,iOS11 适配的大部分问题都是由于它引起的. 在ios 11中,tableView会莫名偏移,解决办法: //解决iOS11 tableview会出现漂移,预估高度都设为0 self.tableView.estimatedRowHeight = 0; self.tableView.estimatedSectionHeaderHeight = 0; self.tableView.estimatedSectionFoot

  • IOS开发 UIAlertController详解及实例代码

     IOS开发 UIAlertController详解 在iOS 8.0后,苹果弃用了UIAlertView和UIActionSheet,转而使用UIAlertController把之前的UIAlertView和UIActionSheet整合在一起.新版的API变得简洁了不少几行代码就可实现之前一大片代码的功能 UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"My Alert" messag

  • IOS 中UIApplication详解及实例

    IOS 中UIApplication详解及实例 以前刚学iPhone开发时,觉得UIApplication这个东西特NB,特神秘,比如它居然能打开一个URL,而且还是用一个很神秘的方法得到实例: [UIApplication sharedApplication] 它对我的神秘感一直保持到今天下午.今天下午负责UI设计的同事在设计,我没有素材,比较清闲,于是发个狠,专门看了一下UIApplication这个类.果然是难者不会,会者不难,看完之后,这个类的神秘感一下子没了.下面让我来揭开它的神秘面纱

  • iOS runtime forwardInvocation详解及整理

     iOS runtime forwardInvocation详解 代码: TestModel - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if(aSelector == @selector(testMethod)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return nil; } -(void)forwardInv

  • IOS接入facebook详解

    facebook 接入 1.在 facebook 后台 配置 ios 平台, 获取 fb appid store id 可以先随便找一个已存在的填入进去. 2.CocoaPods 引入几个核心库 pod 'FBSDKCoreKit', '~> 9.0.1' pod 'FBSDKLoginKit', '~> 9.0.1' pod 'FBSDKShareKit', '~> 9.0.1' pod 工程构建出 静态库丢到 /Users/XXX/Library/Developer/Xcode/De

  • Flutter Module添加到iOS项目示例详解

    目录 1. 创建flutter module 2. flutter 模块嵌入原生应用 3. flutter模块和原生通信 小结 1. 创建flutter module 摘要:我们实际开发开始作为混合开发,可能会把一个模块使用flutter开发,之后嵌入到iOS项目中.比如说我们的商城模块使用flutter开发,这样android和iOS都可以使用. 我们通常会把iOS项目和 flutter module在同一目录,我们创建一个商城的module flutter create --template

  • React-Native 桥接iOS原生开发详解

    react-native的文档的原生模块中可以看到清洗的代码 传送门 接下来先说一下笔者的要实现的功能: 在iOS原生代码中集成高德导航,在RN中用JS去调用原声代码,并进行跳转, 接下来是笔者遇到的问题与不好理解的地方,写出来跟大家分享让大家少走弯路. 刚开始也是一头雾水且查资料也是到处都是但是都没有解决问题. iOS原生写法 在iOS中创建类继承NSObject(OC语言). //类的.h文件 #import <Foundation/Foundation.h> #import <Re

  • 详解Android版本适配:9.0 Pie

    一.前言 本文主要是从官方文档中筛选出一些常见的适配项,若有任何纰漏或需要补充的,欢迎大家在评论区指出. 二.版本适配 1. 限制 HTTP 网络请求 Android 9.0 中限制了 HTTP(明文传输)网络请求,若仍继续使用HTTP请求,则会在日志中提示以下异常(只是无法正常发出请求,不会导致应用崩溃): java.net.UnknownServiceException: CLEARTEXT communication to xxx not permitted by network secu

  • 一道值得深入思考的iOS面试题详解

    前言 最近在群里看到有人发的一道面试题,题目如下: @interface Spark : NSObject @property(nonatomic,copy) NSString *name; @end @implementation Spark - (void)speak { NSLog(@"My name is:%@",self.name); } @end @implementation ViewController - (void)viewDidLoad { [super view

随机推荐