iOS 11 safeArea詳解 & iphoneX 適配

iphoneX.jpg

最近看了許多iPhone X適配的文章,發(fā)現(xiàn)很少有介紹safeArea的蛔糯,就來(lái)隨便寫寫

現(xiàn)在對(duì)于iPhone X的適配产喉,有一種常見(jiàn)的做法是給導(dǎo)航欄或tabbar增加一個(gè)固定的距離郑诺,比如頂部增加44pt碘梢,底部增加34pt。這種寫死距離的做法乍看上去挺簡(jiǎn)單,其實(shí)并不好司致,理由如下

  1. 不適合多機(jī)型的適配拆吆,如果以后出了一種帶劉海的iPad,需要預(yù)留出來(lái)的距離就未必是現(xiàn)在寫死的距離
  2. 不適合需要支持橫豎屏的app脂矫,橫屏頂部不需要增加距離枣耀,反而是左右各有44pt,底部的距離也和豎屏不同
  3. 不夠動(dòng)態(tài)庭再。還是舉個(gè)例子捞奕,假如有電話打進(jìn)來(lái)了,導(dǎo)航欄應(yīng)該會(huì)下移拄轻,這時(shí)候view可能還是會(huì)被擋住

這里我想探討一下如何使用safeAreaLayoutGuide和safeAreaInsets颅围,以一種動(dòng)態(tài)的方式,一勞永逸地解決iPhone X甚至后續(xù)所有機(jī)型的適配問(wèn)題恨搓。

safeAreaLayoutGuide


首先我們看看什么是safeAreaLayoutGuide

A6522569-34FC-4A13-A357-63D91CE134AB.png

看起來(lái)復(fù)雜院促,其實(shí)很簡(jiǎn)單,我歸納一下有幾點(diǎn):

  1. 它是UIView的一個(gè)只讀屬性奶卓,意味著所有UIView對(duì)象都有并且是系統(tǒng)幫我們創(chuàng)建好的
  2. 它繼承UILayoutGuide一疯,有l(wèi)ayoutFrame意味著它能代表一塊區(qū)域
  3. 它代表的區(qū)域避開(kāi)了諸如導(dǎo)航欄、tabbar或者其他有可能擋住你這個(gè)UIView對(duì)象顯示的所有父view夺姑,意味著你的view對(duì)象只要相對(duì)另一個(gè)view的safeLayoutGuide做布局就不用擔(dān)心她被奇奇怪怪的東西擋住
  4. 對(duì)于控制器的view的safeAreaLayoutGuide,他的區(qū)域同樣避開(kāi)了statusbar或其他有可能擋住view顯示的東西掌猛,我們甚至可以用控制器的additionalSafeAreaInsets屬性盏浙,來(lái)額外指定inset
  5. 如果view完全在父view的安全區(qū)域內(nèi),或者view不在視圖層級(jí)或屏幕上荔茬,那么view的safeAreaLayoutGuide區(qū)域其實(shí)和view自身是一樣大的

safeAreaLayoutGuide是一個(gè)相對(duì)抽象的概念废膘,為了便于理解,我們可以把safeAreaLayoutGuide看成是一個(gè)“view”慕蔚,這個(gè)“view”系統(tǒng)自動(dòng)幫我們調(diào)整它的bounds丐黄,讓它不會(huì)被各種奇奇怪怪的東西擋住,包括iPhone X的劉海區(qū)域和底部的一道杠區(qū)域孔飒,可以認(rèn)為在這個(gè)“view”上一定能完整顯示所有內(nèi)容灌闺。

以下綠色部分就是當(dāng)前控制器view的safeAreaLayoutGuide區(qū)域
iphone X豎屏safeAreaLayoutGuide的bounds.png

iPhone X橫屏safeAreaLayoutGuide的bounds.png

截圖來(lái)自https://developer.apple.com/videos/play/fall2017/801/

不過(guò)需要銘記的一點(diǎn)是這個(gè)“view”并不會(huì)顯示在我們的視圖層級(jí)上。
UILayoutGuides will not show up in the view hierarchy, but may be used as items in an NSLayoutConstraint and represent a rectangle in the layout engine.

在我看來(lái)坏瞄,他最大的作用是作為參照物桂对,讓view可以相對(duì)某個(gè)view的safeAreaLayoutGuide做布局,從而保證view能正常鸠匀、安全地顯示(相對(duì)的那個(gè)view不一定要是父view)

在一種常見(jiàn)的使用場(chǎng)景里蕉斜,以前我的某個(gè)view是相對(duì)于控制器的view做布局,現(xiàn)在是相對(duì)控制器view的safeAreaLayoutGuide做布局了

以前是這樣寫
[NSLayoutConstraint constraintWithItem:someView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.vc.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];

現(xiàn)在是這樣
[NSLayoutConstraint constraintWithItem:someView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.vc.view.safeAreaLayoutGuide attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];

適配前后的效果
適配前.png
改成相對(duì)于view的safeAreaLayoutGuide后-豎屏.png
改成相對(duì)于view的safeAreaLayoutGuide后-橫屏.png

可以看到,相同的布局下宅此,橫屏在沒(méi)有statusbar時(shí)机错,距離頂部是0,左邊是44父腕,如果有statusbar弱匪,距離頂部就是20。反正不管怎么弄侣诵,只要我們相對(duì)safeAreaLayoutGuide做布局痢法,我們的view就能夠安全完整地顯示出來(lái)

那么非iOS11怎么辦?

非iOS11 還是只能對(duì)view做布局杜顺,就要寫兩套布局代碼财搁,稍后會(huì)介紹

這樣是不是就足夠應(yīng)對(duì)所有情況了呢?

并不是

  1. 我們自定義的view有一邊需要緊挨著屏幕邊緣躬络,比如我項(xiàng)目里是自定義的導(dǎo)航欄尖奔,它的頂部是挨著屏幕頂部的,那么導(dǎo)航欄就不能相對(duì)view的safeAreaLayoutGuide布局穷当,否則頂部會(huì)空出來(lái)一截子
  2. frame布局

這時(shí)就輪到safeAreaInsets來(lái)發(fā)揮作用啦

safeAreaInsets


先看一下safeAreaInsets的官方解釋

CABD7DCE-79F0-4464-81ED-161F5FA16452.png

有沒(méi)有覺(jué)得和safeAreaLayoutGuide很像提茁?safeAreaLayoutGuide可能就是根據(jù)safeAreaInsets來(lái)調(diào)整自己的bounds的

iPhone X豎屏?xí)r占滿整個(gè)屏幕的控制器的view的safeAreaInsets是(44,0馁菜,34茴扁,0),橫屏是(0汪疮,44峭火,21,44)智嚷,inset后的區(qū)域正好是safeAreaLayoutGuide區(qū)域

既然如此卖丸,對(duì)于自定義的頂部導(dǎo)航欄來(lái)說(shuō),我們可以給導(dǎo)航欄的高度加上一個(gè)vc.view.safeAreaInsets.top盏道,讓他變高一點(diǎn)就可以了稍浆,這樣在X上,豎屏?xí)rtop = 44猜嘱, 橫屏?xí)rtop = 0衅枫,導(dǎo)航欄的高度能響應(yīng)改變

需要注意的是,無(wú)論safeAreaLayoutGuide還是safeAreaInsets都是iOS11才能使用的泉坐。
對(duì)于safeAreaInsets为鳄,我們可以把版本判斷寫在一個(gè)函數(shù)里

我們可以這樣寫

static inline UIEdgeInsets sgm_safeAreaInset(UIView *view) {
    if (@available(iOS 11.0, *)) {
        return view.safeAreaInsets;
    }
    return UIEdgeInsetsZero;
}
UIEdgeInsets safeAreaInsets = sgm_safeAreaInset(self.view);
CGFloat height = kDefaultTopViewHeight; // 導(dǎo)航欄原本的高度,通常是44.0
height += safeAreaInsets.top > 0 ? safeAreaInsets.top : 20.0; // 20.0是statusbar的高度

問(wèn)題又來(lái)了腕让,這段代碼放在什么地方合適呢孤钦?前面官方文檔提到過(guò)歧斟,如果view不在屏幕上或顯示層級(jí)里,view的safeAreaInsets = UIEdgeInsetsZero偏形,所以我們需要明確知道safeAreaInsets改變的時(shí)機(jī)

實(shí)際上系統(tǒng)已經(jīng)提供了回調(diào)

對(duì)于UIViewController

-(void)viewSafeAreaInsetsDidChange NS_REQUIRES_SUPER API_AVAILABLE(ios(11.0), tvos(11.0));

對(duì)于UIView

-(void)safeAreaInsetsDidChange API_AVAILABLE(ios(11.0),tvos(11.0));

這里主要探討controller的回調(diào)静袖,view的回調(diào)是類似的。只要controller的view的safeAreaInsets改變俊扭,系統(tǒng)就會(huì)調(diào)用viewSafeAreaInsetsDidChange队橙。自然而然,我們會(huì)想把以上代碼放在這里萨惑,然而這里有個(gè)大坑捐康,你會(huì)發(fā)現(xiàn),當(dāng)這個(gè)控制器以動(dòng)畫(huà)的方式push進(jìn)來(lái)時(shí)庸蔼,導(dǎo)航欄的高度也會(huì)動(dòng)畫(huà)地變高解总,產(chǎn)生了不必要的多余動(dòng)畫(huà),這種體驗(yàn)很糟糕

那么究竟應(yīng)該放在哪里姐仅?我們很有必要看一下新的viewController調(diào)用時(shí)序
以下是從“rootVC” push 到 “pushVC”控制臺(tái)輸出的調(diào)用時(shí)序以及對(duì)應(yīng)控制器的view的safeAreaInsets

2017-10-04 16:59:59.594811+0800 XXX[15662:803767] Begin pushViewController to [<_TtCC8XXXTests27ContainerViewControllerTest20MockUIViewController: 0x7f9c07b643b0>]
viewDidLoad()---Optional("pushVC")---UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
willMove(toParentViewController:)---Optional("pushVC")---UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
viewWillDisappear---Optional("rootVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewWillAppear---Optional("pushVC")---UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
viewSafeAreaInsetsDidChange()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewWillLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewDidLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewWillLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewDidLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewDidAppear---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewDidDisappear---Optional("rootVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
didMove(toParentViewController:)---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
2017-10-04 16:59:59.604563+0800 XXX[15662:803767] Did PushViewController [<_TtCC8XXXTests27ContainerViewControllerTest20MockUIViewController: 0x7f9c0790d170>]->[<_TtCC8XXXTests27ContainerViewControllerTest20MockUIViewController: 0x7f9c07b643b0>] time = [0.009772]

可以看到花枫,viewSafeAreaInsetsDidChange調(diào)用時(shí)機(jī)很早,在viewWillAppear后掏膏,這是為什么出現(xiàn)多余動(dòng)畫(huà)的原因劳翰。并且“pushVC”的safeAreaInsets直到viewSafeAreaInsetsDidChange調(diào)用前,都是UIEdgeInsetsZero馒疹,之后才是正確的UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
并且viewSafeAreaInsetsDidChange后面會(huì)調(diào)用兩次viewDidLayoutSubviews佳簸,所以我們應(yīng)該把改變高度或布局的代碼都寫在viewDidLayoutSubviews里,這樣就不會(huì)有多余的動(dòng)畫(huà)效果了颖变。需要注意viewDidLayoutSubviews可能會(huì)由別的操作頻繁觸發(fā)溺蕉,所以如果調(diào)整safeArea布局的代碼比較耗時(shí),可以考慮加上一個(gè)狀態(tài)標(biāo)記悼做,只在didChange后執(zhí)行一次布局調(diào)整

最后的代碼應(yīng)該長(zhǎng)這樣

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    UIEdgeInsets safeAreaInsets = sgm_safeAreaInset(self.view);
    CGFloat height = 44.0; // 導(dǎo)航欄原本的高度,通常是44.0
    height += safeAreaInsets.top > 0 ? safeAreaInsets.top : 20.0; // 20.0是statusbar的高度哗魂,這里假設(shè)statusbar不消失
    if (_navigationbar && _navigationbar.height != height) {
        _navigationbar.height = height;
    }
適配前后的效果
適配前

適配后

這樣對(duì)于frame布局和autolayout布局的各種情況肛走,有了一個(gè)動(dòng)態(tài)的適配方案,就是分別使用safeAreaLayoutGuide和safeAreaInsets來(lái)靈活處理布局录别,相比寫死一個(gè)固定距離朽色,當(dāng)前和未來(lái)的各種機(jī)型都能一套代碼適配,擴(kuò)展性更好组题。我們項(xiàng)目目前也是采用這種做法葫男,如果你的項(xiàng)目需要適配橫豎屏或UI控件布局相對(duì)復(fù)雜,真的應(yīng)該考慮使用safeArea

順便提一下崔列,VFL似乎已經(jīng)廢了梢褐,因?yàn)閨只能表示父view的邊緣旺遮,并沒(méi)有一個(gè)符號(hào)來(lái)表示父view的safeAreaLayoutGuide的邊緣,以前我們寫的VFL代碼盈咳,好多得改耿眉,改起來(lái)也特別麻煩,建議別再用VFL了

最后一個(gè)版本判斷的問(wèn)題鱼响,safeAreaInsets和safeAreaLayoutGuide都是iOS11的API鸣剪,如果不做封裝,直接在代碼里寫丈积,勢(shì)必會(huì)出現(xiàn)大量@available這種版本判斷語(yǔ)句筐骇,代碼里到處是@available,看起來(lái)很崩潰江滨,破壞代碼可讀性铛纬。

因?yàn)槲抑皩懥艘粋€(gè)自動(dòng)布局框架,這次就將safeAreaLayoutGuide和版本判斷都順便封裝在里面了牙寞,個(gè)人覺(jué)得這套框架比NSLayoutAnchor好用饺鹃,主要作用是簡(jiǎn)化布局代碼書(shū)寫,以下是生成一個(gè)NSLayoutConstraint的對(duì)比

// 需求是topLeftView的top等于self.view的safeAreaLayoutGuide的top
// 使用系統(tǒng)API
if (@available(iOS 11.0, *)) {
        [NSLayoutConstraint constraintWithItem:self.topLeftView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view.safeAreaLayoutGuide attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
    } else {
        [NSLayoutConstraint constraintWithItem:self.topLeftView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
    }

// 使用NSLayoutConstraint-SSLayout
self.topLeftView.top_attr = self.view.top_attr_safe

在業(yè)務(wù)代碼里不會(huì)出現(xiàn)任何版本判斷间雀,大家有興趣的話可以試一下悔详,哈哈
傳送門:http://www.reibang.com/p/c044f3de564a

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惹挟,隨后出現(xiàn)的幾起案子茄螃,更是在濱河造成了極大的恐慌,老刑警劉巖连锯,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件归苍,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡运怖,警方通過(guò)查閱死者的電腦和手機(jī)拼弃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)摇展,“玉大人吻氧,你說(shuō)我怎么就攤上這事∮搅” “怎么了盯孙?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)祟滴。 經(jīng)常有香客問(wèn)我振惰,道長(zhǎng),這世上最難降的妖魔是什么垄懂? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任骑晶,我火速辦了婚禮痛垛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘透罢。我一直安慰自己榜晦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布羽圃。 她就那樣靜靜地躺著乾胶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪朽寞。 梳的紋絲不亂的頭發(fā)上识窿,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音脑融,去河邊找鬼喻频。 笑死,一個(gè)胖子當(dāng)著我的面吹牛肘迎,可吹牛的內(nèi)容都是我干的甥温。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼妓布,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼姻蚓!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起匣沼,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤狰挡,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后释涛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體加叁,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年唇撬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了它匕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窖认,死狀恐怖超凳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情耀态,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布暂雹,位于F島的核電站首装,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏杭跪。R本人自食惡果不足惜仙逻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一驰吓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧系奉,春花似錦檬贰、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至萌踱,卻和暖如春葵礼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背并鸵。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工鸳粉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人园担。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓届谈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親弯汰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子艰山,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容