本文是针对WWDC2018 Session234:UIKit:Apps for Every Size and Shape快速适配所有型号的iOS移动设备介绍的理解。
Safe area and layout margins(安全区域和布局边距)
如何在应用程序中使用这个属性适应各种屏幕的尺寸和形状呢?
Safe Area: 安全区域,这个概念在iOS11中被提出,安全区域帮助我们将view放置在整个屏幕的可视区域内,保证view不被系统的状态栏、导航栏或tabbar等覆盖,这个概念的提出主要是为了适配像iphoneX一样的全面屏。
我们可以通过UIView的safeAreaInsets来获取安全区域的UIEdgeInsets。如果你使用Auto Layout进行自动布局,你可以使用safeAreaLayoutGuide来确定安全区域,同时安全区域限制了视图的可见部分,如图1所示。
安全区域是如何从父视图传递到子视图的呢?
父视图A的安全区域如下图2所示。
接下来,我们在视图A上添加一个子视图B,视图B的约束不依赖于视图A的safeAreaLayoutGuide,视图B的左右和底部都超出了父视图的安全区域,如图3所示。
然后在视图B上添加子视图C,并设置视图C的约束依赖于视图B的 safeAreaLayoutGuide,结果视图C的可视范围会被限制在图4的黄色区域内,由此看出父视图的安全区域会向上传递。
如何扩展安全区域?
通过使用UIViewController的.additionalSafeAreaInsets属性,可以自定义扩展安全区域的大小,并且可以通过viewSafeAreaInsetsDidChange()方法,获取此时视图的安全区域。
例如,如图5,竖屏时,视图本身的安全区域UIEdgeInsets为 (top = 88, left = 0, bottom = 83, right = 0),再设置视图的additionalSafeAreaInsets为(50,50,50,50),结果视图的安全区域就变为了(top = 138, left = 50, bottom = 133, right = 50)黄色区块表示。
UIView同样提供了 safeAreaInsetsDidChange()方法用于获取安全区域的UIEdgeInsets。
布局边距layoutMargins
iOS8 中提出了 layoutMargins的概念,使用layoutMargins可以获取和设置子控件显示内容距离父控件的边距。在 iOS11 中,新增了directionalLayoutMargins属性来指定边距。这两个属性的结构定义如下:
typedef struct UIEdgeInsets {
CGFloat top, left, bottom, right;
} UIEdgeInsets;
typedef struct NSDirectionalEdgeInsets {
CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets API_AVAILABLE(ios(11.0),tvos(11.0),watchos(4.0));
从定义上看,区别在于将UIEdgeInsets的left和right调整为NSDirectionalEdgeInsets的leading和trailing。这一调整主要是为了Right To Left(RTL)语言下可以进行自动适配,例如:要实现文本每行尾部边距设置为30px,在以前做法则需要判断语言来区分哪些是RTL语言,然后再做设置,iOS11后,可以一步到位。默认情况下,layoutMargin到各边的距离是8个点。通过在 Interface Builder里面勾选 Constrain to margins,它会根据版本在 iOS11 及以上的系统中自动使用 directionalLayoutMargins。通过viewLayoutMarginsDidChange()方法获取当前视图的layoutMargin。
安全区域和布局边距协同作用
正常情况下子视图的布局边距会依赖父视图的安全区域,但是当设置了insetsLayoutMarginsFromSafeArea = false之后,子视图可以达到突破父视图安全区域的布局效果,如 图7、8 所示。
need-to-insert-img
图7
need-to-insert-img
图8
布局之子视图传播
当一个视图的preservesSuperviewLayoutMargins属性为 true 时,在对它的子视图进行布局时,父视图的 margin 也会被考虑在内。如果存在一个子视图的 frame 刚好和父视图的 margin 表示区域有重合,此时设置 preservesSuperviewLayoutMargins为 true ,则子视图会被刚好限制在父视图的 margin 内,如 图9、10 所示。
need-to-insert-img
图9
need-to-insert-img
图10
最小边距
UIViewController存在属性 systemMinimumLayoutMargins,可以对其进行重写,默认情况下 view 的布局边距会受这个属性的返回值制约。如下重写了该属性,则 view 的边距最小会为 [20,20,20,20]。
overridevarsystemMinimumLayoutMargins:NSDirectionalEdgeInsets{returnNSDirectionalEdgeInsets(top:20, leading:20, bottom:20, trailing:20)}
若设置 viewRespectsSystemMinimumLayoutMargins为 false,则 view 布局边距不受 systemMinimumLayoutMargins属性的影响,默认为 [8,8,8,8]。
Scroll views
adjustedContentInset
iOS11 中提出了 UIScrollView的新属性 adjustedContentInset,它的值等于 UIScrollView原有的 contentInset加上 安全区域等 system inset,如 图11 所示。
adjustedContentInset = contentInset + system inset
need-to-insert-img
图11
废弃 Automatic Content Inset
本 Session 再次提到 iOS11 之后废除了原有的 UIViewController属性 automaticallyAdjustsScrollViewInsets取而代之的是 UIScrollView的新增枚举 ContentInsetAdjustmentBehavior,该枚举结构如下:
publicenumContentInsetAdjustmentBehavior:Int{caseautomaticcasescrollableAxescasenevercasealways }
如下图如果设置枚举值为 .always,则默认情况下 scrollView的 adjustedContentInset就等于 safeAreaInsets,即可视区域不会被 navigationBar和 tabBar遮挡,如 图12 所示。
need-to-insert-img
图12
如果将枚举值设置为 .scrollableAxes,则在可以滚动的方向上,或者设置了 alwaysBounceHorizontal/Vertical为 true 的时候,Inset 才会生效。如 图14,页面内容比较少的时候,垂直方向上 scrollView不可滚动,导致文本标题部分被 navigationBar遮挡,如 图13、14 所示。
need-to-insert-img
图13
need-to-insert-img
图14
系统默认设置的枚举值是 .automatic, 这个枚举值基本和 .scrollableAxes表现一致,但唯一不同的是它还秉承了原来 automaticallyAdjustsScrollViewInsets = true的特性,在有 navigationBar且 isTranslucent为 true 时,即使垂直方向上不能够滚动,依然能够调整 Inset 使内容可见,如 图15 所示。
图15
如果将枚举值设置为 .nerver,则 scrollView的 Inset 不会受 safeAreaInserts的影响而改变、如 图16 所示。
图16
编写自适应的应用程序
隐藏 status bar
若果在一些场景下需要隐藏 status bar,我们一般会这么做:
classArticleViewController:UIViewController{overridevarprefersStatusBarHidden:Bool{returntrue} }
不幸的是在 iPhone X 上面这么写是无效的,在 iPhone X 上面只有在隐藏了 navigationBar的前提下,上面这段代码才会生效,所以苹果官方给出的建议是同时隐藏 navigationBar和 status bar,如 图17 所示。
need-to-insert-img
图17
readableContentGuide & cellLayoutMarginsFollowreadableWidth
iOS9 就提出了 readableContentGuide这一概念,主要是用于一些阅读类应用,在可视宽度较大的时候,希望能够通过布局将阅读区域限定在一定范围,已缓解用户阅读的过程中追踪内容移动头部所造成的疲劳。readableContentGuide的间距大小会随着字体大小、设备不同等因素而发生改变。现这一属性同样兼容 safeAreaInsets。同样的 UITableView的 cellLayoutMarginsFollowreadableWidth属性也同样兼容 safeAreaInsets,如 图18、19、20 所示。
need-to-insert-img
图18
need-to-insert-img
图19
need-to-insert-img
图20
insetsContentViewToSafeArea
UITableView在iOS11开始添加了一个新的属性insetsContentViewsToSafeArea,该属性能够控制 TableViewCell的 ContentView是否被 safeAreaInsets所影响,如 图20、21 所示。
need-to-insert-img
图21
need-to-insert-img
图22
底部按钮布局最佳实践
iPhone X 之后,我们在开发过程中经常会遇到如何布局底部按钮的问题。在本 Session 中官方给出了一种方案,例如设置按钮距底部相对于 superView 的约束为16,约束的 Priority为 999,同时设置按钮底部相对于 safeAreaLayoutGuide的约束值为大于等于 0。即可实现按钮在 iphone X 和 其他设备上的不同布局,如 图23 所示。
图23
总结
其实本 Session 并没有提出任何新的属性和方法,最新的属性在 iOS11 SDK 中就已经提出来了。可能很多开发者,在适配iPhone X 的时候遇到的问题也都解决的差不多了。但个人认为这个 Session 还是很有必要的,它将现有的用于适配开发的 UIKit SDK 进行了归纳总结,这将有助于开发者进一步了解这些属性之间的关联关系对快速适配多种尺寸设备的项目开发会有很大帮助。