蘋(píng)果公司于9月份如期發(fā)布了新的iPhone-iPhone8,iPhone8 Plus,iPhoneX,前兩個(gè)不用多說(shuō),正常形態(tài)的iPhone和前代外觀上沒(méi)有太大區(qū)別。iPhoneX則帶來(lái)了不同的樣式,不同的體驗(yàn),18:9的全面屏屏幕,小劉海,去掉Home鍵后超前的交互方式。當(dāng)然對(duì)于開(kāi)發(fā)者也帶來(lái)了對(duì)于這塊屏幕的適配問(wèn)題。我想蘋(píng)果爸爸決定在11月初開(kāi)售iPhoneX也有一部分讓開(kāi)發(fā)者對(duì)自己的App做iPhoneX適配的一部分原因,畢竟現(xiàn)在Xcode已經(jīng)有iPhoneX的模擬器了。
過(guò)去,我們拿到的手機(jī)是方方正正的矩形,所以整個(gè)屏幕都可以看做是安全區(qū)域Safe Area,而如今由于iPhone X屏幕上的“劉?!币约捌聊凰闹懿捎脠A角的設(shè)計(jì),需要設(shè)計(jì)師對(duì)繪圖區(qū)域做出調(diào)整。蘋(píng)果給出的安全區(qū)域如下

頁(yè)面內(nèi)容不能超出安全區(qū)域(Safe Area)

下面我們以通訊錄和News應(yīng)用為例看下iPhoneX模擬器中原生應(yīng)用對(duì)于這塊全面屏如何適配的

通過(guò)例子我們可以發(fā)現(xiàn)主要的三點(diǎn)原則:
- 帶有空間按鈕的頂部導(dǎo)航欄(NavigationBar)要處在“劉?!毕旅?/li>
- 底部導(dǎo)航欄(Tabbar)不能在虛擬橫條Home鍵(不知道咋叫,暫且這么叫吧)下面,也就是說(shuō)要和屏幕底部保持距離
- 可滾動(dòng)的列表整塊屏幕都是可展示的,但是滾動(dòng)條要和頂部和底部保持距離不能超出
基本以上三點(diǎn)原則可以概括為一句話,所有不可滾動(dòng)的控件推薦在安全區(qū)域內(nèi)展示,可滾動(dòng)的控件整個(gè)屏幕都可以用來(lái)展示
這么做也算是充分利用了這塊屏幕,并且不影響用戶正常使用iPhoneX了
Toon的適配
初期Toon對(duì)于iPhoneX的適配基本為0,所以出現(xiàn)不少問(wèn)題,主要集中以下幾點(diǎn):
- 頂部導(dǎo)航欄和底部導(dǎo)航欄超出安全區(qū)域
- 沒(méi)有導(dǎo)航欄的列表沒(méi)有全屏展現(xiàn)
- 吸底按鈕超出安全區(qū)域
- 頂部導(dǎo)航欄UI錯(cuò)亂
- 列表加載控件在安全區(qū)域外部展示
下面通過(guò)一個(gè)Gif圖來(lái)看下未經(jīng)過(guò)適配的Toon的部分界面在iPhoneX上表現(xiàn)

可見(jiàn)未經(jīng)適配的Toon將會(huì)以16:9的樣子展現(xiàn)在用戶手中,這對(duì)于產(chǎn)品在iPhoneX中的體驗(yàn)來(lái)說(shuō)將會(huì)是極大的災(zāi)難,沒(méi)有充分利用iPhoneX的全面屏,用戶體驗(yàn)將是缺失的
經(jīng)過(guò)一段時(shí)間的適配,現(xiàn)在開(kāi)發(fā)版的Toon在iPhoneX上已經(jīng)可以有良好的展示了,雖然還有很多地方?jīng)]有經(jīng)過(guò)重新設(shè)計(jì)和優(yōu)化,不過(guò)已經(jīng)利用了iPhoneX的屏幕展示了

經(jīng)過(guò)一段時(shí)間的適配,解決了16:9展示,導(dǎo)航欄錯(cuò)位等問(wèn)題,在的問(wèn)題主要集中在列表底部加載控件的問(wèn)題等問(wèn)題上,接下來(lái)本文將通過(guò)Demo和Toon的部分界面來(lái)具體講一下iPhoneX UI適配上的問(wèn)題
啟動(dòng)頁(yè)的適配
如果對(duì)于啟動(dòng)頁(yè)不做任何適配,那么App啟動(dòng)后你會(huì)發(fā)現(xiàn)應(yīng)用是16:9的樣式展示的
解決方案有兩種:
- Xib或者Stroyboard來(lái)作為應(yīng)用的啟動(dòng)圖
- 添加iPhoneX下啟動(dòng)頁(yè)的圖片
Toon工程中采取的方案是第二種

頂部的適配
以前通過(guò)加減20來(lái)覆蓋或者避免狀態(tài)的代碼都會(huì)出問(wèn)題在iPhoneX上

狀態(tài)欄高度不是20了,iOS11安全區(qū)的提出,在iPhoneX上狀態(tài)欄的高變?yōu)?4
代碼中需要通過(guò)[UIApplication sharedApplication].statusBarFrame.size.height獲取狀態(tài)欄高度
[self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset([UIApplication sharedApplication].statusBarFrame.size.height);
make.bottom.equalTo(self.view.mas_bottom);
make.left.equalTo(self.view.mas_left);
make.right.equalTo(self.view.mas_right);
}];

iOS11automaticallyAdjustsScrollViewInsets屬性廢棄了會(huì)出現(xiàn)ScorllView下沉20的現(xiàn)象

可以調(diào)用scrollview新的apicontentInsetAdjustmentBehavior
self.automaticallyAdjustsScrollViewInsets = NO;
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}

但是這么寫(xiě)會(huì)導(dǎo)致在iPhoneX下出現(xiàn),由于在X下安全區(qū)域的出現(xiàn),頂部異形區(qū)域不建議覆蓋,會(huì)造成視覺(jué)的差異

在代碼中我們需要來(lái)根據(jù)設(shè)備高度來(lái)判斷iPhoneX,從而來(lái)避免這種情況
[self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
if (CGRectGetHeight([UIScreen mainScreen].bounds) == 812.0) {
if (@available(iOS 11.0, *)) {
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
}
} else {
make.top.equalTo(self.view.mas_top);
}
make.bottom.equalTo(self.view.mas_bottom);
make.left.equalTo(self.view.mas_left);
make.right.equalTo(self.view.mas_right);
}];

如果用了MJRefresh在iPhoneX下列表頂部會(huì)出現(xiàn)這樣的情況,頂部刷新控件會(huì)有露出,UI不美觀

如果設(shè)置contentInsetAdjustmentBehavior為UIScrollViewContentInsetAdjustmentNever,并且設(shè)置頂部距離為導(dǎo)航欄距離,又會(huì)造成全面屏展示不充分也不是很好
- (void)viewDidLoad {
[super viewDidLoad];
<!--省略部分代碼-->
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
<!--省略部分代碼-->
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset(self.view.layoutMargins.top);
<!--省略部分代碼-->
}];
}

我建議的適配方式,根據(jù)具體情況來(lái)設(shè)置contentInset的值
- (void)viewDidLoad {
[super viewDidLoad];
<!--省略部分代碼-->
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
<!--省略部分代碼-->
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (CGRectGetHeight([UIScreen mainScreen].bounds) == 812.0) {
// 只在iPhoneX下適配
if (@available(iOS 11.0, *)) {
self.tableView.contentInset = UIEdgeInsetsMake(self.view.safeAreaInsets.top, 0, 0, 0);
}
}
[self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset(0);
<!--省略部分代碼-->
}];
}
使用以上代碼,或者UI設(shè)計(jì)頂部刷新控件都樣式都可以解決該問(wèn)題,但是我覺(jué)得最終極的解決方案還是UI設(shè)計(jì)根據(jù)iPhoneX的異性全面屏給以良好的適配方案,如果有更好的設(shè)計(jì)方案,比如當(dāng)列表為初始滾動(dòng)狀態(tài)時(shí)不顯示頂部刷新控件,可以跟我交流

頂部的適配問(wèn)題主要集中體現(xiàn)在以前通過(guò)寫(xiě)死狀態(tài)高度20造成的,對(duì)于這個(gè)問(wèn)題,只要調(diào)用系統(tǒng)提供獲取狀態(tài)欄高度的方法,就可以避免,至于頂部刷新控件的問(wèn)題,這個(gè)本文建議采取和本文建議的處理底部加載控件的方案來(lái)實(shí)施,具體可以繼續(xù)看下文
底部的適配
底部導(dǎo)航欄
如果是采用系統(tǒng)默認(rèn)的底部導(dǎo)航欄,沒(méi)有采用自定義的方式,底部導(dǎo)航欄iOS系統(tǒng)級(jí)就做了處理,會(huì)保證在Tabbar是在安全區(qū)域之內(nèi)
如果是采取自定義的方式那么則要對(duì)做出響應(yīng)的處理
+ (CGFloat)computeTabbarHeight {
NSInteger style = [[TNAppStackManager shareInstance] rootStyle];
if (style == RootControllerStyle_TabCircleDrawer || style == RootControllerStyle_TabCircleNoDrawer) {
return 70.;
} else if (style == RootControllerStyle_TabNormal) {
return [[UIDevice currentDevice] systemVersion].doubleValue >= 11.0 ? (fabs(CGRectGetHeight([UIScreen mainScreen].bounds) - 812.) >= 1.0 ? 49. : 83.) : 53.;
}
return 0;
}
以上是Toon工程在處理底部導(dǎo)航欄高度的示例代碼,通過(guò)系統(tǒng)版本和設(shè)備來(lái)判斷具體導(dǎo)航欄的高度
列表底部加載控件的的處理
列表的底部加載控件和在全屏下的頂部刷新控件的問(wèn)題是我認(rèn)為不不好給出解決方案的問(wèn)題
iOS11廢棄了原有的automaticallyAdjustsScrollViewInsets屬性,為scrollview添加了新的屬性
contentInsetAdjustmentBehavior
現(xiàn)在對(duì)于我列表的適配,我看大都是這個(gè)樣子的
self.automaticallyAdjustsScrollViewInsets = NO;
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
這兩個(gè)屬性都是為了讓列表對(duì)于全屏和異形屏幕下有良好的展示設(shè)計(jì)的,對(duì)于非全屏狀態(tài)下的列表,這兩個(gè)屬性不處理沒(méi)有關(guān)系,因?yàn)橹挥性谌粱蛘甙肴粒ㄖ挥许敳繉?dǎo)航或者)
對(duì)于Toon來(lái)說(shuō),剛開(kāi)我們對(duì)于所有的列表都將上面的屬性置為了UIScrollViewContentInsetAdjustmentNever,這樣在iPhoneX部分界面就變成這樣了

會(huì)發(fā)現(xiàn)在iPhoneX下,tableView展示區(qū)域是到底的,這樣會(huì)影響用戶使用home虛擬橫條,所以這個(gè)值是需要根據(jù)具體情況分析的,例如如果tableView是全屏展示的就需要設(shè)置為UIScrollViewContentInsetAdjustmentNever
在此基礎(chǔ)上需要適配就是tableView的刷新控件和加載控件了,假設(shè)大家使用的都是MjRefresh,那么對(duì)于刷新控件出現(xiàn)的問(wèn)題上文已經(jīng)講過(guò)了,不在贅述。我們來(lái)討論下加載控件會(huì)出現(xiàn)的問(wèn)題。
刷新控件還好,大部分刷新控件都是在有頂部導(dǎo)航欄的情況下,可是底部加載控件不同,又很多處理方式,本文只做一個(gè)拋磚引玉的示例,具體處理方式還是要結(jié)合產(chǎn)品、UI、技術(shù)來(lái)以前討論針對(duì)具體情況具體分析,接下來(lái)我將會(huì)以Toon中小組模塊我的評(píng)論界面為例,給出我的解決方案
如果contentInsetAdjustmentBehavior設(shè)置為UIScrollViewContentInsetAdjustmentNever,那么出現(xiàn)的問(wèn)題是,底部加載控件會(huì)在安全區(qū)域意外露出。

為了明顯我講底部加載控件的背景色置成了橙色,可以看到正常情況,加載控件是暴露在安全區(qū)域外部,上面的文字也能看到,這樣一來(lái)既不沒(méi)關(guān)也顯得不夠?qū)I(yè),并且文字也被home虛擬橫條擋住了
那么怎么處理這種情況才會(huì)更好些呢,本文給的解決方案是給底部加載控件加一個(gè)遮罩,而這個(gè)遮罩是根據(jù),tableView的偏移量來(lái)展示的,最后的效果如下。

核心代碼如下:
- (void)setLoadFooter {
self.tableView.mj_footer = [MJRefreshBackStateFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadCommentData)];
self.tableView.mj_footer.backgroundColor = [UIColor orangeColor];
self.tableView.mj_footer.maskView = [[UIView alloc] init];
self.tableView.mj_footer.maskView.backgroundColor = [UIColor whiteColor];
[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (object == self.tableView && [keyPath isEqualToString:@"contentOffset"]) {
if (@available(iOS 11.0, *)) {
/*
判斷設(shè)備為iPhoneX時(shí),
并且contentInsetAdjustmentBehavior不為UIApplicationBackgroundFetchIntervalNever
*/
if (CGRectGetHeight([UIScreen mainScreen].bounds) == 812.0 && self.tableView.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentAutomatic) {
CGFloat distanceToSafeBottom = (self.tableView.contentOffset.y + CGRectGetHeight(self.tableView.frame) - self.view.safeAreaInsets.bottom) - self.tableView.contentSize.height;
if (distanceToSafeBottom < 0) {
self.tableView.mj_footer.maskView.frame = CGRectZero;
} else {
CGFloat showFooterHeight = distanceToSafeBottom;
if (showFooterHeight > CGRectGetHeight(self.tableView.mj_footer.bounds)) {
showFooterHeight = CGRectGetHeight(self.tableView.mj_footer.bounds);
}
if (self.tableView.mj_footer.state != MJRefreshStateRefreshing) {
self.tableView.mj_footer.maskView.frame = CGRectMake(0, 0, CGRectGetWidth(self.tableView.mj_footer.bounds), showFooterHeight);
}
}
}
}
}
}