前言
隨著蘋果日趨追趕大屏風(fēng)潮的腳步凰棉,之前讓 Android 同學(xué)頭疼不已的屏幕適配問(wèn)題得哆,也逐漸開始困擾我們廣大 iOS 小伙伴們沥曹。該篇文章在 WWDC 關(guān)于 UIKit:Apps for Every Size and Shape 的基礎(chǔ)上進(jìn)行總結(jié)與實(shí)踐髓帽,希望能夠幫助大家快速準(zhǔn)確的處理布局與適配問(wèn)題奄薇。
文章結(jié)構(gòu)如下:
- Safe Area 的概念與特性
- safeAreaInsets 與 safeAreaLayoutGuide 調(diào)用時(shí)機(jī)
- 安全區(qū)域擴(kuò)展
- 安全區(qū)域計(jì)算規(guī)則
- 安全區(qū)域傳遞性
- UIView 的 Margins
- layoutMargins
- directionalLayoutMargins
- systemMinimumLayoutMargins
- preservesSuperviewLayoutMargins
- insetsLayoutMarginsFromSafeArea
- UIScrollView 適配
- adjustedContentInset
- contentInsetAdjustmentBehavior
- UITableView & UICollectionVIew
- UITableView 的 insetsContentViewsToSafeArea
- UICollectionViewFlowLayout 的 sectionInsetReference
常見機(jī)型尺寸對(duì)照表:
機(jī)型 | 尺寸(英寸) | 寬高 | 分辨率 |
---|---|---|---|
5驳阎、SE | 4 | 320x568 | 640x1136 |
6、7馁蒂、8 | 4.7 | 375x667 | 750x1334 |
6+呵晚、7+、8+ | 5.5 | 414x736 | 1080x1920 |
X沫屡、XS | 5.8 | 375x812 | 1125x2436 |
XR | 6.1 | 414x896 | 828x1792 |
XS Max | 6.5 | 414x896 | 1242x2688 |
Safe Area 的概念與特性
可被完全看見的饵隙,不影響用戶操作的矩形區(qū)域,稱為安全區(qū)沮脖,在 iOS 11 伴隨著全(劉)面(海)屏被提出金矛。它的出現(xiàn)取代了 iOS 7 以來(lái)的 topLayoutGuide
與 bottomLayoutGuide
,成為新的參照物來(lái)保證 view 能正常勺届、安全地顯示驶俊。
這個(gè)區(qū)域的大小是由系統(tǒng)來(lái)決定的。在某個(gè) view 上的布局只需要相對(duì)于其 safe area 就可以了免姿。每個(gè) view 的 safe area 都可以通過(guò) iOS 11 新增的 API safeAreaInsets
或者 safeAreaLayoutGuide
獲取饼酿。
safeAreaInsets 與 safeAreaLayoutGuide 調(diào)用時(shí)機(jī)
在視圖顯示在屏幕上或者裝載到一個(gè)視圖層級(jí)中的時(shí)候,才能正確獲取到 safeAreaInsets
與 safeAreaLayoutGuide
胚膊,否則返回0故俐。
所以與安全區(qū)相關(guān)的布局操作應(yīng)放在 layoutSubviews
或 viewDidLayoutSubviews
中進(jìn)行處理奈应。
安全區(qū)域擴(kuò)展
UIViewController 支持使用 additionalSafeAreaInsets
屬性,自定義擴(kuò)展安全區(qū)域大小购披,以滿足一些應(yīng)用場(chǎng)景杖挣。這里蘋果官方給出了一個(gè)例子:
You might use this property to extend the safe area to include custom content in your interface. For example, a drawing app might use this property to avoid displaying content underneath tool palettes.
安全區(qū)域計(jì)算規(guī)則
對(duì)于 ViewController 的根視圖,會(huì)根據(jù)各種 bar 的高度刚陡,以及開發(fā)者自己設(shè)置的 additionalSafeAreaInsets
屬性來(lái)計(jì)算惩妇。
對(duì)于層級(jí)中的其他視圖,safeAreaInsets
反映了 View 被覆蓋的區(qū)域筐乳。只有當(dāng)該視圖存在超出其父視圖安全區(qū)域的部分歌殃,safeAreaInsets
才會(huì)返回相應(yīng)的值。如果整個(gè)視圖已經(jīng)處于安全區(qū)域中蝙云,那么 safeAreaInsets
返回 0氓皱。
安全區(qū)域傳遞性
通過(guò) self.additionalSafeAreaInsets = UIEdgeInsetsMake(15, 15, 15, 15)
更改 ViewController 根視圖的 Safe Area(藍(lán)色部分),然后在其上添加一個(gè)超出安全區(qū)域的子視圖(黃色部分)勃刨,最后在子視圖上添加一個(gè)Label(綠色部分)并依據(jù)該子視圖的 safeAreaInsets
建立約束波材。從最終的呈現(xiàn)效果可以論證父視圖的安全區(qū)域會(huì)向上傳遞。
小結(jié)
這個(gè)由系統(tǒng)控制的區(qū)域身隐,是我們適配時(shí)候需要重點(diǎn)關(guān)注的對(duì)象廷区。尤其是要清楚地知道,安全區(qū)到屏幕邊距在橫屏或豎屏贾铝、NavigationBar 與 TabBar 存在或不存在等情況下的具體數(shù)值隙轻,這樣在對(duì)控件進(jìn)行布局時(shí),才能準(zhǔn)確把握其位置垢揩。
iOS 11 之后玖绿,我們可以通過(guò)下面的代碼來(lái)獲取這個(gè)具體數(shù)值:
UIEdgeInsets insets = UIEdgeInsetsZero;
if(@available(iOS 11.0, *)) {
insets = view.safeAreaInsets;
}
iOS 11 之前就需要我們通過(guò)宏定義去進(jìn)行相應(yīng)的處理。例如狀態(tài)欄叁巨、導(dǎo)航欄等的高度獲劝叻恕:
#define StatusBarHeight CGRectGetHeight([UIApplication sharedApplication].statusBarFrame)
#define TabBarHeight(tabBar) CGRectGetHeight(tabBar.frame)
#define NavigationBarHeight(navigationBar) CGRectGetHeight(navigationBar.frame)
常見系統(tǒng)控件高度表:
控件 | 豎屏 | 橫屏 |
---|---|---|
StatusBar | 20 | 0 |
StatusBar(X) | 44 | 0 |
NavigationBar | 44 | 32 |
NavigationBar(XR、Max) | 44 | 44 |
TabBar | 49 | 32 |
TabBar(X) | 83 | 53 |
TabBar(XR俘种、Max) | 83 | 70 |
HomeIndicator | 34 | 21 |
注:iPhone X 橫屏下左右兩邊安全距離為44
UIView 的 Margins
就像我們用田字格寫漢字時(shí)上留天秤标,下留地,左右要留空一樣宙刘,界面的布局也應(yīng)該留有邊界苍姜。這個(gè)邊界就是 Margins
。
layoutMargins
iOS 8 中提出了 layoutMargins
的概念悬包,其主要用于設(shè)置子視圖與父視圖之間的邊距衙猪。默認(rèn)情況下,layoutMargin
到各邊的距離為8。
typedef struct UIEdgeInsets {
CGFloat top, left, bottom, right;
} UIEdgeInsets;
In iOS 11 and later, use the directionalLayoutMargins property to specify layout margins instead of this property.
directionalLayoutMargins
iOS 11 提出垫释,主要是為了Right To Left(RTL)語(yǔ)言下可以進(jìn)行自動(dòng)適配丝格。上面蘋果的官方文檔也有指出,用directionalLayoutMargins
替換掉layoutMargin
棵譬,這樣在做國(guó)際化的時(shí)候就無(wú)需針對(duì)語(yǔ)言專門進(jìn)行適配了显蝌。
typedef struct NSDirectionalEdgeInsets {
CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets API_AVAILABLE(ios(11.0),tvos(11.0),watchos(4.0));
systemMinimumLayoutMargins
UIViewController
存在屬性 systemMinimumLayoutMargins
,當(dāng) viewRespectsSystemMinimumLayoutMargins
為 YES 時(shí)订咸,可以通過(guò)該屬性更改 layoutMargins
的默認(rèn)值曼尊。
preservesSuperviewLayoutMargins
iOS 8 開始引入,當(dāng)這個(gè)屬性的值為 YES 的時(shí)候脏嚷,一個(gè)視圖布局內(nèi)容時(shí)其父視圖的 margins
也會(huì)被考慮在內(nèi)骆撇,此時(shí)該視圖的實(shí)際 margins
為該視圖與父視圖 margins
中的最大值。默認(rèn)是 NO父叙。
insetsLayoutMarginsFromSafeArea
iOS 11 開始引入神郊,控制 safeAreaInsets
是否加到 layoutMargins
上。默認(rèn)為 YES趾唱。
//orangeView.insetsLayoutMarginsFromSafeArea = YES;
safeAreaInsets: {88, 0, 0, 0}
layoutMargins: {88, 50, 0, 0}
//orangeView.insetsLayoutMarginsFromSafeArea = NO;
safeAreaInsets: {88, 0, 0, 0}
layoutMargins: {0, 50, 0, 0}
獲取實(shí)際layoutMargins
的偽代碼如下:
- (UIEdgeInsets)getRealLayoutMargins {
UIEdgeInsets layoutMargins = self.layoutMargins; //默認(rèn)是8
if (self.preservesSuperviewLayoutMargins) {
layoutMargins = Max(layoutMargins, self.superview.layoutMargins);
}
if (self.insetsLayoutMarginsFromSafeArea) {
layoutMargins = Add(layoutMargins, self.safeAreaInsets);
}
return layoutMargins;
}
小結(jié)
日常布局的時(shí)候涌乳,往往都不會(huì)使用 layoutMargins
,這是因?yàn)樘峁┑囊曈X標(biāo)注是不會(huì)考慮這個(gè)值的鲸匿,我們更希望能直接設(shè)置約束值為標(biāo)注值爷怀,而不是進(jìn)行轉(zhuǎn)換操作阻肩。目前看來(lái)只有在進(jìn)行語(yǔ)言適配的情況下才會(huì)以 view 的 directionalLayoutMargins
為基準(zhǔn)建立約束吧带欢。
UIScrollView 適配
adjustedContentInset
The insets derived from the content insets and the safe area of the scroll view.
這句話用代碼翻譯過(guò)來(lái)就是adjustedContentInset = safeAreaInset + contentInset
,這里要不要加safeAreaInset
則取決于contentInsetAdjustmentBehavior
烤惊。下面將重點(diǎn)解釋下contentInsetAdjustmentBehavior
這個(gè)屬性乔煞。
另外值得注意的一點(diǎn)是 iOS 10 中 contentInset
與 iOS 11 的 adjustedContentInset
表現(xiàn)是一致的,即 contentInset
的值在 iOS 10 中 與 iOS 11 中可能是不同的:
//iOS 10 automaticallyAdjustsScrollViewInsets = YES
contentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)
//iOS 11 contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways
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)
contentInsetAdjustmentBehavior
iOS 7 中 UIViewController 的 automaticallyAdjustsScrollViewInsets
屬性在 iOS 11 中被廢棄掉了柒室。取而代之的是 UIScrollView 的 contentInsetAdjustmentBehavior
渡贾。
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
UIScrollViewContentInsetAdjustmentAutomatic, // Similar to .scrollableAxes, but for backward compatibility will also adjust the top & bottom contentInset when the scroll view is owned by a view controller with automaticallyAdjustsScrollViewInsets = YES inside a navigation controller, regardless of whether the scroll view is scrollable
UIScrollViewContentInsetAdjustmentScrollableAxes, // Edges for scrollable axes are adjusted (i.e., contentSize.width/height > frame.size.width/height or alwaysBounceHorizontal/Vertical = YES)
UIScrollViewContentInsetAdjustmentNever, // contentInset is not adjusted
UIScrollViewContentInsetAdjustmentAlways, // contentInset is always adjusted by the scroll view's safeAreaInsets
} API_AVAILABLE(ios(11.0),tvos(11.0));
- UIScrollViewContentInsetAdjustmentNever
不依據(jù) scroll view 的safeAreaInsets
進(jìn)行適配。
- (UIEdgeInsets)adjustedContentInset {
return self.contentInset;
}
- UIScrollViewContentInsetAdjustmentAlways
總是依據(jù) scroll view 的safeAreaInsets
進(jìn)行適配
- (UIEdgeInsets)adjustedContentInset {
return UIEdgeInsetsMake(self.contentInset.top + self.safeAreaInsets.top,
self.contentInset.left + self.safeAreaInsets.left,
self.contentInset.bottom + self.safeAreaInsets.bottom,
self.contentInset.right + self.safeAreaInsets.right);
}
- UIScrollViewContentInsetAdjustmentScrollableAxes
在可以滑動(dòng)的方向上(contentSize.width/height > frame.size.width/height
或alwaysBounceHorizontal/Vertical = YES
)依據(jù) scroll view 的safeAreaInsets
進(jìn)行適配雄右。
當(dāng)展示內(nèi)容太少空骚,contentSize.width/height < frame.size.width/height
而導(dǎo)致 scroll view 不能滑動(dòng),則不會(huì)適配擂仍。
- (UIEdgeInsets)adjustedContentInset {
UIEdgeInsets adjustedContentInset = self.contentInset;
if (self.contentSize.width > self.frame.size.width || self.alwaysBounceHorizontal == YES) {
adjustedContentInset.left += self.safeAreaInsets.left;
adjustedContentInset.right += self.safeAreaInsets.right;
}
if (self.contentSize.height > self.frame.size.height || self.alwaysBounceVertical) {
adjustedContentInset.top += self.safeAreaInsets.top;
adjustedContentInset.bottom += self.safeAreaInsets.bottom;
}
return adjustedContentInset;
}
- UIScrollViewContentInsetAdjustmentAutomatic
處于導(dǎo)航層級(jí)且automaticallyAdjustsScrollViewInsets = YES
的 view controller 中囤屹,作為第一個(gè)被添加子視圖的 scroll view, 無(wú)論能否滑動(dòng),頂部與底部都將依據(jù) scroll view 的safeAreaInsets
進(jìn)行適配逢渔。其他情況下與ScrollableAxes
表現(xiàn)相同肋坚。
- (UIEdgeInsets)adjustedContentInset {
UIEdgeInsets adjustedContentInset = self.contentInset;
if (viewController.automaticallyAdjustsScrollViewInsets == YES && viewController.navigationController && viewController.view.subviews.firstObject == self) {
adjustedContentInset.top += self.safeAreaInsets.top;
adjustedContentInset.bottom += self.safeAreaInsets.bottom;
}
else {
if (self.contentSize.width > self.frame.size.width || self.alwaysBounceHorizontal == YES) {
adjustedContentInset.left += self.safeAreaInsets.left;
adjustedContentInset.right += self.safeAreaInsets.right;
}
if (self.contentSize.height > self.frame.size.height || self.alwaysBounceVertical) {
adjustedContentInset.top += self.safeAreaInsets.top;
adjustedContentInset.bottom += self.safeAreaInsets.bottom;
}
}
return adjustedContentInset;
}
UITableView & UICollectionVIew
作為 UIScrollView 的子類,以上特性 UITableView 與 UICollectionVIew 也都具有。但是由于 UITableViewHeaderFooterView智厌、UITableViewCell诲泌、UICollectionReusableView、UICollectionViewCell 的存在铣鹏,適配安全區(qū)的時(shí)候也有一定的差異化敷扫。
UITableView 的 insetsContentViewsToSafeArea
該屬性能夠控制 UITableViewHeaderFooterView 與 UITableViewCell 的 contentView
是否被 safeAreaInsets
所影響,默認(rèn)值為 YES诚卸,具體的呈現(xiàn)效果如下:
這就意味著在 iOS 11呻澜,我們?cè)跒閔eader、footer惨险、cell添加子控件時(shí)羹幸,不需要改變子控件的位置,UITableView自動(dòng)幫我們適配辫愉。
當(dāng)insetsContentViewsToSafeArea
為 NO栅受,橫屏下 cell 部分區(qū)域會(huì)被齊劉海遮擋:
UICollectionViewFlowLayout 的 sectionInsetReference
現(xiàn)在,我們使用 UICollectionView 實(shí)現(xiàn)一個(gè)一樣的列表界面:
從截圖中可以看到恭朗,UICollectionView 在默認(rèn)情況下沒有像 UITableView 那樣去處理屏镊。這是由于 UICollectionReusableView 不存在 contentView
,同時(shí)考慮到 UICollectionView 布局的復(fù)雜性痰腮,并不適合統(tǒng)一進(jìn)行適配而芥。
單列列表布局情況下想要正確布局內(nèi)容,唯一的方法是讓子視圖依據(jù)安全區(qū)建立約束膀值。
這里有一個(gè)需要注意的地方就是棍丐,
UICollectionViewCell 雖然存在contentView
屬性,但是通過(guò) xib 拖拽的 cell 視圖層級(jí)中沒有contentView
沧踏,添加在 cell 上的子視圖也不能夠直接以 cell 的 safe area 為基準(zhǔn)添加約束歌逢。可以通過(guò)手動(dòng)添加view
作為內(nèi)容容器來(lái)解決翘狱。
[圖片上傳失敗...(image-c96b19-1542424776344)]
接下來(lái)改變 UICollectionViewFlowLayout 的 itemSize秘案,使其呈現(xiàn)多列網(wǎng)格布局:
可以看到在橫屏?xí)r cell 被齊劉海遮擋了一部分。當(dāng)然我們通過(guò)設(shè)置 UICollectionViewFlowLayout 的 sectionInset
能夠適配安全區(qū)潦匈。但是在 iOS 11 中阱高,蘋果為 UICollectionViewFlowLayout 添加了一個(gè)新屬性 sectionInsetReference
來(lái)處理它。
typedef NS_ENUM(NSInteger, UICollectionViewFlowLayoutSectionInsetReference) {
UICollectionViewFlowLayoutSectionInsetFromContentInset,
UICollectionViewFlowLayoutSectionInsetFromSafeArea,
UICollectionViewFlowLayoutSectionInsetFromLayoutMargins
} API_AVAILABLE(ios(11.0), tvos(11.0)) API_UNAVAILABLE(watchos);
系統(tǒng)默認(rèn)值為 UICollectionViewFlowLayoutSectionInsetFromContentInset
茬缩,那么我們將 sectionInsetReference
設(shè)置為 UICollectionViewFlowLayoutSectionInsetFromSafeArea
:
這種情況下 sectionInset
等于原來(lái)的大小加上 safeAreaInsets
的大小赤惊。
類似地,當(dāng)使用 UICollectionViewFlowLayoutSectionInsetFromLayoutMargins
時(shí)寒屯,collection view 的 layoutMargins
會(huì)被添加到 sectionInset
荐捻。
小結(jié)
對(duì)于 Scroll View黍少,我們通常會(huì)禁用掉系統(tǒng)自動(dòng)添加偏移量的行為,即設(shè)置 contentInsetAdjustmentBehavior
為 UIScrollViewContentInsetAdjustmentNever
处面,然后通過(guò) frame 去控制展示厂置。其他三個(gè)枚舉值的具體使用場(chǎng)景目前還不是很常見,我覺得一些沉浸式的設(shè)計(jì)可能會(huì)有發(fā)揮的余地魂角。
Table View 不需要我們做額外的處理昵济,它的 insetsContentViewsToSafeArea
屬性默認(rèn)為 YES,會(huì)幫我們調(diào)整 header野揪、footer 以及 cell 的 contentView
访忿。
Collection View 在 iOS 11 之前,單列情況可以設(shè)置 cell 子視圖與 cell 左右邊距的距離大于 44 來(lái)避免橫屏齊劉海的遮蓋問(wèn)題斯稳,多列則可以通過(guò)設(shè)置 sectionInset
進(jìn)行適配海铆。iOS 11 之后單列情況 cell 子視圖直接依據(jù)安全區(qū)進(jìn)行布局,多列可以使用 sectionInsetReference
防止齊劉海遮蓋挣惰。
尾聲
Safe Area 的出現(xiàn)卧斟,為我們提供了一個(gè)虛擬的布局區(qū)域,我們只需要專注于根據(jù)安全區(qū)建立約束即可憎茂,不用再去關(guān)心狀態(tài)欄珍语、導(dǎo)航欄等系統(tǒng)控件變化導(dǎo)致的適配∈#總的來(lái)說(shuō)是對(duì)界面布局效率的重大提升板乙,蘋果也通過(guò)使 xib 的安全區(qū)向下兼容到 iOS 9 等方式,鼓勵(lì)廣大開發(fā)者去使用這些新特性拳氢,做出更精美的應(yīng)用募逞。那就讓我們愉快的使用起來(lái)吧,Just Do IT饿幅!
參考文獻(xiàn):