本文是針對(duì)WWDC2018 Session234:UIKit:Apps for Every Size and Shape快速適配所有型號(hào)的iOS移動(dòng)設(shè)備介紹的理解。
Safe area and layout margins(安全區(qū)域和布局邊距)
如何在應(yīng)用程序中使用這個(gè)屬性適應(yīng)各種屏幕的尺寸和形狀呢?
Safe Area: 安全區(qū)域,這個(gè)概念在iOS11中被提出,安全區(qū)域幫助我們將view放置在整個(gè)屏幕的可視區(qū)域內(nèi),保證view不被系統(tǒng)的狀態(tài)欄、導(dǎo)航欄或tabbar等覆蓋,這個(gè)概念的提出主要是為了適配像iphoneX一樣的全面屏。
我們可以通過(guò)UIView的safeAreaInsets來(lái)獲取安全區(qū)域的UIEdgeInsets。如果你使用Auto Layout進(jìn)行自動(dòng)布局,你可以使用safeAreaLayoutGuide來(lái)確定安全區(qū)域,同時(shí)安全區(qū)域限制了視圖的可見(jiàn)部分,如圖1所示。
安全區(qū)域是如何從父視圖傳遞到子視圖的呢?
父視圖A的安全區(qū)域如下圖2所示。
接下來(lái),我們?cè)谝晥DA上添加一個(gè)子視圖B,視圖B的約束不依賴于視圖A的safeAreaLayoutGuide,視圖B的左右和底部都超出了父視圖的安全區(qū)域,如圖3所示。
然后在視圖B上添加子視圖C,并設(shè)置視圖C的約束依賴于視圖B的 safeAreaLayoutGuide,結(jié)果視圖C的可視范圍會(huì)被限制在圖4的黃色區(qū)域內(nèi),由此看出父視圖的安全區(qū)域會(huì)向上傳遞。
如何擴(kuò)展安全區(qū)域?
通過(guò)使用UIViewController的.additionalSafeAreaInsets屬性,可以自定義擴(kuò)展安全區(qū)域的大小,并且可以通過(guò)viewSafeAreaInsetsDidChange()方法,獲取此時(shí)視圖的安全區(qū)域。
例如,如圖5,豎屏?xí)r,視圖本身的安全區(qū)域UIEdgeInsets為 (top = 88, left = 0, bottom = 83, right = 0),再設(shè)置視圖的additionalSafeAreaInsets為(50,50,50,50),結(jié)果視圖的安全區(qū)域就變?yōu)榱?top = 138, left = 50, bottom = 133, right = 50)黃色區(qū)塊表示。
UIView同樣提供了 safeAreaInsetsDidChange()方法用于獲取安全區(qū)域的UIEdgeInsets。
布局邊距l(xiāng)ayoutMargins
iOS8 中提出了 layoutMargins的概念,使用layoutMargins可以獲取和設(shè)置子控件顯示內(nèi)容距離父控件的邊距。在 iOS11 中,新增了directionalLayoutMargins屬性來(lái)指定邊距。這兩個(gè)屬性的結(jié)構(gòu)定義如下:
typedef struct UIEdgeInsets {
CGFloat top, left, bottom, right;
} UIEdgeInsets;
typedef struct NSDirectionalEdgeInsets {
CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets API_AVAILABLE(ios(11.0),tvos(11.0),watchos(4.0));
從定義上看,區(qū)別在于將UIEdgeInsets的left和right調(diào)整為NSDirectionalEdgeInsets的leading和trailing。這一調(diào)整主要是為了Right To Left(RTL)語(yǔ)言下可以進(jìn)行自動(dòng)適配,例如:要實(shí)現(xiàn)文本每行尾部邊距設(shè)置為30px,在以前做法則需要判斷語(yǔ)言來(lái)區(qū)分哪些是RTL語(yǔ)言,然后再做設(shè)置,iOS11后,可以一步到位。默認(rèn)情況下,layoutMargin到各邊的距離是8個(gè)點(diǎn)。通過(guò)在 Interface Builder里面勾選 Constrain to margins,它會(huì)根據(jù)版本在 iOS11 及以上的系統(tǒng)中自動(dòng)使用 directionalLayoutMargins。通過(guò)viewLayoutMarginsDidChange()方法獲取當(dāng)前視圖的layoutMargin。
安全區(qū)域和布局邊距協(xié)同作用
正常情況下子視圖的布局邊距會(huì)依賴父視圖的安全區(qū)域,但是當(dāng)設(shè)置了insetsLayoutMarginsFromSafeArea = false之后,子視圖可以達(dá)到突破父視圖安全區(qū)域的布局效果,如 圖7、8 所示。
need-to-insert-img
圖7
need-to-insert-img
圖8
布局之子視圖傳播
當(dāng)一個(gè)視圖的preservesSuperviewLayoutMargins屬性為 true 時(shí),在對(duì)它的子視圖進(jìn)行布局時(shí),父視圖的 margin 也會(huì)被考慮在內(nèi)。如果存在一個(gè)子視圖的 frame 剛好和父視圖的 margin 表示區(qū)域有重合,此時(shí)設(shè)置 preservesSuperviewLayoutMargins為 true ,則子視圖會(huì)被剛好限制在父視圖的 margin 內(nèi),如 圖9、10 所示。
need-to-insert-img
圖9
need-to-insert-img
圖10
最小邊距
UIViewController存在屬性 systemMinimumLayoutMargins,可以對(duì)其進(jìn)行重寫,默認(rèn)情況下 view 的布局邊距會(huì)受這個(gè)屬性的返回值制約。如下重寫了該屬性,則 view 的邊距最小會(huì)為 [20,20,20,20]。
overridevarsystemMinimumLayoutMargins:NSDirectionalEdgeInsets{returnNSDirectionalEdgeInsets(top:20, leading:20, bottom:20, trailing:20)}
若設(shè)置 viewRespectsSystemMinimumLayoutMargins為 false,則 view 布局邊距不受 systemMinimumLayoutMargins屬性的影響,默認(rèn)為 [8,8,8,8]。
Scroll views
adjustedContentInset
iOS11 中提出了 UIScrollView的新屬性 adjustedContentInset,它的值等于 UIScrollView原有的 contentInset加上 安全區(qū)域等 system inset,如 圖11 所示。
adjustedContentInset = contentInset + system inset
need-to-insert-img
圖11
廢棄 Automatic Content Inset
本 Session 再次提到 iOS11 之后廢除了原有的 UIViewController屬性 automaticallyAdjustsScrollViewInsets取而代之的是 UIScrollView的新增枚舉 ContentInsetAdjustmentBehavior,該枚舉結(jié)構(gòu)如下:
publicenumContentInsetAdjustmentBehavior:Int{caseautomaticcasescrollableAxescasenevercasealways }
如下圖如果設(shè)置枚舉值為 .always,則默認(rèn)情況下 scrollView的 adjustedContentInset就等于 safeAreaInsets,即可視區(qū)域不會(huì)被 navigationBar和 tabBar遮擋,如 圖12 所示。
need-to-insert-img
圖12
如果將枚舉值設(shè)置為 .scrollableAxes,則在可以滾動(dòng)的方向上,或者設(shè)置了 alwaysBounceHorizontal/Vertical為 true 的時(shí)候,Inset 才會(huì)生效。如 圖14,頁(yè)面內(nèi)容比較少的時(shí)候,垂直方向上 scrollView不可滾動(dòng),導(dǎo)致文本標(biāo)題部分被 navigationBar遮擋,如 圖13、14 所示。
need-to-insert-img
圖13
need-to-insert-img
圖14
系統(tǒng)默認(rèn)設(shè)置的枚舉值是 .automatic, 這個(gè)枚舉值基本和 .scrollableAxes表現(xiàn)一致,但唯一不同的是它還秉承了原來(lái) automaticallyAdjustsScrollViewInsets = true的特性,在有 navigationBar且 isTranslucent為 true 時(shí),即使垂直方向上不能夠滾動(dòng),依然能夠調(diào)整 Inset 使內(nèi)容可見(jiàn),如 圖15 所示。
圖15
如果將枚舉值設(shè)置為 .nerver,則 scrollView的 Inset 不會(huì)受 safeAreaInserts的影響而改變、如 圖16 所示。
圖16
編寫自適應(yīng)的應(yīng)用程序
隱藏 status bar
若果在一些場(chǎng)景下需要隱藏 status bar,我們一般會(huì)這么做:
classArticleViewController:UIViewController{overridevarprefersStatusBarHidden:Bool{returntrue} }
不幸的是在 iPhone X 上面這么寫是無(wú)效的,在 iPhone X 上面只有在隱藏了 navigationBar的前提下,上面這段代碼才會(huì)生效,所以蘋果官方給出的建議是同時(shí)隱藏 navigationBar和 status bar,如 圖17 所示。
need-to-insert-img
圖17
readableContentGuide & cellLayoutMarginsFollowreadableWidth
iOS9 就提出了 readableContentGuide這一概念,主要是用于一些閱讀類應(yīng)用,在可視寬度較大的時(shí)候,希望能夠通過(guò)布局將閱讀區(qū)域限定在一定范圍,已緩解用戶閱讀的過(guò)程中追蹤內(nèi)容移動(dòng)頭部所造成的疲勞。readableContentGuide的間距大小會(huì)隨著字體大小、設(shè)備不同等因素而發(fā)生改變。現(xiàn)這一屬性同樣兼容 safeAreaInsets。同樣的 UITableView的 cellLayoutMarginsFollowreadableWidth屬性也同樣兼容 safeAreaInsets,如 圖18、19、20 所示。
need-to-insert-img
圖18
need-to-insert-img
圖19
need-to-insert-img
圖20
insetsContentViewToSafeArea
UITableView在iOS11開(kāi)始添加了一個(gè)新的屬性insetsContentViewsToSafeArea,該屬性能夠控制 TableViewCell的 ContentView是否被 safeAreaInsets所影響,如 圖20、21 所示。
need-to-insert-img
圖21
need-to-insert-img
圖22
底部按鈕布局最佳實(shí)踐
iPhone X 之后,我們?cè)陂_(kāi)發(fā)過(guò)程中經(jīng)常會(huì)遇到如何布局底部按鈕的問(wèn)題。在本 Session 中官方給出了一種方案,例如設(shè)置按鈕距底部相對(duì)于 superView 的約束為16,約束的 Priority為 999,同時(shí)設(shè)置按鈕底部相對(duì)于 safeAreaLayoutGuide的約束值為大于等于 0。即可實(shí)現(xiàn)按鈕在 iphone X 和 其他設(shè)備上的不同布局,如 圖23 所示。
圖23
總結(jié)
其實(shí)本 Session 并沒(méi)有提出任何新的屬性和方法,最新的屬性在 iOS11 SDK 中就已經(jīng)提出來(lái)了??赡芎芏嚅_(kāi)發(fā)者,在適配iPhone X 的時(shí)候遇到的問(wèn)題也都解決的差不多了。但個(gè)人認(rèn)為這個(gè) Session 還是很有必要的,它將現(xiàn)有的用于適配開(kāi)發(fā)的 UIKit SDK 進(jìn)行了歸納總結(jié),這將有助于開(kāi)發(fā)者進(jìn)一步了解這些屬性之間的關(guān)聯(lián)關(guān)系對(duì)快速適配多種尺寸設(shè)備的項(xiàng)目開(kāi)發(fā)會(huì)有很大幫助。