safe area-- iphone x

iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望被透明的状态栏或者导航栏遮挡的最高位置(status bar, navigation bar, toolbar, tab bar 等)。这个属性的值是一个 length 属性( topLayoutGuide.length)。 这个值可能由当前的 ViewController 或者 NavigationController 或者 TabbarController 决定。

  • 一个独立的ViewController,不包含于任何其他的ViewController。如果状态栏可见,topLayoutGuide表示状态栏的底部,否则表示这个ViewController的上边缘。

  • 包含于其他ViewController的ViewController不对这个属性起决定作用,而是由容器ViewController决定这个属性的含义:

  • 如果导航栏(Navigation Bar)可见,topLayoutGuide表示导航栏的底部。

  • 如果状态栏可见,topLayoutGuide表示状态栏的底部。

  • 如果都不可见,表示ViewController的上边缘。

    这部分还比较好理解,总之是屏幕上方任何遮挡内容的栏的最底部。

iOS 11 开始弃用了这两个属性, 并且引入了 Safe Area 这个概念。苹果建议: 不要把 Control 放在 Safe Area 之外的地方

// These objects may be used as layout items in the NSLayoutConstraint API

@available(iOS, introduced: 7.0, deprecated: 11.0)

open var topLayoutGuide: UILayoutSupport { get }

@available(iOS, introduced: 7.0, deprecated: 11.0)

open var bottomLayoutGuide: UILayoutSupport { get }

今天, 来研究一下 iOS 11 中新引入的这个 API。

UIView 中的 safe area

iOS 11 中 UIViewController 的 topLayoutGuide 和 bottonLayoutGuide 两个属性被 UIView 中的 safe area 替代了。

@available(iOS 11.0, *)

open var safeAreaInsets: UIEdgeInsets { get }

@available(iOS 11.0, *)

open func safeAreaInsetsDidChange()

safeAreaInsets

这个属性表示相对于屏幕四个边的间距, 而不仅仅是顶部还有底部。这么说好像没有什么感觉, 我们来看一看这个东西分别在 iPhone X 和 iPhone 8 中是什么样的吧!

什么都没有做, 只是新建了一个工程然后在

Main.storyboard

中的 UIViewController 中拖了一个橙色的 View 并且设置约束为:

image

ViewController.swift

viewDidLoad

中打印

override func viewDidLoad() {

    super.viewDidLoad()

    print(view.safeAreaInsets)

}

// 无论是iPhone 8 还是 iPhone X 输出结果均为

// UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)

image

iPhone 8 VS iPhone X Safe Area (竖屏)

image

iPhone 8 VS iPhone X Safe Area (横屏)

这样对比可以看出, iPhone X 同时具有上下, 还有左右的 Safe Area。

**再来看这个例子: ** 拖两个自定义的 View, 这个 View 上有一个 显示很多字的Label。然后设置这两个 View 的约束分别是:

let view1 = MyView()

let view2 = MyView()

view.addSubview(view1)

view.addSubview(view2)

let screenW = UIScreen.main.bounds.size.width

let screenH = UIScreen.main.bounds.size.height

view1.frame = CGRect(

x: 0,

y: 0,

width:screenW,

height: 200)

view2.frame = CGRect(

x: 0,

y: screenH - 200,

width:screenW,

height: 200)
image

可以看出来, 子视图被顶部的刘海以及底部的 home 指示区挡住了。我们可以使用 frame 布局或者 auto layout 来优化这个地方:

let insets = UIApplication.shared.delegate?.window??.safeAreaInsets ?? UIEdgeInsets.zero

view1.frame = CGRect(

x: insets.left,

y: insets.top,

width:view.bounds.width - insets.left - insets.right,

height: 200)

view2.frame = CGRect(

x: insets.left,

y: screenH - insets.bottom - 200,

width:view.bounds.width - insets.left - insets.right,

height: 200)
image

这样起来好多了, 还有另外一个更好的办法是直接在自定义的 View 中修改 Label 的布局:

override func layoutSubviews() {

super.layoutSubviews()

if #available(iOS 11.0, *) {

    label.frame = safeAreaLayoutGuide.layoutFrame

}

}

image

这样, 不仅仅是在 ViewController 中能够使用 safe area 了。

UIViewController 中的 safe area

在 iOS 11 中 UIViewController 有一个新的属性

@available(iOS 11.0, *)

open var additionalSafeAreaInsets: UIEdgeInsets

当 view controller 的子视图覆盖了嵌入的子 view controller 的视图的时候。比如说, 当 UINavigationController 和 UITabbarController 中的 bar 是半透明(translucent) 状态的时候, 就有

additionalSafeAreaInsets

image

自定义的 View 上面的 label 布局兼容了 safe area

// UIView

@available(iOS 11.0, *)

open func safeAreaInsetsDidChange()

//UIViewController

@available(iOS 11.0, *)

open func viewSafeAreaInsetsDidChange()

这两个方法分别是 UIView 和 UIViewController 的 safe area insets 发生改变时调用的方法,如果需要做一些处理,可以重写这个方法。有点类似于 KVO 的意思。

模拟 iPhone X 的 safe area

额外的 safe area insets 也能用来测试你的 app 是否支持 iPhone X。在没有 iPhone X 也不方便使用模拟器的时候, 这个还是很有用的。

//竖屏

additionalSafeAreaInsets.top = 24.0

additionalSafeAreaInsets.bottom = 34.0

//竖屏, status bar 隐藏

additionalSafeAreaInsets.top = 44.0

additionalSafeAreaInsets.bottom = 34.0

//横屏

additionalSafeAreaInsets.left = 44.0

additionalSafeAreaInsets.bottom = 21.0

additionalSafeAreaInsets.right = 44.0

UIScrollView 中的 safe area

在 scroll view 上加一个 label。设置scroll 的约束为:

scrollView.snp.makeConstraints { (make) in

        make.edges.equalToSuperview()

    }
image

iOS 7 中引入 UIViewController 的 automaticallyAdjustsScrollViewInsets 属性在 iOS11 中被废弃掉了。取而代之的是 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 不做调整。

scrollableAxes content insets 只会针对 scrollview 滚动方向做调整。

always content insets 会针对两个方向都做调整。

automatic 这是默认值。当下面的条件满足时, 它跟 always 是一个意思

  • 能够水平滚动,不能垂直滚动

  • scroll view 是 当前 view controller 的第一个视图

  • 这个controller 是被navigation controller 或者 tab bar controller 管理的

  • automaticallyAdjustsScrollViewInsets 为 true

在其他情况下 automoatc 跟 scrollableAxes 一样

Adjusted Content Insets

iOS 11 中 UIScrollView 新加了一个属性: adjustedContentInset

@available(iOS 11.0, *)

open var adjustedContentInset: UIEdgeInsets { get }

adjustedContentInset 和 contentInset 之间有什么区别呢?

在同时有 navigation 和 tab bar 的 view controller 中添加一个 scrollview 然后分别打印两个值:

//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)

然后再设置:

// 给 scroll view 的四个方向都加 10 的间距

scrollView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 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 inset 可以通过 adjustedContentInset 获取。这就是说如果你要适配 iOS 10 的话。这一部分的逻辑是不一样的。

系统还提供了两个方法来监听这个属性的改变

//UIScrollView

@available(iOS 11.0, *)

open func adjustedContentInsetDidChange()

//UIScrollViewDelegate

@available(iOS 11.0, *)

optional public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)

UITableView 中的 safe area

我们现在再来看一下 UITableView 中 safe area 的情况。我们先添加一个有自定义 header 以及自定义 cell 的 tableview。设置边框为 self.view 的边框。也就是

tableView.snp.makeConstraints { (make) in

make.edges.equalToSuperview()

}

或者

tableView.frame = view.bounds

image

自定义的 header 上面有一个 lable,自定义的 cell 上面也有一个 label。将屏幕横屏之后会发现,cell 以及 header 的布局均自动留出了 safe area 以外的距离。cell 还是那么大,只是 cell 的 contnt view 留出了相应的距离。这其实是 UITableView 中新引入的属性管理的:

@available(iOS 11.0, *)

open var insetsContentViewsToSafeArea: Bool

insetsContentViewsToSafeArea 的默认值是 true, 将其设置成 no 之后:

image

可以看出来 footer 和 cell 的 content view 的大小跟 cell 的大小相同了。这就是说:在 iOS 11 下, 并不需要改变 header/footer/cell 的布局, 系统会自动区适配 safe area

需要注意的是, Xcode 9 中使用 IB 拖出来的 TableView 默认的边框是 safe area 的。所以实际运行起来 tableview 都是在 safe area 之内的。

image

UICollectionView 中的 safe area

我们在做一个相同的 collection view 来看一下 collection view 中是什么情况:

image

这是一个使用了

UICollectionViewFlowLayout

的 collection view。 滑动方向是竖向的。cell 透明, cell 的 content view 是白色的。这些都跟上面 table view 一样。header(UICollectionReusableView) 没有 content view 的概念, 所以给其自身设置了红色的背景。

从截图上可以看出来, collection view 并没有默认给 header cell footer 添加safe area 的间距。能够将布局调整到合适的情况的方法只有将 header/ footer / cell 的子视图跟其 safe area 关联起来。跟 IB 中拖 table view 一个道理。

image.gif

现在我们再试试把布局调整成更像 collection view 那样:

image

截图上可以看出来横屏下, 左右两边的 cell 都被刘海挡住了。这种情况下, 我们可以通过修改 section insets 来适配 safe area 来解决这个问题。但是再 iOS 11 中, UICollectionViewFlowLayout 提供了一个新的属性 sectionInsetReference 来帮你做这件事情。

@available(iOS 11.0, *)

public enum UICollectionViewFlowLayoutSectionInsetReference : Int {

case fromContentInset

case fromSafeArea

case fromLayoutMargins

}

/// The reference boundary that the section insets will be defined as relative to. Defaults to .fromContentInset.

/// NOTE: Content inset will always be respected at a minimum. For example, if the sectionInsetReference equals .fromSafeArea, but the adjusted content inset is greater that the combination of the safe area and section insets, then section content will be aligned with the content inset instead.

@available(iOS 11.0, *)

open var sectionInsetReference: UICollectionViewFlowLayoutSectionInsetReference

可以看出来,系统默认是使用

.fromContentInset

我们再分别修改, 看具体会是什么样子的。

image

fromSafeArea

这种情况下 section content insets 等于原来的大小加上 safe area insets 的大小。

跟使用

.fromLayoutMargins

相似使用这个属性 colection view 的 layout margins 会被添加到 section content insets 上面。

image

IB 中的 Safe Area

前面的例子都说的是用代码布局要实现的部分。但是很多人都还是习惯用 Interface Builder 来写 UI 界面。苹果在 WWDC 2107 Session 412 中提到:Storyboards 中的 safe area 是向下兼容的

也就是说, 即使在 iOS10 及以下的 target 中,也可以使用 safe area 来做布局。唯一需要做的就是给每个 stroyboard 勾选 Use Safe Area Layout Guide。实际测试看,应该是 iOS9 以后都只需要这么做。

知识点

: 在使用 IB 设置约束之后, 注意看相对的是 superview 还是 topLayoutGuide/bottomLayoutGuide, 包括在 Xcode 9 中勾选了 Use Safe Area Layout Guide 之后,默认应该是相对于 safe area 了。

总结

  • 在适配 iPhone X 的时候首先是要理解 safe area 是怎么回事。盲目的 if iPhoneX{} 只会给之后的工作代码更多的麻烦。

  • 如果只需要适配到 iOS9 之前的 storyboard 都只需要做一件事情。

  • Xcode9 用 IB 可以看得出来, safe area 到处都是了。理解起来很简单。就是系统对每个 View 都添加了 safe area, 这个区域的大小,是否跟 view 的大小相同是系统来决定的。在这个 View 上的布局只需要相对于 safe area 就可以了。每个 View 的 safe area 都可以通过 iOS 11 新增的 API

    safeAreaInsets

    或者

    safeAreaLayoutGuide

    获取。

  • 对与 UIViewController 来说新增了

    additionalSafeAreaInsets

    这个属性, 用来管理有 tabbar 或者 navigation bar 的情况下额外的情况。

  • 对于 UIScrollView, UITableView, UICollectionView 这三个控件来说,系统以及做了大多数的事情。

  • scrollView 只需要设置 contentInsetAdjustmentBehavior 就可以很容易的适配带 iPhoneX

  • tableView 只需要在 cell header footer 等设置约束的时候相对于 safe area 来做

  • 对 collection view 来说修改 sectionInsetReference 为 .safeArea 就可以做大多数的事情了。

  • 总的来说, safe area 可以看作是系统在所有的 view 上加了一个虚拟的 view, 这个虚拟的 view 的大小等都是跟 view 的位置等有关的(当然是在 iPhoneX上才有值)

    以后在写代码的时候,自定义的控件都尽量针对 safe area 这个虚拟的 view 进行布局

原文:https://mp.weixin.qq.com/s?__biz=MzA3NzM0NzkxMQ==&mid=2655359233&idx=1&sn=4eb8ad2ccd6a2705037bfce2394b8566&chksm=84e25c29b395d53ffe2d9f17aa96580f8c53cec0f5bdabc71814a5dfcfad37b9e5bf4129b9c0&mpshare=1&scene=24&srcid=1104dyU7McC2zWLzbj3WKugM&key=5dba587a8b06ccca244188734caa87801c04b5fac2cec798fdecfd3379d3775b9d1023e378e3b55061eec4e61977618af724c1f58e8d8765ae59e0f9b954f6b4b5c8a39ee67c6e767516436b0c4ba6a5&ascene=0&uin=MTU4NDc4MTMwMQ%3D%3D&devicetype=iMac16%2C2+OSX+OSX+10.12.5+build(16F73)&version=12020810&nettype=WIFI&fontScale=100&pass_ticket=%2FbflYGhQOmSeptirRLkQ5YI7B%2F0OTRjnd10kdPdarRJtSCIUZKjcut%2F0h6ukHNF6

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容