HsusueCocoa開發(fā)者社區(qū)
前言
全面屏剛出時,網上有說反人類。但過去這么久了,趨于技術的進步或看久了,大家也都慢慢習慣了(只是筆者還是買不起全面屏)。官方適配中文版文檔也出來了。
回想起剛開始適配全面屏用了一種暴力、并不優(yōu)雅的方法,以至于后來出了XS(MAX)和XR后出了bug。所以選擇一種可靠的、優(yōu)雅的方案是很有必要的。如今網上關于探討適配全面屏的文章五花八門,筆者將探究其中的各種方案。
由于筆者水平有限,眼界狹窄,難免出現(xiàn)疏忽的地方,希望大神提出更好的方案。
全面屏的數據
頂部
從以上兩圖,我們可以看出全面屏的頂部Statusbar變高了,其他部分沒變。
Largetitle是iOS11中新加入的特性。當然我們開發(fā)中很少用到Largetitle。
底部
全面屏底部多出了高度為34的Home Indicator 區(qū)域。
個人總結的全面屏適配觀點
1.雖然筆者買不起XStyle,但是虛擬器應該能滿足適配的所有測試。
所以開發(fā)中,請優(yōu)先使用全面屏開發(fā)。筆者有個朋友,開發(fā)時用非全面屏,偶爾會出現(xiàn)忘記適配全面屏問題。如果用全面屏,開發(fā)效率將會進一步提升。畢竟界面適配全面屏的時候,很難忘記適配非全面屏。
2.App顯示界面大小是由App啟動頁決定的。
記得iPhoneX剛出時,App在其上面運行顯示居中,大小和6s一樣,上下各有一塊黑塊。嘗試打印出分辨率驚奇發(fā)現(xiàn)不是官方宣傳的1124,2436。把啟動頁大小改了宏才達到預想效果。如果用xib,那就沒什么問題。啟動頁用圖片的話,要適配上@3x的圖片。
3.宏怎么定義
宏里只要能區(qū)分開XStyle,其他高度就好說。
網上很多教程都是按照分辨率來區(qū)分。然而,根據上面我們可以發(fā)現(xiàn),XStyle的分辨率并非固定。所以單純按照分辨率是不行的。筆者一開始適配X就是這樣,后來XSMax出了問題,被迫強行更新XCode10用XSMax,發(fā)現(xiàn)宏沒寫好(順便吐槽一句,XCode10真是噩夢,又懶得下回去)。
也有舊教程是按照屏幕寬高。但根據上面數據也不是固定的,所以要注意。這兩種失效宏都列在下面。
//?單純根據分辨率
#defineK_iPhoneXStyle?([UIScreen?instancesRespondToSelector:@selector(currentMode)]???CGSizeEqualToSize(CGSizeMake(1125,?2436),?[[UIScreen?mainScreen]?currentMode].size)?:?NO)
//?單純根據屏幕寬高
#defineK_iPhoneXStyle?(KScreenWidth?==?375.f?&&?KScreenHeight?==?812.
f???YES?:?NO)
其實筆者很想弄出一個通用的宏,不那么怕出新機會失效的宏,但奈何實在想不到。只能照XStyle不一樣的數據寫出宏。下面列出的宏在XS Max時還是有效的。至于以后就說不準了,各位還是要根據新機分辨率或者寬高適當修改。
#defineK_iPhoneXStyle?(?(CGSizeEqualToSize(CGSizeMake(414,?896),?[[UIScreen?mainScreen]?bounds].size))?||?([UIScreen?instancesRespondToSelector:@selector(currentMode)]???CGSizeEqualToSize(CGSizeMake(1125,?2436),?[[UIScreen?mainScreen]?currentMode].size)?:?NO)?)
或者
#define??K_iPhoneXStyle?((KScreenWidth?==375.f&&?KScreenHeight?==812.f??YES?:?NO)?||?(KScreenWidth?==414.f&&?KScreenHeight?==896.f??YES?:?NO))
還有其他的宏
#defineKScreenWidth?([UIScreen?mainScreen].bounds.size.width)
#defineKScreenHeight?([UIScreen?mainScreen].bounds.size.height)
#defineK_iPhoneXStyle?((KScreenWidth?==?375.f?&&?KScreenHeight?==?812.f???YES?:?NO)?||?(KScreenWidth?==?414.f?&&?KScreenHeight?==?896.f???YES?:?NO))
#defineKStatusBarAndNavigationBarHeight?(K_iPhoneXStyle???88.f?:?64.f)
#defineKStatusBarHeight?(K_iPhoneXStyle???44.f?:?20.f)
#defineKTabbarHeight?(K_iPhoneXStyle???83.f?:?49.f)
#defineKMagrinBottom?(K_iPhoneXStyle???34.f?:?0.f)
還有些宏,是適配字體或者view用的。將在后面介紹UI在不同尺寸下適配方案再提起。
#defineKScaleWidth(width)?((width)*(KScreenWidth/375.f))
#defineIsIphone6P??????????SCREEN_WIDTH==414
#defineSizeScale???????????(IsIphone6P???1.5?:?1)
#definekFontSize(value)????value*SizeScale
#definekFont(value)????????[UIFont?systemFontOfSize:value*SizeScale]
最后說一句,利用宏來寫雖然簡單,但有以下弊端。
找不到能可靠性強的宏XStyle。畢竟以后的手機分辨率或者屏幕寬高肯定會變化(即使短時間內不會)。要適應新機型就要重新上線。
假如以后新出的機劉海長度變了,到時又要修改宏。
App適配起橫屏,也挺麻煩的。
即使有弊端,筆者還是覺得這么寫可行。畢竟大多數App都不用支持橫屏,而且屏幕短時間內不會有太大變動。
人要保持一顆活到老學到老的心,這些弊端有方法避免。
iOS11出了安全區(qū)域SafeArea這個概念,用得好可以解決以上問題。要點時間適應。
弊端是目前大多App都支持iOS11.0-,這樣就要寫判斷版本號,代碼多將近一倍。
優(yōu)點也很明顯
如果蘋果新出了新機型,不用改動代碼適應的可能性非常大。這意味著不用為了適配問題上線新版本。
橫屏時頂部不會有偏移。有橫屏需求的話,也許就不用為了橫屏做額外適配。
等以后App iOS11.0起步的時候(短時間不太可能qaq),個人感覺SafeArea將會成為主流。
但至少目前,看上去很美好,實際上適配寫多一倍的代碼讓筆者望而生畏。
4.SafeAreaiOS7以后,蘋果給UIViewController引入了topLayoutGuide 和 bottomLayoutGuide兩個屬性。用于表示頂部或底部的高度。根據有無顯示狀態(tài)欄、導航欄、tabBar返回高度。如果VC內嵌VC,內嵌的VC將視為另起的頂底坐標體系,不受原狀態(tài)欄等影響。你可能聽都沒聽過這兩屬性,因為開發(fā)中我們習慣直接用宏寫上數值,幾乎不用這兩個屬性。更何況那時iPhone還沒有全面屏這種東西。
到了iOS11,蘋果棄用了topLayoutGuide和bottomLayoutGuide兩個屬性。引入了safeArea代替。官方的建議是? 不能被遮擋的內容和控件在安全區(qū)域范圍內顯示。如果視圖底部有按鈕,在全面屏下,請約束底部距離34,不要影響到Home功能。
safeAreaLayoutGuide
此屬性適用于自動布局。

使用前
[NSLayoutConstraintconstraintWithItem:someView?attribute:NSLayoutAttributeToprelatedBy:NSLayoutRelationEqualtoItem:self.view?attribute:NSLayoutAttributeTopmultiplier:1.0constant:0];
使用后
[NSLayoutConstraintconstraintWithItem:someView?attribute:NSLayoutAttributeToprelatedBy:NSLayoutRelationEqualtoItem:self.view.safeAreaLayoutGuide?attribute:NSLayoutAttributeTopmultiplier:1.0constant:0];
筆者用的是Masonry。注意該屬性是iOS11后出現(xiàn)的。因為X發(fā)布時,最低版本超過了11,所以全面屏都能用此屬性。在這里可以看出,因為11不支持,這代碼多了一倍。我們完全可以不用這新屬性,減少一半代碼爽歪歪。所以筆者目前開發(fā)還未使用。
[testViewmas_makeConstraints:^(MASConstraintMaker?*make)?{
make.height.equalTo(@44);
if(@available(iOS11.0,*))?{
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
make.left.equalTo(self.view.mas_safeAreaLayoutGuideLeft);
make.right.equalTo(self.view.mas_safeAreaLayoutGuideRight);
}else{
make.top.equalTo(self.view).offset(KStatusBarHeight);
make.left.equalTo(self.view);
make.right.equalTo(self.view);
}
}];
筆者沒有全面屏,只能展示一下熱點了。該效果與用宏KStatusBarHeight一樣。
(注:此手機系統(tǒng)12.0)

但橫屏時就不一樣了。宏寫法會與上方有一段KStatusBarHeight距離,此方法沒有。這就是筆者說的其中一個優(yōu)點,然而并沒有好到足夠讓筆者用它的程度。
最后有兩個觀點。
對于在ViewController的view,推薦使用mas_safeAreaLayoutGuide。這樣就能動態(tài)更改,即使橫屏。
對于在View之間的約束,推薦使用mas_left。一來沒必要用safeArea,二來不用判斷版本號,減少代碼量。
safeAreaInsets
此屬性適用于手動計算frame。
X豎屏時控制器view的safeAreaInsets是(44,0,34,0);橫屏(0, 44, 21, 44)。

用到的是這個方法- (void)viewSafeAreaInsetsDidChange;
-?(void)viewDidLoad?{
[superviewDidLoad];
self.view.backgroundColor?=?[UIColorredColor];
UIView*testView?=?[[UIViewalloc]?initWithFrame:CGRectMake(0,?KStatusBarHeight,?KScreenWidth,200)];
testView.backgroundColor?=?[UIColorblackColor];
[self.view?addSubview:testView];
self.testView?=?testView;
}
-?(void)viewSafeAreaInsetsDidChange?{
[superviewSafeAreaInsetsDidChange];
NSLog(@"%s",?__func__);
[selfupdateFrame];
}
-?(void)updateFrame?{
if(@available(iOS11.0,?*))?{
CGRectnewFrame?=self.testView.frame;
newFrame.origin.x?=self.view.safeAreaInsets.left;
newFrame.size.width?=?KScreenWidth?-self.view.safeAreaInsets.left?-self.view.safeAreaInsets.right;
self.testView.frame?=?newFrame;
}
}
用frame的話,不僅低于11的系統(tǒng),就連高于11的系統(tǒng),要適配起橫屏問題都比較麻煩。
所以筆者是趨向于用約束的,見仁見智吧。
這里再貼出兩篇說SafeArea的文章。
筆者很久不用xib了,貼出現(xiàn)成的一篇文章。
iOS XIB使用Safe Area后在iOS9和10上面出現(xiàn)的問題和解決方案
4.automaticallyAdjustsScrollViewInsets 和 contentInsetAdjustmentBehavior
automaticallyAdjustsScrollViewInsets:
在iOS7.0以后,相對于ScrollView新增屬性,默認為YES,系統(tǒng)會根據所在界面的astatus bar, search bar, navigation bar, toolbar, or tab bar等自動調整ScrollView的inset。
-?(void)viewDidLoad?{
[superviewDidLoad];
self.title?=@"我是導航條";
self.view.backgroundColor?=?[UIColorredColor];
UITableView*testTableView?=?[[UITableViewalloc]?initWithFrame:CGRectMake(0,0,?KScreenWidth,?KScreenHeight)?style:UITableViewStylePlain];
testTableView.backgroundColor?=?[UIColorblueColor];
testTableView.delegate?=self;
testTableView.dataSource?=self;
[self.view?addSubview:testTableView];
}
-?(UITableViewCell*)tableView:(UITableView*)tableView?cellForRowAtIndexPath:(NSIndexPath*)indexPath?{
UITableViewCell*cell?=?[tableView?dequeueReusableCellWithIdentifier:@"cell"];
if(!cell)?{
cell?=?[[UITableViewCellalloc]?initWithStyle:UITableViewCellStyleDefaultreuseIdentifier:@"cell"];
}
cell.textLabel.text?=?[NSStringstringWithFormat:@"%ld",?(long)indexPath.row];
returncell;
}
-?(NSInteger)tableView:(UITableView*)tableView?numberOfRowsInSection:(NSInteger)section?{
return20;
}
可以看到沒有改變tableView的frame,只是顯示范圍變了。
如果沒有這個屬性,我們要實現(xiàn)同樣的效果,tableView尺寸要這樣設置。當然手動修改insets也是可以的。
x=0,
y?=?KStatusBarAndNavigationBarHeight,
width?=?KScreenWidth,
height?=?KScreenHeight?-?KStatusBarAndNavigationBarHeight
但是要注意:這種自動調整是在ScrollView是其根視圖添加的的第一個控件的時候,才會出現(xiàn)自動調整的效果。詳情查看automaticallyAdjustsScrollViewInsets屬性
contentInsetAdjustmentBehavior
iOS11中廢棄了automaticallyAdjustsScrollViewInsets,取而代之的是contentInsetAdjustmentBehavior屬性。
該屬性原理和automaticallyAdjustsScrollViewInsets原理相似,是為了進一步適應安全區(qū)域。
如果你需要自定義內邊距,代碼將變成以下這樣。
if(@available(iOS11.0,?*))?{
self.tableView.contentInsetAdjustmentBehavior?=UIScrollViewContentInsetAdjustmentNever;
}else{
self.automaticallyAdjustsScrollViewInsets?=NO;
}
contentInsetAdjustmentBehavior各個值之間的區(qū)別
鏈接里有這觀點
最后來談一下關于全面屏的適配方案。
支持橫屏的App,每個界面帶ScrollView的吧。
如果你的App最低支持iOS11.0,那么用safeAreaLayoutGuide約束將非常刺激。橫屏基本不用管,除非排版需求不一樣。
如果你的App要適配11.0-,不支持橫屏。那么采用宏來寫目前是主流。只是以后出了新類型可能要稍微修改一下宏,并且運行逐個界面檢查。目前11.0起步的App畢竟少數,用safeAreaLayoutGuide反而要多判斷版本號。看以后App支持版本號趨勢吧。
如果你的App要適配11.0-,支持橫屏。建議用約束。用frame可能寫死人,自求多福吧。
橫屏有時實在沒辦法解決某些問題的話,就寫兩套。關于iOS橫豎屏適配
電話、熱點狀態(tài)欄問題
有電話打進來、手機開了熱點有連接,狀態(tài)欄會變長20。雖然很多App并未對這些情況適配,但優(yōu)秀的App應該要處理好。
橫屏和電話熱點并無直接關系。橫屏狀態(tài)下默認狀態(tài)欄是不顯示的。
當狀態(tài)欄增高時,App的控制器的view將會下移20,但是高度卻不變。tabBar不會有任何改變。所以如果某個界面scrollView一直到底的話,最好用約束到底部,這樣調用viewWillLayoutSubviews時就會修改scrollView高度。建議不要寫高度,不然會出現(xiàn)scrollView底部顯示丟失20高度的問題。
個人總結的UI在不同尺寸下適配方案
這時就要說回上面提到的宏了。
#defineKScaleWidth(width)?((width)*(KScreenWidth/375.f))
#defineIsIphone6P??????????SCREEN_WIDTH==414
#defineSizeScale???????????(IsIphone6P???1.5?:?1)
#definekFontSize(value)????value*SizeScale
#definekFont(value)????????[UIFont?systemFontOfSize:value*SizeScale]
1.UILabel的適配問題
相信開發(fā)過程中都遇到過字體適配問題。
UIFont
App如果要設置全局字體,可以通過Swizzing修改。或者像以上宏一樣,傳進參數,修改字體大小。
看過一篇文章,淘寶在Plus機型的字體都加大成1.5倍。筆者買不起Plus機型,更不敢裝淘寶這種剁手App,所以無法驗證。
本文要探究的是UILabel顯示的問題。重點并不在UIFont上。
先研究UILabel。
UILabel默認字體為[UIFont systemFontOfSize:17];// 每個中文文字寬度為17。但字母、數字寬度并非固定。例如1比0瘦。注意這將可能是個坑。
numberOfLines只有設定了寬度約束的情況下起效。否則Label只會顯示一行。
UILabel默認垂直方向會居中顯示,即當Label高度高于text高度,文字在垂直方向上居中。水平方向上textAlignment可以設置。
如果要設置成頂部對齊,有好幾種方法。最粗暴的是用UITextView。
lineBreakMode設置文字過長時省略號放哪。
label.lineBreakMode?=NSLineBreakByCharWrapping;以字符為顯示單位顯
示,后面部分省略不顯示。
label.lineBreakMode?=NSLineBreakByClipping;剪切與文本寬度相同的內
容長度,后半部分被刪除。
label.lineBreakMode?=NSLineBreakByTruncatingHead;前面部分文字
以……方式省略,顯示尾部文字內容。
label.lineBreakMode?=NSLineBreakByTruncatingMiddle;中間的內容
以……方式省略,顯示頭尾的文字內容。
label.lineBreakMode?=NSLineBreakByTruncatingTail;結尾部分的內容
以……方式省略,顯示頭的文字內容。
label.lineBreakMode?=NSLineBreakByWordWrapping;以單詞為顯示單位顯
示,后面部分省略不顯示。
adjustsFontSizeToFitWidth //設置字體大小適應label寬度
minimumScaleFactor
sizeToFit
改變Label的尺寸以顯示文字。需要注意的是,需要在label.text賦值后執(zhí)行。如果寬高都進行了約束,那么調用sizeToFit將無效果。如果只約束了寬度,并且行數非1,那么sizeToFit會修改Label的高度;如果只約束了高度,或者行數為1,那么sizeToFit只會修改Label的寬度。如果二者皆未約束,只會修改Label寬度。
sizeToFit和adjustsFontSizeToFitWidth的區(qū)別。
從字面上我們就能區(qū)分開,前者是改變Label的寬高,后者是改變字體大小。
反正以后做項目的時候,明確需求,我們是固定了字體的大小來適配label的寬,還是固定了label的寬來適配字體的大小,前者用sizeToFit,后者用adjustFontsToFit。
以淘寶舉例。為了研究只能下這剁手App了。
se下搜iPhoneX

6s下搜iPhoneX

這里無視Label左邊的圖標(天貓、雙11)。
其商品標題有兩行。之前提到了,如果不作處理,Label默認垂直方向居中。如果文字長度只有一行,那會顯示奇怪。所以筆者來約束的話,先做垂直方向處理,并且約束了寬度距離左邊圖片,距離cell.contentView右邊。然后設置行數為2(或者約束高度,行數為0,lineBreakMode裁剪)。
還有一種方案是Label根據文字長度自動適配高度,并設置最大高度限制。輸入框高度就用類似方案。網上搜UITextView自動高度一堆教程。筆者懶得翻就不弄了?;驹硎歉鶕淖执笮¢L度、Label的寬度、文字間距,算出文字高度,然后設置Label高度為 最大限制高度與文字高度 較小者。
再來看價格和付款人數Label。
筆者設計的話,會采用以下形式。
priceLabel.font?=?...
[priceLabel?mas_makeConstraints:^(MASConstraintMaker?*make)?{
make.left.equalTo(imageView.mas_right).offset(...);
make.height.equalTo(...);
make.top...;
}];
numberOfPeopleLabel.font?=?...
[numberOfPeopleLabel?mas_makeConstraints:^(MASConstraintMaker?*make)?{
make.left.equalTo(priceLabel.mas_right).offset(...);
make.height.equalTo(...);
make.top...;
}];
然后在cell設置model時
priceLabel.text?=?...
[priceLabel?sizeToFit];
numberOfPeopleLabel.text?=?...
[numberOfPeopleLabel?sizeToFit];
總結一下,開發(fā)中很少會用到adjustsFontSizeToFitWidth,大多數時候都會頂部對齊、換行、裁剪,或設置自動高度。
2.UI在不同尺寸的適配
來說最后一個非常有用的宏。
#defineKScaleWidth(width)?((width)*(KScreenWidth/375.f))
筆者遇到的設計師給的圖都是i6屏幕,其寬度為375.f。如果給的圖不是,那么將這個宏數值修改即可。
這個宏有什么用呢?
其實就是一個比例轉換的問題。不同屏幕下,某些UI可能大小不一樣,這時候采用這個宏將會非常方便。
還是舉回上面的兩張?zhí)詫殘D例子。
筆者目測(目測而已),cell是不同高度的。假如6s中商品圖的寬度150.f,占屏寬0.4。在se中按照比例,320 * 0.4,為128。
那么我們用這個宏,就能一步到位。KScaleWidth(150),在6s中就為150,在se中為128。
除此之外,間距約束用這個宏也有奇效。
這個宏在collectionView中更顯神威。
設計圖算好了兩個cell的間距,每個cell的大小,整個collectionView的大小,contentInset。這時我們采用這個宏,在不同屏幕下的適配問題將迎刃而解。
這里為什么不寫出KScaleHeight呢?
筆者并不是說不能用,只是view通常是被寬度所限制。你見過微信的cell文本內容高度有變化嗎hhhhh。就像圖片尺寸變了,高度也是被圖片寬度帶動,而不是屏幕高度。
當然此宏雖很有用,但是開發(fā)中還是要經過考慮哪些地方需要用。
作者:Hsusue
鏈接:https://juejin.im/post/5bd2a094518825289f7f3d17