適配iPhoneX & iOS11

Mitsui_發表於2018-01-02

一、Screen Size

iPhoneX 的螢幕尺寸為 375pt × 812pt @3x,畫素為 1125px × 2436px。可以通過判斷螢幕的高度來判斷裝置是否是 iPhoneX,可以在全域性巨集定義中新增判斷裝置的巨集定義(橫豎屏通用):

#define IS_IPHONE_X     (( fabs((double)[[UIScreen mainScreen] bounds].size.height - (double)812) < DBL_EPSILON ) || (fabs((double)[[UIScreen mainScreen] bounds].size.width - (double)812) < DBL_EPSILON ))
複製程式碼

如果在 iPhoneX模擬器執行現有 app,出現上下螢幕沒填充滿的情況時,說明 app 沒有適合 iPhoneX 尺寸的啟動圖,因此,需要新增一張 1125px × 2436px(@3x)的啟動圖,或者在專案中新增 LaunchScreen.xib,然後在專案的 target 中,設定啟動 Launch Screen File 為 LaunchScreen.xib。

二、safe Area

官方指出:

When designing for iPhone X, you must ensure that layouts fill the screen and aren't obscured by the device's rounded corners, sensor housing, or the indicator for accessing the Home screen.

當我們在設計 iPhoneX app 的時候,必須確保佈局充滿螢幕,並且佈局不會被裝置的圓角、感測器外殼或者用於訪問主螢幕的指示燈遮擋住。因此,蘋果提出了safe area(安全區)的概念,就是上述可能遮擋介面的區域以外的區域被定義為安全區。

豎屏
橫屏

為了儘可能的使佈局和手勢等不被圓角和感測器遮擋,豎屏情況下,蘋果官方建議的安全區大小為上圖(豎屏),指定的狀態列高度為 44pt,下方指示燈處的高度為 34pt;橫屏情況下為上圖(橫屏)所示,上下安全邊距分別為 0pt/21pt,左右安全邊距為 44pt/34pt,如果使用了UINavigationBarUITabBar,安全區的上邊緣會變成導航欄下邊緣的位置,如果是自定義的navigationBar,並且還繼承於UIView,就需要手動修改狀態列的高度,在我們的專案中,狀態列的高度是用的全域性巨集定義,因此,修改狀態列高度的巨集定義為:

#define STATUS_HEIGHT   (IS_IPHONE_X?44:20)
複製程式碼

增加安全區域下面的區域的高度巨集定義為:

#define BOTTOM_SAFEAREA_HEIGHT (IS_IPHONE_X? 34 : 0)
複製程式碼

如果你同時也用了自定義的UITabBar那麼就需要修改TABBAR_HEIGHT的巨集定義為:

#define TABBAR_HEIGHT   (IS_IPHONE_X? (49 + 34) : 49)
複製程式碼

當需要將整個介面最下方的控制元件上移或者改變中間滾動檢視的高度的時候,使用BOTTOM_SAFEAREA_HEIGHT這個巨集,方便後期的統一維護。因為基本上每一個介面都需要下方留白,因此在BaseViewController新增屬性:

@property (nonatomic, strong) UIView *areaBelowSafeArea;

並且統一新增到view上:

#warning 背景色待定
if (IS_IPHONE_X) {
   self.areaBelowSafeArea = [[UIView alloc] initWithFrame:CGRectMake(0, SCREEN_HEIGHT - BOTTOM_SAFEAREA_HEIGHT, SCREEN_WIDTH, BOTTOM_SAFEAREA_HEIGHT)];
   // 儘量使用約束佈局
   self.areaBelowSafeArea.backgroundColor = DefaultTabBarBackgroundColor;
   [self.view addSubview:self.areaBelowSafeArea];
 }
複製程式碼

也可以在特定的viewController中,自定義它的樣式。

self.areaBelowSafeArea.backgroundColor = XXX;
複製程式碼

更詳細:Designing for iPhone X

三、UIScrollView及其子類

在 iOS11 中,決定滾動檢視的內容和邊緣距離的屬性改為adjustedContentInset,而不是原來的contentInsets,在 iOS11 之前,UIViewController有一個automaticallyAdjustsScrollViewInsets屬性,並且預設值為YES,這個屬性的作用為,當scrollView為控制器根檢視的最上層子檢視時,如果這個控制器被嵌入到UINavigationControllerUITabBarController中,那麼,它的contentInsets會自動設定為(64,0,49,0);這個屬性會使滾動檢視中的內容不被導航欄和tabBar遮擋。

iOS11 使用了safeAreaInsets的新屬性,這個屬性的作用就是規定了檢視的安全區的四個邊到螢幕的四個邊的距離,例如在 iPhoneX 上,如果沒使用或者隱藏了UINavigationBar,則safeAreaInsets = (44,0,0,34),如果既使用了UINavigationBar,又使用了UITabBar,則safeAreaInset = (88,0,0,34+49)。也可以使用additionalSafeAreaInsets屬性來為系統預設的safeAreaInsets新增 insets,比如,safeAreaInsets = (44,0,0,34),設定additionalSafeAreaInsets = UIEdgeInsetsMake(-44, 0, 0, 0);,那麼實際上的安全區域到螢幕邊緣的insets為(0,0,0,34)

adjustedContentInset屬性的值的確定由 iOS11 API 提供的新的列舉變數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));
複製程式碼

四個列舉值的意義分別為:

UIScrollViewContentInsetAdjustmentAutomatic

Content is always adjusted vertically when the scroll view is the content view of a view controller that is currently displayed by a navigation or tab bar controller. If the scroll view is horizontally scrollable, the horizontal content offset is also adjusted when there are nonzero safe area insets.

當滾動檢視的父檢視所在的控制器嵌入導航控制器和標籤控制器的時候,滾動檢視的內容總會調整垂直方向上的 insets,如果滾動檢視允許水平方向上可滾動,則當水平方向上的安全區 insets 不為 0 的時候,也會調整水平方向上的 insets。即:adjustedContentInset = safeAreaInsets + contentInsets,其中contentInsets為我們設定的滾動檢視的contentInsets,下同。如下程式碼,

self.scroll.contentInset = UIEdgeInsetsMake(100, 0, -34, 10);
if (@available(iOS 11.0, *)) {
    self.scroll.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
}else {
    self.automaticallyAdjustsScrollViewInsets = YES;
}
複製程式碼

新增導航欄的情況下,在 iPhoneX 裝置上執行列印的 log 為:

2017-10-20 11:48:56.664048+0800 TestIphoneX[81880:2541000] contentInset:{100, 0, -34, 10}
2017-10-20 11:48:56.664916+0800 TestIphoneX[81880:2541000] adjustedContentInset:{188, 0, 0, 10}
2017-10-20 11:48:56.665220+0800 TestIphoneX[81880:2541000] safeAreaInset:{88, 0, 34, 0}
複製程式碼

UIScrollViewContentInsetAdjustmentScrollableAxes

The top and bottom insets include the safe area inset values when the vertical content size is greater than the height of the scroll view itself. The top and bottom insets are also adjusted when the alwaysBounceVertical property is YES. Similarly, the left and right insets include the safe area insets when the horizontal content size is greater than the width of the scroll view.

adjustedContentInset = safeAreaInsets + contentInsets,它的成立依賴於滾動軸,當垂直方向上的contentSize大於滾動檢視的高度時,那麼垂直方向上的 insets 就由safeAreaInsets + contentInsets決定,水平方向上同理。

UIScrollViewContentInsetAdjustmentNever

Do not adjust the scroll view insets.

顧名思義,adjustedContentInset = contentInsets

UIScrollViewContentInsetAdjustmentAlways

Always include the safe area insets in the content adjustment.

顧名思義,adjustedContentInset = safeAreaInsets + contentInsets

由於我們的 APP 沒用系統的導航控制器,但是我們用了系統的標籤控制器,所以在專案中會存在 iOS11 下滾動檢視的位置不對的情況,那麼就可能是因為它的adjustedContentInset = safeAreaInsets + contentInsets造成的,可以這樣解決:

if (@available(iOS 11.0, *)) {
    self.scroll.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}else {
    self.automaticallyAdjustsScrollViewInsets = NO;
}
複製程式碼

四、UITableView sectionHeader 和 sectionFooter高度問題

iOS11 中 UITableView的 sectionHeader 和 sectionFooter 也啟用了 self-sizing,即通過估算的高度乘以個數來確定tableViewcontenSize的估算值,然後隨著滾動展示 section 和 cell 的過程中更新它的contenSize,iOS11 之前只有 cell 是採用的這個機制,iOS11中 sectionHeader 和 sectionFooter 也採用了這個機制,並且,如果只實現了

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
複製程式碼

沒實現

- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section;
複製程式碼

那麼系統就會直接採用估算的高度,而不是heightForHeaderInSection方法中設定的高度,也就是此時的sectionHeight

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section
複製程式碼

方法,或者estimatedSectionHeaderHeight設定的估算高度,如果沒有設定估算高度,則系統預設為UITableViewAutomaticDimension

所以必須同時實現heightForHeaderInSectionviewForHeaderInSection方法,可以返回[UIView new],但是不能不實現。或者只實現heightForHeaderInSection方法,並且設定estimatedSectionHeaderHeight為 0 來關閉估算機制。

注:如果在 iOS11 中,使用了 self-sizing cell,並且使用了上拉載入更多,並且使用了高度自適應的方式計算 cell 的高度,那麼上拉載入更多的時候會發現 tableView 會跳動一下或者滾動一段距離,什麼原因呢,這裡解釋一下可能是由於:假如一個列表有10個 cell,你設定的估算高度是 80,那麼整個列表的估算高度為10 * 80 = 800,但是實際高度不是 800,假如是 1000,那麼當滾動到最下方的時候,此時的contentOffSet = 1000,然後上拉再載入 10條資料,此時會呼叫- (void)reloadData;方法,此時,列表的高度仍然會重新使用估算高度計算,80 * 20 = 1600,而contentOffSet = 1000,這個位置已經不是剛才的第 10條資料了,而是第1000 / 80= 12.5條資料了,因此會造成載入更多的時候資料銜接不上的問題。你可能需要設定estimatedRowHeight = 0來關閉它的估算高度解決這個問題,但是如果你非要開啟它的估算高度來使 cell 中的約束自適應高度的話,可以通過這種方式計算高度:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static DisplayCell *cell;//‘static’將cell儲存在靜態儲存區,這裡建立的cell僅用來計算高度,因此,記憶體中只有一份就可以了,因為此方法會呼叫多次,每次都建立的話即會耗費時間也會耗費空間。
    if (!cell) {
        cell = [[[NSBundle mainBundle] loadNibNamed:kCellNibName owner:self options:nil] lastObject];
    }
    cell.displayLab.text = self.data[indexPath.row];//給cell賦值,賦值是為了通過內容計算高度
    CGFloat height = [cell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    return height;
}
複製程式碼

另外,如果 cell 中使用了多行 label 的話,注意設定它的換行寬度: self.displayLab.preferredMaxLayoutWidth = XXX;//XXX應該和你約束的label寬度相同

五、Tips

1、每個介面中的控制元件的位置

如果專案中用 frame 佈局的控制元件較多,很多控制元件的位置依賴於self.view的頂部和底部,由於狀態列和底部空間的調整就會造成一部分控制元件的位置發生變化,修改過程中應該注意和線上 APP 比對。建議能用約束的就別用 frame,依賴上下控制元件的位置比依賴螢幕的邊緣和寬高更好維護一些。autolayout 並不影響寫動畫!

2、重新佈局

專案中有些控制元件的位置會因為響應事件、動畫和資料請求等重新佈局,因此應該特別注意的地方就是重新佈局後控制元件的位置是否和線上專案一致。另外,還有一些初始化時隱藏的控制元件,由於某些條件發生後才展示,也要注意其佈局。

3、全屏顯示

全屏顯示和橫屏模式下的介面,注意橫屏之後下方的感應器在安全區之外。

4、LaunchuImage

畫素為:1125 * 2436 並在LaunchImage中的Contents.json檔案中增加 JSON:

{
    "extent" : "full-screen",
    "idiom" : "iphone",
    "subtype" : "2436h",
    "filename" : "圖片名字.png",
    "minimum-system-version" : "11.0",
    "orientation" : "portrait",
    "scale" : "3x"
}
複製程式碼

5、定位

在 iOS 11 中必須支援 When In Use 授權模式(NSLocationWhenInUseUsageDescription),在 iOS 11 中,為了避免開發者只提供請求 Always 授權模式這種情況,加入此限制,如果不提供When In Use 授權模式,那麼 Always 相關授權模式也無法正常使用。(就是為了打倒流氓軟體的流氓強制定位)

如果要支援老版本,即 iOS 11 以下系統版本,那麼建議在 info.plist 中配置所有的 Key(即使 NSLocationAlwaysUsageDescription 在 iOS 11及以上版本不再使用):

NSLocationWhenInUseUsageDescription
NSLocationAlwaysAndWhenInUseUsageDescription
NSLocationAlwaysUsageDescription
複製程式碼

NSLocationAlwaysAndWhenInUseUsageDescription為 iOS 11 中新引入的一個 Key。 參考:WWDC17: What's New in Location Technologies ? (這是一個帶簡體中文字幕的視訊,我並沒有看!!!)

相關文章