蘋果公司于9月份如期發布了新的iPhone-iPhone8,iPhone8 Plus,iPhoneX,前兩個不用多說,正常形態的iPhone和前代外觀上沒有太大區別。iPhoneX則帶來了不同的樣式,不同的體驗,18:9的全面屏屏幕,小劉海,去掉Home鍵后超前的交互方式。當然對于開發者也帶來了對于這塊屏幕的適配問題。我想蘋果爸爸決定在11月初開售iPhoneX也有一部分讓開發者對自己的App做iPhoneX適配的一部分原因,畢竟現在Xcode已經有iPhoneX的模擬器了。
過去,我們拿到的手機是方方正正的矩形,所以整個屏幕都可以看做是安全區域Safe Area,而如今由于iPhone X屏幕上的“劉海”以及屏幕四周采用圓角的設計,需要設計師對繪圖區域做出調整。蘋果給出的安全區域如下
頁面內容不能超出安全區域(Safe Area)
下面我們以通訊錄和News應用為例看下iPhoneX模擬器中原生應用對于這塊全面屏如何適配的
通過例子我們可以發現主要的三點原則:
- 帶有空間按鈕的頂部導航欄(NavigationBar)要處在“劉海”下面
- 底部導航欄(Tabbar)不能在虛擬橫條Home鍵(不知道咋叫,暫且這么叫吧)下面,也就是說要和屏幕底部保持距離
- 可滾動的列表整塊屏幕都是可展示的,但是滾動條要和頂部和底部保持距離不能超出
基本以上三點原則可以概括為一句話,所有不可滾動的控件推薦在安全區域內展示,可滾動的控件整個屏幕都可以用來展示
這么做也算是充分利用了這塊屏幕,并且不影響用戶正常使用iPhoneX了
Toon的適配
初期Toon對于iPhoneX的適配基本為0,所以出現不少問題,主要集中以下幾點:
- 頂部導航欄和底部導航欄超出安全區域
- 沒有導航欄的列表沒有全屏展現
- 吸底按鈕超出安全區域
- 頂部導航欄UI錯亂
- 列表加載控件在安全區域外部展示
下面通過一個Gif圖來看下未經過適配的Toon的部分界面在iPhoneX上表現
可見未經適配的Toon將會以16:9的樣子展現在用戶手中,這對于產品在iPhoneX中的體驗來說將會是極大的災難,沒有充分利用iPhoneX的全面屏,用戶體驗將是缺失的
經過一段時間的適配,現在開發版的Toon在iPhoneX上已經可以有良好的展示了,雖然還有很多地方沒有經過重新設計和優化,不過已經利用了iPhoneX的屏幕展示了
經過一段時間的適配,解決了16:9展示,導航欄錯位等問題,在的問題主要集中在列表底部加載控件的問題等問題上,接下來本文將通過Demo和Toon的部分界面來具體講一下iPhoneX UI適配上的問題
啟動頁的適配
如果對于啟動頁不做任何適配,那么App啟動后你會發現應用是16:9的樣式展示的
解決方案有兩種:
- Xib或者Stroyboard來作為應用的啟動圖
- 添加iPhoneX下啟動頁的圖片
Toon工程中采取的方案是第二種
頂部的適配
以前通過加減20來覆蓋或者避免狀態的代碼都會出問題在iPhoneX上
狀態欄高度不是20了,iOS11安全區的提出,在iPhoneX上狀態欄的高變為44
代碼中需要通過[UIApplication sharedApplication].statusBarFrame.size.height
獲取狀態欄高度
[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屬性廢棄了
會出現ScorllView下沉20的現象
可以調用scrollview新的apicontentInsetAdjustmentBehavior
self.automaticallyAdjustsScrollViewInsets = NO;
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
但是這么寫會導致在iPhoneX下出現,由于在X下安全區域的出現,頂部異形區域不建議覆蓋,會造成視覺的差異
在代碼中我們需要來根據設備高度來判斷iPhoneX,從而來避免這種情況
[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下列表頂部會出現這樣的情況,頂部刷新控件會有露出,UI不美觀
如果設置contentInsetAdjustmentBehavior
為UIScrollViewContentInsetAdjustmentNever
,并且設置頂部距離為導航欄距離,又會造成全面屏展示不充分也不是很好
- (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);
<!--省略部分代碼-->
}];
}
我建議的適配方式,根據具體情況來設置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設計頂部刷新控件都樣式都可以解決該問題,但是我覺得最終極的解決方案還是UI設計根據iPhoneX的異性全面屏給以良好的適配方案,如果有更好的設計方案,比如當列表為初始滾動狀態時不顯示頂部刷新控件,可以跟我交流
頂部的適配問題主要集中體現在以前通過寫死狀態高度20造成的,對于這個問題,只要調用系統提供獲取狀態欄高度的方法,就可以避免,至于頂部刷新控件的問題,這個本文建議采取和本文建議的處理底部加載控件的方案來實施,具體可以繼續看下文
底部的適配
底部導航欄
如果是采用系統默認的底部導航欄,沒有采用自定義的方式,底部導航欄iOS系統級就做了處理,會保證在Tabbar是在安全區域之內
如果是采取自定義的方式那么則要對做出響應的處理
+ (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工程在處理底部導航欄高度的示例代碼,通過系統版本和設備來判斷具體導航欄的高度
列表底部加載控件的的處理
列表的底部加載控件和在全屏下的頂部刷新控件的問題是我認為不不好給出解決方案的問題
iOS11廢棄了原有的automaticallyAdjustsScrollViewInsets
屬性,為scrollview添加了新的屬性
contentInsetAdjustmentBehavior
現在對于我列表的適配,我看大都是這個樣子的
self.automaticallyAdjustsScrollViewInsets = NO;
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
這兩個屬性都是為了讓列表對于全屏和異形屏幕下有良好的展示設計的,對于非全屏狀態下的列表,這兩個屬性不處理沒有關系,因為只有在全屏或者半全屏(只有頂部導航或者)
對于Toon來說,剛開我們對于所有的列表都將上面的屬性置為了UIScrollViewContentInsetAdjustmentNever
,這樣在iPhoneX部分界面就變成這樣了

會發現在iPhoneX下,tableView展示區域是到底的,這樣會影響用戶使用home虛擬橫條,所以這個值是需要根據具體情況分析的,例如如果tableView是全屏展示的就需要設置為UIScrollViewContentInsetAdjustmentNever
在此基礎上需要適配就是tableView的刷新控件和加載控件了,假設大家使用的都是MjRefresh,那么對于刷新控件出現的問題上文已經講過了,不在贅述。我們來討論下加載控件會出現的問題。
刷新控件還好,大部分刷新控件都是在有頂部導航欄的情況下,可是底部加載控件不同,又很多處理方式,本文只做一個拋磚引玉的示例,具體處理方式還是要結合產品、UI、技術來以前討論針對具體情況具體分析,接下來我將會以Toon中小組模塊我的評論界面為例,給出我的解決方案
如果contentInsetAdjustmentBehavior
設置為UIScrollViewContentInsetAdjustmentNever
,那么出現的問題是,底部加載控件會在安全區域意外露出。
為了明顯我講底部加載控件的背景色置成了橙色,可以看到正常情況,加載控件是暴露在安全區域外部,上面的文字也能看到,這樣一來既不沒關也顯得不夠專業,并且文字也被home虛擬橫條擋住了
那么怎么處理這種情況才會更好些呢,本文給的解決方案是給底部加載控件加一個遮罩,而這個遮罩是根據,tableView的偏移量來展示的,最后的效果如下。
核心代碼如下:
- (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, *)) {
/*
判斷設備為iPhoneX時,
并且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);
}
}
}
}
}
}