【轉】最近很火的 Safe Area 究竟是什麼

iOS 7 以後蘋果給 UIViewController 引入了 topLayoutGuidebottomLayoutGuide 兩個屬性來描述不但願被透明的狀態欄或者導航欄遮擋的最高位置(status bar, navigation bar, toolbar, tab bar 等)。這個屬性的值是一個 length 屬性( topLayoutGuide.length)。 這個值可能由當前的 ViewController 或者 NavigationController 或者 TabbarController 決定。swift

  • 一個獨立的ViewController,不包含於任何其餘的ViewController。若是狀態欄可見,topLayoutGuide表示狀態欄的底部,不然表示這個ViewController的上邊緣。
  • 包含於其餘ViewController的ViewController不對這個屬性起決定做用,而是由容器ViewController決定這個屬性的含義:
    • 若是導航欄(Navigation Bar)可見,topLayoutGuide表示導航欄的底部。
    • 若是狀態欄可見,topLayoutGuide表示狀態欄的底部。
    • 若是都不可見,表示ViewController的上邊緣。
      這部分還比較好理解,總之是屏幕上方任何遮擋內容的欄的最底部。

iOS 11 開始棄用了這兩個屬性, 而且引入了 Safe Area 這個概念。蘋果建議: 不要把 Control 放在 Safe Area 以外的地方app

// 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。ide

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 而且設置約束爲:ui


 

ViewController.swiftviewDidLoad 中打印spa

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) 
iPhone 8 VS iPhone X Safe Area (豎屏)
iPhone 8 VS iPhone X Safe Area (豎屏)
iPhone 8 VS iPhone X Safe Area (橫屏)
iPhone 8 VS iPhone X Safe Area (橫屏)

這樣對比能夠看出, iPhone X 同時具備上下, 還有左右的 Safe Area。3d

**再來看這個例子: ** 拖兩個自定義的 View, 這個 View 上有一個 顯示不少字的Label。而後設置這兩個 View 的約束分別是:code

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) 

 

能夠看出來, 子視圖被頂部的劉海以及底部的 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) 

 

這樣起來好多了, 還有另一個更好的辦法是直接在自定義的 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 有一個新的屬性

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

當 view controller 的子視圖覆蓋了嵌入的子 view controller 的視圖的時候。好比說, 當 UINavigationController 和 UITabbarController 中的 bar 是半透明(translucent) 狀態的時候, 就有 additionalSafeAreaInsets

自定義的 View 上面的 label 佈局兼容了 safe area。
自定義的 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() } 

 

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

在其餘狀況下 automoatcscrollableAxes 同樣

Adjusted Content Insets

iOS 11 中 UIScrollView 新加了一個屬性: adjustedContentInset

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

adjustedContentInsetcontentInset 之間有什麼區別呢?

在同時有 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

 

自定義的 header 上面有一個 lable,自定義的 cell 上面也有一個 label。將屏幕橫屏以後會發現,cell 以及 header 的佈局均自動留出了 safe area 之外的距離。cell 仍是那麼大,只是 cell 的 contnt view 留出了相應的距離。這實際上是 UITableView 中新引入的屬性管理的:

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

insetsContentViewsToSafeArea 的默認值是 true, 將其設置成 no 以後:


 

能夠看出來 footer 和 cell 的 content view 的大小跟 cell 的大小相同了。這就是說:在 iOS 11 下, 並不須要改變 header/footer/cell 的佈局, 系統會自動區適配 safe area

須要注意的是, Xcode 9 中使用 IB 拖出來的 TableView 默認的邊框是 safe area 的。因此實際運行起來 tableview 都是在 safe area 以內的。


 

UICollectionView 中的 safe area

咱們在作一個相同的 collection view 來看一下 collection view 中是什麼狀況:


 

這是一個使用了 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 一個道理。


 

如今咱們再試試把佈局調整成更像 collection view 那樣:


 

截圖上能夠看出來橫屏下, 左右兩邊的 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 咱們再分別修改, 看具體會是什麼樣子的。

fromSafeArea
fromSafeArea

這種狀況下 section content insets 等於原來的大小加上 safe area insets 的大小。

跟使用 .fromLayoutMargins 類似使用這個屬性 colection view 的 layout margins 會被添加到 section content insets 上面。


 

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 了。

總結

  1. 在適配 iPhone X 的時候首先是要理解 safe area 是怎麼回事。盲目的 if iPhoneX{} 只會給以後的工做代碼更多的麻煩。
  2. 若是隻須要適配到 iOS9 以前的 storyboard 都只須要作一件事情。
  3. Xcode9 用 IB 能夠看得出來, safe area 處處都是了。理解起來很簡單。就是系統對每一個 View 都添加了 safe area, 這個區域的大小,是否跟 view 的大小相同是系統來決定的。在這個 View 上的佈局只須要相對於 safe area 就能夠了。每一個 View 的 safe area 均可以經過 iOS 11 新增的 API safeAreaInsets 或者 safeAreaLayoutGuide 獲取。
  4. 對與 UIViewController 來講新增了 additionalSafeAreaInsets 這個屬性, 用來管理有 tabbar 或者 navigation bar 的狀況下額外的狀況。
  5. 對於 UIScrollView, UITableView, UICollectionView 這三個控件來講,系統以及作了大多數的事情。
    • scrollView 只須要設置 contentInsetAdjustmentBehavior 就能夠很容易的適配帶 iPhoneX
    • tableView 只須要在 cell header footer 等設置約束的時候相對於 safe area 來作
    • 對 collection view 來講修改 sectionInsetReference 爲 .safeArea 就能夠作大多數的事情了。
  6. 總的來講, safe area 能夠看做是系統在全部的 view 上加了一個虛擬的 view, 這個虛擬的 view 的大小等都是跟 view 的位置等有關的(固然是在 iPhoneX上纔有值) 之後在寫代碼的時候,自定義的控件都儘可能針對 safe area 這個虛擬的 view 進行佈局。
做者:CepheusSun連接:http://www.jianshu.com/p/63c0b6cc66fd來源:簡書著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索