iOS11适配-Safe Area

在iOS 7中,苹果介绍了UIViewController中的topLayoutGuide和bottomLayoutGuide这两个属性,用来描述一个不被任何内容遮挡的屏幕区域,比如说顶部状态栏status bar、导航栏navigation bar、工具栏toolbar、菜单栏tab bar等。在IOS 11,苹果弃用了这些属性而启用safe area。苹果建议我们不要把任何交互放在safe area之外,从iOS 11开始,我们在开发iOS应用的时候,需要进行视图布局时,必须使用新的safe area API。
当我们做iPhone X适配或者去试着支持safe area时,会发现UIKit中的很多类里都有新的safe area(安全区)特性。本文的目的就是对他们进行概括和介绍。

目录

本文分为以下几个部分。

  • UIView
  • UIViewController
  • UIScrollView
  • UITableView
  • UICollectionView

这些都是具有安全区属性和方法的类。

示例

可以在Github中找到示例代码,阅读本文时可以同步运行。

UIView

在iOS 11,UIViewController中的UIView的topLayoutGuide和bottomLayoutGuide被替换成了新的安全区属性。

@available(iOS 11.0, *)
open var safeAreaInsets: UIEdgeInsets { get }

@available(iOS 11.0, *)
open var safeAreaLayoutGuide: UILayoutGuide { get }

safeAreaInsets属性意味着屏幕可以被任何方向遮挡,并不只是上下,当iPhone X出现时,我们就明白了为什么我们需要对左右两边也进行缩进。
iPhone 8 vs iPhone X safe area (portrait orientation)
iPhone 8 vs iPhone X safe area (landscape orientation)
可以看到,iPhone X在纵向时有上、下的safe area缩进,而在横向时有左、右、下的缩进。


看一下示例中的情况,我们在一个控制器的view中添加了两个子视图,它们分别包含一个label和一段特定的高度,然后,让他们抵住了view的顶部和底部,也都贴到了view的边缘。
Subviews are attached to the view’s edges
我们看到子视图的内容和顶部的刘海、底部的home指示器重叠了。要正确布局子视图的位置,我们可以使用手动布局将它们附加到safe area。

topSubview.frame.origin.x = view.safeAreaInsets.left
topSubview.frame.origin.y = view.safeAreaInsets.top
topSubview.frame.size.width = view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right
topSubview.frame.size.height = 300

或者使用自动布局

bottomSubview.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
bottomSubview.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
bottomSubview.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
bottomSubview.heightAnchor.constraint(equalToConstant: 300).isActive = true

Subviews are attached to the superview safe area
看起来好点了。此外还可以在子视图的类里直接设置子视图上的孙视图的位置,将孙视图放在子视图的safe area里。

label.frame = safeAreaLayoutGuide.layoutFrame

或者使用自动布局

label.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor).isActive = true
label.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor).isActive = true

子视图填满了view,而label放在了它的父视图的安全区
这下就厉害了,我们不仅在控制器里可以将view放在安全区,还可以在任意一层子视图结构中,将view放在其父控件的安全区。这样一来,我们可以在保证文本不被刘海和home指示器遮挡的情况下,还能自定义安全区外部分的颜色或者图片背景。

UIViewController

在iOS 11,UIViewController有了一个新属性(额外的安全区缩进):

@available(iOS 11.0, *)
open var additionalSafeAreaInsets: UIEdgeInsets

当ViewController包含其他嵌入的子ViewController时,将使用这个属性。比如说苹果会在UINavigationController和UITabBarController中当它们各自的bar为半透明时使用这个属性。
导航栏和菜单栏为半透明时的安全区。view的子视图贴在view的边缘,label在子视图的安全区
效果还不错,但是当状态栏隐藏时,奇怪的事情发生了。
导航栏和菜单栏为半透明而且状态栏为隐藏时的安全区。view的子视图贴在view的边缘,label在子视图的安全区
其他安全区的缩进尺寸都在预料中,但是导航栏向上移动到了刘海下面。这是个棘手的bug,而且除了在Stack Overflow上找到的一个还好的方法之外,目前没找到更好的方法去解决。
当我们修改additional safe area insets属性或者safe area insets被系统修改时,我们可以通过UIViewController或者UIView中特定的方法来监听。

// UIView
@available(iOS 11.0, *)
open func safeAreaInsetsDidChange()

//UIViewController
@available(iOS 11.0, *)
open func viewSafeAreaInsetsDidChange()

模拟iPhone X的安全区

额外的安全区缩进也可以用于,当你没有iPhone X真机或者不方便用iPhone X的模拟器时,测试你的app是否支持iPhone X。比如像示例代码里一样,通过实时修改缩进值,观察安全区的变化。

//portrait orientation, status bar is shown
additionalSafeAreaInsets.top = 24.0
additionalSafeAreaInsets.bottom = 34.0

//portrait orientation, status bar is hidden
additionalSafeAreaInsets.top = 44.0
additionalSafeAreaInsets.bottom = 34.0

//landscape orientation
additionalSafeAreaInsets.left = 44.0
additionalSafeAreaInsets.bottom = 21.0
additionalSafeAreaInsets.right = 44.0

模拟iPhone X上的安全区缩进

UIScrollView

在控制器上添加一个带label的scroll view,并且将其设置为贴着view边缘。
scrollView贴着view边缘
我们可以看到scroll view的缩进在顶部和底部自动适配了。在iOS 7及更高版本的系统,scroll view的内容缩进可以用UIViewController’s中的automaticallyAdjustsScrollViewInsets属性进行适配;但是在iOS 11中,这个属性被弃用了,取而代之的是UIScrollView中的一个新属性contentInsetAdjustmentBehavior。

@available(iOS 11.0, *)
public enum UIScrollViewContentInsetAdjustmentBehavior : Int {    
    case automatic          //default value
    case scrollableAxes
    case never
    case always
}

@available(iOS 11.0, *)
open var contentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentBehavior
Content Insets Adjustment Behavior

never —— scroll view内容永远不适配,很好理解。
 scroll view贴在view的边缘(contentInsetAdjustmentBehavior == .never)
scrollableAxes —— 只对可滚动的方向上适配。比如说当scroll view的content size高度大于frame.size的高度或者启用了alwaysBounceVertical属性时,纵向可滑动;类似的当content size宽度大于frame.size的宽度或者启用了alwaysBounceHorizontal属性时,横向可滑动。
 scroll view贴在view的边缘(contentInsetAdjustmentBehavior == .scrollableAxes)。纵向可滚动
水平模式下只有底部content inset会适配,左侧和右侧content inset不会适配,因为此时横向不能滑动。
always —— 不管是否可滑动,scroll view的content insets永远适配。
scroll view贴在view的边缘(contentInsetAdjustmentBehavior == .always)。纵向可滚动
automatic —— 是默认项,也是最有意思的。当它满足下列情况时,它和always 一样:

  • scroll view横向可滑动,纵向不可滑动
  • scroll view是控制器view的最底层子视图
  • 控制器是navigation控制器或tabBar控制器的子控制器
  • automaticallyAdjustsScrollViewInsets为真

在其他情况下automatic相当于scrollableAxes
关于automatic选项的更多描述,可以查看UIScrollView类:

类似于.scrollableAxes,但是考虑到向下兼容,当scroll view位于navigation controller中且设定了automaticallyAdjustsScrollViewInsets为真时,也会自动调整顶部和底部的contentInset,不管scroll view是否可滑动。

然而xcode苹果文档中的描述略有不同

当一个控制器展示在一个navigation控制器或者tabBar控制器时,其中的scroll view总是会被自动调整。如果scroll view处于横向可滑动,那么当未设置安全区indest的时候,横向的content offset也会自动调整。
因为考虑到向下兼容,automatic项被设置为默认。也就是说,在 iOS 10和 iOS 11中,横向可滑动的scroll view将会拥有相同的顶部和底部缩进。

Adjusted Content Insets

在iOS 11中,UIScrollView拥有了一个新的属性——adjustedContentInset:

@available(iOS 11.0, *)
open var adjustedContentInset: UIEdgeInsets { get }

那么contentInset和adjustedContentInset属性有什么不同?我们打印一下当scroll view被导航栏和菜单栏遮挡时两者的值:

//iOS 10
//contentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)
//iOS 11
//contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
//adjustedContentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)

现在我们给contentInset从四个方向分别加10个点,然后再打印一下:

//iOS 10
//contentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)
//iOS 11
//contentInset = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
//adjustedContentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)

我们可以看到在iOS 11中,scroll view实际的content insets将从adjustedContentInset属性中取得,而不再是contentInset属性。意思是当app需要同时支持iOS 10和 iOS 11时,我们需要为content insets的适配写两套不同的逻辑。
想要监听contentInset的值被修改的情况,UIScrollView和UIScrollViewDelegate提供了响应的方法。

//UIScrollView
@available(iOS 11.0, *)
open func adjustedContentInsetDidChange()

//UIScrollViewDelegate
@available(iOS 11.0, *)
optional public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)

UITableView

在控制器上添加一个带有自定义header和自定义cell的table view。

自定义的header上有一个label。自定义的cell也有一个label,带一个分割线。header和cell是透明的,cell的content view背景为白色,header的content view背景为红色。
我们可以看到,当横屏时,header和cell的content view frame变了,但是同时cell以及分割线的frame没有变。这是个默认项,我们可以用UITableView的新属性insetsContentViewsToSafeArea来进行控制。

@available(iOS 11.0, *)
open var insetsContentViewsToSafeArea: Bool

如果将此属性设置为NO:

我们可以看到现在header、footer、cell的content views frame就等于他们各自的frame。
这就意味着在iOS 11,我们在为header、footer、cell添加子控件时,不需要改变子控件的位置,UITableView自动帮我们适配。

UICollectionView

我们再试着在UICollectionView做一个同样的列表:

在collection view中使用了UICollectionViewFlowLayout,滑动方向设为纵向,cell是透明的,cell的content view背景设为白色,header也就是UICollectionReusableView没有contentView,我们把它的背景设为红色。
从截图中可以看到,collection view在默认情况下没有缩进header、footer和cell的内容。想要正确布局它的内容,唯一的方法是把子视图放到安全区:

好,现在改变cell的size,使我们的collection view显示网格:

可以看到在横屏时cell被刘海遮挡了一部分。想要解决这个问题我们需要在section的content insets上添加安全区,但是其实在iOS 11中UICollectionViewFlowLayout有了一个新属性sectionInsetReference用来处理它。

@available(iOS 11.0, *)
public enum UICollectionViewFlowLayoutSectionInsetReference : Int {    
    case fromContentInset        //default value
    case fromSafeArea
    case fromLayoutMargins
}

@available(iOS 11.0, *)
open var sectionInsetReference: UICollectionViewFlowLayoutSectionInsetReference

想做出我们想要的效果只需要设置成fromSafeArea。这个时候section的实际 content insets就等于content insets加上安全区insets。

类似地,当使用fromLayoutMargins时,collection view就在content insets基础上加了margins。

总结

在iOS 11中,苹果为我们加了很多实用的工具来使用安全区。在这篇文章我试着把他们全部介绍了一遍。希望本文可以帮助大家更好地在app中应用安全区。另外,我建议大家看一下相关的WWDC视频或者阅读相关文章:

如果使用storyboards的话,还有专门的讲解(我不用):

也可以看一下我为这篇文章专门写的示例代码:
示例代码

如果有什么问题欢迎留言,谢谢观看!

原文地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值