1. 程式人生 > >最近很火的 Safe Area 到底是什麼

最近很火的 Safe Area 到底是什麼

點選上方“iOS開發”,選擇“置頂公眾號”

關鍵時刻,第一時間送達!

640?wxfrom=5&wx_lazy=1

0.gif?wxfrom=5&wx_lazy=1

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 並且設定約束為:

0?wx_fmt=jpeg

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

0?wx_fmt=jpeg

iPhone 8 VS iPhone X Safe Area (豎屏)

0?wx_fmt=jpeg

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)

0?wx_fmt=jpeg

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

0?wx_fmt=jpeg

這樣起來好多了, 還有另外一個更好的辦法是直接在自定義的 View 中修改 Label 的佈局:

override func layoutSubviews() {

    super.layoutSubviews()

    if #available(iOS 11.0, *) {

        label.frame = safeAreaLayoutGuide.layoutFrame

    }

}

0?wx_fmt=jpeg

這樣, 不僅僅是在 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

0?wx_fmt=jpeg

自定義的 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()

        }

0?wx_fmt=jpeg

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

0?wx_fmt=jpeg

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

@available(iOS 11.0, *)

open var insetsContentViewsToSafeArea: Bool

insetsContentViewsToSafeArea 的預設值是 true, 將其設定成 no 之後:

0?wx_fmt=jpeg

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

需要注意的是, Xcode 9 中使用 IB 拖出來的 TableView 預設的邊框是 safe area 的。所以實際執行起來 tableview 都是在 safe area 之內的。

0?wx_fmt=gif

UICollectionView 中的 safe area

我們在做一個相同的 collection view 來看一下 collection view 中是什麼情況:

0?wx_fmt=jpeg

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

0?wx_fmt=jpeg

現在我們再試試把佈局調整成更像 collection view 那樣:

0?wx_fmt=jpeg

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

0?wx_fmt=jpeg

fromSafeArea

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

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

0?wx_fmt=jpeg

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 進行佈局

640?

  • 作者:CepheusSun

  • 連結:http://www.jianshu.com/p/63c0b6cc66fd

  • 來源:簡書

  • iOS開發整理髮布,轉載請聯絡作者授權

640?