iOS 7 之后蘋(píng)果給 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 兩個(gè)屬性來(lái)描述不希望被透明的狀態(tài)欄或者導(dǎo)航欄遮擋的最高位置(status bar, navigation bar, toolbar, tab bar 等)挑庶。這個(gè)屬性的值是一個(gè) length 屬性( topLayoutGuide.length)颁独。 這個(gè)值可能由當(dāng)前的 ViewController 或者 NavigationController 或者 TabbarController 決定臣樱。
一個(gè)獨(dú)立的ViewController朵逝,不包含于任何其他的ViewController。如果狀態(tài)欄可見(jiàn),topLayoutGuide表示狀態(tài)欄的底部,否則表示這個(gè)ViewController的上邊緣舰褪。
包含于其他ViewController的ViewController不對(duì)這個(gè)屬性起決定作用,而是由容器ViewController決定這個(gè)屬性的含義:
如果導(dǎo)航欄(Navigation Bar)可見(jiàn)疏橄,topLayoutGuide表示導(dǎo)航欄的底部占拍。
如果狀態(tài)欄可見(jiàn),topLayoutGuide表示狀態(tài)欄的底部捎迫。
-
如果都不可見(jiàn)晃酒,表示ViewController的上邊緣。
這部分還比較好理解窄绒,總之是屏幕上方任何遮擋內(nèi)容的欄的最底部贝次。
iOS 11 開(kāi)始棄用了這兩個(gè)屬性, 并且引入了 Safe Area 這個(gè)概念颗祝。蘋(píng)果建議: 不要把 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 }
今天, 來(lái)研究一下 iOS 11 中新引入的這個(gè) API浊闪。
UIView 中的 safe area
iOS 11 中 UIViewController 的 topLayoutGuide 和 bottonLayoutGuide 兩個(gè)屬性被 UIView 中的 safe area 替代了。
@available(iOS 11.0, *)
open var safeAreaInsets: UIEdgeInsets { get } @available(iOS 11.0, *) open func safeAreaInsetsDidChange()
safeAreaInsets
這個(gè)屬性表示相對(duì)于屏幕四個(gè)邊的間距螺戳, 而不僅僅是頂部還有底部。這么說(shuō)好像沒(méi)有什么感覺(jué), 我們來(lái)看一看這個(gè)東西分別在 iPhone X 和 iPhone 8 中是什么樣的吧折汞!
什么都沒(méi)有做, 只是新建了一個(gè)工程然后在
Main.storyboard
中的 UIViewController 中拖了一個(gè)橙色的 View 并且設(shè)置約束為:
在
ViewController.swift
的
viewDidLoad
中打印
override func viewDidLoad() {
super.viewDidLoad() print(view.safeAreaInsets) }
// 無(wú)論是iPhone 8 還是 iPhone X 輸出結(jié)果均為
// UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
iPhone 8 VS iPhone X Safe Area (豎屏)
iPhone 8 VS iPhone X Safe Area (橫屏)
這樣對(duì)比可以看出, iPhone X 同時(shí)具有上下, 還有左右的 Safe Area倔幼。
**再來(lái)看這個(gè)例子: ** 拖兩個(gè)自定義的 View, 這個(gè) View 上有一個(gè) 顯示很多字的Label。然后設(shè)置這兩個(gè) 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)
可以看出來(lái)爽待, 子視圖被頂部的劉海以及底部的 home 指示區(qū)擋住了损同。我們可以使用 frame 布局或者 auto layout 來(lái)優(yōu)化這個(gè)地方:
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)
這樣起來(lái)好多了, 還有另外一個(gè)更好的辦法是直接在自定義的 View 中修改 Label 的布局:
override func layoutSubviews() {
super.layoutSubviews() if #available(iOS 11.0, *) { label.frame = safeAreaLayoutGuide.layoutFrame }
}
這樣, 不僅僅是在 ViewController 中能夠使用 safe area 了翩腐。
UIViewController 中的 safe area
在 iOS 11 中 UIViewController 有一個(gè)新的屬性
@available(iOS 11.0, *)
open var additionalSafeAreaInsets: UIEdgeInsets
當(dāng) view controller 的子視圖覆蓋了嵌入的子 view controller 的視圖的時(shí)候。比如說(shuō)膏燃, 當(dāng) UINavigationController 和 UITabbarController 中的 bar 是半透明(translucent) 狀態(tài)的時(shí)候, 就有
additionalSafeAreaInsets
自定義的 View 上面的 label 布局兼容了 safe area
// UIView
@available(iOS 11.0, *)
open func safeAreaInsetsDidChange()
//UIViewController
@available(iOS 11.0, *)
open func viewSafeAreaInsetsDidChange()
這兩個(gè)方法分別是 UIView 和 UIViewController 的 safe area insets 發(fā)生改變時(shí)調(diào)用的方法茂卦,如果需要做一些處理,可以重寫(xiě)這個(gè)方法组哩。有點(diǎn)類(lèi)似于 KVO 的意思等龙。
模擬 iPhone X 的 safe area
額外的 safe area insets 也能用來(lái)測(cè)試你的 app 是否支持 iPhone X。在沒(méi)有 iPhone X 也不方便使用模擬器的時(shí)候伶贰, 這個(gè)還是很有用的蛛砰。
//豎屏
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 上加一個(gè) label。設(shè)置scroll 的約束為:
scrollView.snp.makeConstraints { (make) in
make.edges.equalToSuperview() }
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 不做調(diào)整泥畅。
scrollableAxes content insets 只會(huì)針對(duì) scrollview 滾動(dòng)方向做調(diào)整。
always content insets 會(huì)針對(duì)兩個(gè)方向都做調(diào)整琅翻。
automatic 這是默認(rèn)值位仁。當(dāng)下面的條件滿足時(shí), 它跟 always 是一個(gè)意思
能夠水平滾動(dòng)方椎,不能垂直滾動(dòng)
scroll view 是 當(dāng)前 view controller 的第一個(gè)視圖
這個(gè)controller 是被navigation controller 或者 tab bar controller 管理的
automaticallyAdjustsScrollViewInsets 為 true
在其他情況下 automoatc 跟 scrollableAxes 一樣
Adjusted Content Insets
iOS 11 中 UIScrollView 新加了一個(gè)屬性: adjustedContentInset
@available(iOS 11.0, *)
open var adjustedContentInset: UIEdgeInsets { get }
adjustedContentInset 和 contentInset 之間有什么區(qū)別呢吼畏?
在同時(shí)有 navigation 和 tab bar 的 view controller 中添加一個(gè) scrollview 然后分別打印兩個(gè)值:
//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)
然后再設(shè)置:
// 給 scroll view 的四個(gè)方向都加 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)
由此可見(jiàn)洞翩,在 iOS 11 中 scroll view 實(shí)際的 content inset 可以通過(guò) adjustedContentInset 獲取。這就是說(shuō)如果你要適配 iOS 10 的話。這一部分的邏輯是不一樣的贬养。
系統(tǒng)還提供了兩個(gè)方法來(lái)監(jiān)聽(tīng)這個(gè)屬性的改變
//UIScrollView
@available(iOS 11.0, *)
open func adjustedContentInsetDidChange()
//UIScrollViewDelegate
@available(iOS 11.0, *)
optional public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)
UITableView 中的 safe area
我們現(xiàn)在再來(lái)看一下 UITableView 中 safe area 的情況。我們先添加一個(gè)有自定義 header 以及自定義 cell 的 tableview挣输。設(shè)置邊框?yàn)?self.view 的邊框刊驴。也就是
tableView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
或者
tableView.frame = view.bounds
自定義的 header 上面有一個(gè) lable,自定義的 cell 上面也有一個(gè) label胸墙。將屏幕橫屏之后會(huì)發(fā)現(xiàn)我注,cell 以及 header 的布局均自動(dòng)留出了 safe area 以外的距離。cell 還是那么大迟隅,只是 cell 的 contnt view 留出了相應(yīng)的距離但骨。這其實(shí)是 UITableView 中新引入的屬性管理的:
@available(iOS 11.0, *)
open var insetsContentViewsToSafeArea: Bool
insetsContentViewsToSafeArea 的默認(rèn)值是 true, 將其設(shè)置成 no 之后:
可以看出來(lái) footer 和 cell 的 content view 的大小跟 cell 的大小相同了智袭。這就是說(shuō):在 iOS 11 下, 并不需要改變 header/footer/cell 的布局奔缠, 系統(tǒng)會(huì)自動(dòng)區(qū)適配 safe area
需要注意的是, Xcode 9 中使用 IB 拖出來(lái)的 TableView 默認(rèn)的邊框是 safe area 的。所以實(shí)際運(yùn)行起來(lái) tableview 都是在 safe area 之內(nèi)的吼野。
UICollectionView 中的 safe area
我們?cè)谧鲆粋€(gè)相同的 collection view 來(lái)看一下 collection view 中是什么情況:
這是一個(gè)使用了
UICollectionViewFlowLayout
的 collection view校哎。 滑動(dòng)方向是豎向的。cell 透明, cell 的 content view 是白色的闷哆。這些都跟上面 table view 一樣腰奋。header(UICollectionReusableView) 沒(méi)有 content view 的概念, 所以給其自身設(shè)置了紅色的背景。
從截圖上可以看出來(lái)抱怔, collection view 并沒(méi)有默認(rèn)給 header cell footer 添加safe area 的間距劣坊。能夠?qū)⒉季终{(diào)整到合適的情況的方法只有將 header/ footer / cell 的子視圖跟其 safe area 關(guān)聯(lián)起來(lái)。跟 IB 中拖 table view 一個(gè)道理屈留。
現(xiàn)在我們?cè)僭囋嚢巡季终{(diào)整成更像 collection view 那樣:
截圖上可以看出來(lái)橫屏下, 左右兩邊的 cell 都被劉海擋住了局冰。這種情況下, 我們可以通過(guò)修改 section insets 來(lái)適配 safe area 來(lái)解決這個(gè)問(wèn)題。但是再 iOS 11 中绕沈, UICollectionViewFlowLayout 提供了一個(gè)新的屬性 sectionInsetReference 來(lái)幫你做這件事情锐想。
@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
可以看出來(lái),系統(tǒng)默認(rèn)是使用
.fromContentInset
我們?cè)俜謩e修改, 看具體會(huì)是什么樣子的乍狐。
fromSafeArea
這種情況下 section content insets 等于原來(lái)的大小加上 safe area insets 的大小赠摇。
跟使用
.fromLayoutMargins
相似使用這個(gè)屬性 colection view 的 layout margins 會(huì)被添加到 section content insets 上面。
IB 中的 Safe Area
前面的例子都說(shuō)的是用代碼布局要實(shí)現(xiàn)的部分浅蚪。但是很多人都還是習(xí)慣用 Interface Builder 來(lái)寫(xiě) UI 界面藕帜。蘋(píng)果在 WWDC 2107 Session 412 中提到:Storyboards 中的 safe area 是向下兼容的
也就是說(shuō), 即使在 iOS10 及以下的 target 中,也可以使用 safe area 來(lái)做布局惜傲。唯一需要做的就是給每個(gè) stroyboard 勾選 Use Safe Area Layout Guide洽故。實(shí)際測(cè)試看,應(yīng)該是 iOS9 以后都只需要這么做盗誊。
知識(shí)點(diǎn)
: 在使用 IB 設(shè)置約束之后时甚, 注意看相對(duì)的是 superview 還是 topLayoutGuide/bottomLayoutGuide, 包括在 Xcode 9 中勾選了 Use Safe Area Layout Guide 之后哈踱,默認(rèn)應(yīng)該是相對(duì)于 safe area 了荒适。
總結(jié)
在適配 iPhone X 的時(shí)候首先是要理解 safe area 是怎么回事。盲目的 if iPhoneX{} 只會(huì)給之后的工作代碼更多的麻煩开镣。
如果只需要適配到 iOS9 之前的 storyboard 都只需要做一件事情刀诬。
-
Xcode9 用 IB 可以看得出來(lái), safe area 到處都是了。理解起來(lái)很簡(jiǎn)單邪财。就是系統(tǒng)對(duì)每個(gè) View 都添加了 safe area陕壹, 這個(gè)區(qū)域的大小,是否跟 view 的大小相同是系統(tǒng)來(lái)決定的树埠。在這個(gè) View 上的布局只需要相對(duì)于 safe area 就可以了糠馆。每個(gè) View 的 safe area 都可以通過(guò) iOS 11 新增的 API
safeAreaInsets
或者
safeAreaLayoutGuide
獲取。
-
對(duì)與 UIViewController 來(lái)說(shuō)新增了
additionalSafeAreaInsets
這個(gè)屬性, 用來(lái)管理有 tabbar 或者 navigation bar 的情況下額外的情況弥奸。
對(duì)于 UIScrollView榨惠, UITableView, UICollectionView 這三個(gè)控件來(lái)說(shuō)盛霎,系統(tǒng)以及做了大多數(shù)的事情赠橙。
scrollView 只需要設(shè)置 contentInsetAdjustmentBehavior 就可以很容易的適配帶 iPhoneX
tableView 只需要在 cell header footer 等設(shè)置約束的時(shí)候相對(duì)于 safe area 來(lái)做
對(duì) collection view 來(lái)說(shuō)修改 sectionInsetReference 為 .safeArea 就可以做大多數(shù)的事情了。
-
總的來(lái)說(shuō)愤炸, safe area 可以看作是系統(tǒng)在所有的 view 上加了一個(gè)虛擬的 view期揪, 這個(gè)虛擬的 view 的大小等都是跟 view 的位置等有關(guān)的(當(dāng)然是在 iPhoneX上才有值)
以后在寫(xiě)代碼的時(shí)候,自定義的控件都盡量針對(duì) safe area 這個(gè)虛擬的 view 進(jìn)行布局规个。