不斷完善中。。。
2017.10.02 新增 iPhone X 適配官方中文文檔
更新iOS11后,發(fā)現(xiàn)有些地方需要做適配,整理后按照優(yōu)先級(jí)分為以下三類(lèi):
- 單純升級(jí)iOS11后造成的變化;
- Xcode9 打包后造成的變化;
- iPhoneX的適配
一、單純升級(jí)iOS11后造成的變化
1. 升級(jí)后,發(fā)現(xiàn)某個(gè)擁有tableView的界面錯(cuò)亂,組間距和contentInset錯(cuò)亂,因?yàn)閕OS11中 UIViewController 的 automaticallyAdjustsScrollViewInsets 屬性被廢棄了,因此當(dāng)tableView超出安全區(qū)域時(shí),系統(tǒng)自動(dòng)會(huì)調(diào)整SafeAreaInsets值,進(jìn)而影響adjustedContentInset值
// 有些界面以下使用代理方法來(lái)設(shè)置,發(fā)現(xiàn)并沒(méi)有生效
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;
// 這樣的原理是因?yàn)橹爸皇菍?shí)現(xiàn)了高度的代理方法,卻沒(méi)有實(shí)現(xiàn)View的代理方法,iOS10及以前這么寫(xiě)是沒(méi)問(wèn)題的,iOS11開(kāi)啟了行高估算機(jī)制引起的bug,因此有以下幾種解決方法:
// 解決方法一:添加實(shí)現(xiàn)View的代理方法,只有實(shí)現(xiàn)下面兩個(gè)方法,方法 (CGFloat)tableView: heightForFooterInSection: 才會(huì)生效
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
return nil;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
return nil;
}
// 解決方法二:直接使用tableView屬性進(jìn)行設(shè)置,修復(fù)該UI錯(cuò)亂
self.tableView.sectionHeaderHeight = 0;
self.tableView.sectionFooterHeight = 5;
[_optionTableView setContentInset:UIEdgeInsetsMake(-35, 0, 0, 0)];
// 解決方法三:添加以下代碼關(guān)閉估算行高
self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
2. 如果使用了Masonry 進(jìn)行布局,就要適配safeArea
if ([UIDevice currentDevice].systemVersion.floatValue >= 11.0) {
make.edges.equalTo(self.view.safeAreaInsets);
} else {
make.edges.equalTo(self.view);
}
3. 對(duì)于IM的發(fā)送原圖功能,iOS11啟動(dòng)全新的HEIC 格式的圖片,iPhone7以上設(shè)備+iOS11拍出的live照片是.heic格式圖片,同一張live格式的圖片,iOS10發(fā)送就沒(méi)問(wèn)題(轉(zhuǎn)成了jpg),iOS11就不行
- 微信的處理方式是一比一轉(zhuǎn)化成 jpg 格式
- QQ和釘釘?shù)奶幚矸绞绞侵苯訅嚎s,即使是原圖也壓縮為非原圖
- 最終采取的是微信的方案,使用以下代碼轉(zhuǎn)成jpg格式
// 0.83能保證壓縮前后圖片大小是一致的
// 造成不一致的原因是圖片的bitmap一個(gè)是8位的,一個(gè)是16位的
imageData = UIImageJPEGRepresentation([UIImage imageWithData:imageData], 0.83);
4. 有的頁(yè)面在側(cè)滑返回或者pop操作后,會(huì)出現(xiàn)頁(yè)面下沉的現(xiàn)象,效果如下圖所示

// 這是因?yàn)?UIScrollView 的 contentInsetAdjustmentBehavior 屬性默認(rèn)為 automatic,通過(guò)以下代碼可以修復(fù)
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
// 當(dāng)然,如果是使用 Storyboard,可以依次 Size Inspector -> Content Insets -> Set 'Never' 搞定
進(jìn)行修改之后,沒(méi)有 SearchViewController 的頁(yè)面是沒(méi)有問(wèn)題的,但是擁有searchViewController 的頁(yè)面,進(jìn)行搜索文本的輸入會(huì)造成UI錯(cuò)亂,因此使用以下解決方法
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
}
}
5. 另外,項(xiàng)目中還使用了【FDTemplateLayoutCell】這個(gè)第三方用來(lái)緩存行高,孫源大神可能近期太忙,也少有更新,但是在 iOS11 上發(fā)現(xiàn)近期報(bào)了一個(gè)頻繁的 crash

針對(duì)這個(gè)問(wèn)題的解決方法,在 issue 中找到了答案:
前往文件 "UITableView+FDTemplateLayoutCell.h" 70行
if (isSystemVersionEqualOrGreaterThen10_2) {
// 將這里的 UILayoutPriorityRequired 更改為 UILayoutPriorityDefaultHigh 即可解決問(wèn)題
widthFenceConstraint.priority = UILayoutPriorityDefaultHigh - 1;
}
6. UITableView 的刪除操作,由于iOS11 手感的優(yōu)化,出現(xiàn)了以下問(wèn)題:

- 后來(lái)查明原因是最開(kāi)始寫(xiě)代碼的時(shí)候沒(méi)有注意細(xì)節(jié),在定義刪除按鈕的時(shí)候沒(méi)有設(shè)置合適的類(lèi)型:之前是
UITableViewRowActionStyleNormal,改為UITableViewRowActionStyleDestructive即可 - 原因:由于沒(méi)有設(shè)置 刪除 所特有的type,因此在UI展示上默認(rèn)是不刪除的,因此適配的是保留cell的UI,只有設(shè)置刪除屬性后,才能和
deleteRowsAtIndexPaths方法保持UI上的同步
UITableViewRowAction *deleteRowAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDestructive title:@"刪除" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) {
[self.dataSource removeObjectAtIndex:indexPath.row];
// 刷新tableview
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}

二、使用Xcode9 編譯后發(fā)現(xiàn)的問(wèn)題
1. 發(fā)現(xiàn)fastSocket第三方報(bào)錯(cuò),具體原因是缺少C99的頭文件,引入#include <sys/time.h>即可

2. 導(dǎo)航欄的新特性
- 原生的搜索欄樣式發(fā)生改變

查看 API 后發(fā)現(xiàn),iOS11后將
searchController賦值給了 NavigationItem,通過(guò)屬性 hidesSearchBarWhenScrolling 可以控制搜索欄是否在滑動(dòng)的時(shí)候進(jìn)行隱藏和顯示
// A view controller that will be shown inside of a navigation controller can assign a UISearchController to this property to display the search controller’s search bar in its containing navigation controller’s navigation bar.
@property (nonatomic, retain, nullable) UISearchController *searchController API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos);
// If this property is true (the default), the searchController’s search bar will hide as the user scrolls in the top view controller’s scroll view. If false, the search bar will remain visible and pinned underneath the navigation bar.
@property (nonatomic) BOOL hidesSearchBarWhenScrolling API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos);
-
另外,
UINavigationBar新增屬性 BOOL值prefersLargeTitles來(lái)實(shí)現(xiàn)下面的效果,并可以通過(guò)largeTitleTextAttributes來(lái)設(shè)置大標(biāo)題的文本樣式。設(shè)置大標(biāo)題之后,導(dǎo)航欄的高度就會(huì)由之前的64pt變成 96pt,如果項(xiàng)目中有直接寫(xiě)死的高度或者隱藏導(dǎo)航欄之類(lèi)的操作,就需要適配一下
有個(gè)界面使用到了導(dǎo)航欄按鈕相關(guān)的frame,也發(fā)生了UI錯(cuò)亂,查看UI層級(jí)關(guān)系后發(fā)現(xiàn),iOS11以前是直接把按鈕加到了
UINavigationBar上面,而iOS11則是先將按鈕加到了_UITAMICAdaptorView,再加到_UIButtonBarStackView、_UINavigationBarContentView,接著才是UINavigationBar。因此如果需要獲取導(dǎo)航欄按鈕frame或者superView,這里需要專(zhuān)門(mén)做下適配


三、iPhone X的適配
下載完Xcode9之后,第一件事自然是在 iPhone X(模擬器)上過(guò)把癮,然后編譯后就發(fā)現(xiàn)報(bào)錯(cuò)了
由于iPhone X的狀態(tài)欄是和其他版本手機(jī)差異比較大的,因此api 變化也比較大
先后做了以下適配
適配點(diǎn)一:項(xiàng)目中使用狀態(tài)欄中圖標(biāo)判斷當(dāng)前網(wǎng)絡(luò)的具體狀態(tài)

打印的 Log 報(bào)出以下錯(cuò)誤: Trapped uncaught exception 'NSUnknownKeyException', reason: '[<UIStatusBar_Modern 0x7fcdb0805770> valueForUndefinedKey:]: this class is not key value coding-compliant for the key foregroundView.'


使用 runtime 打印其所有屬性,發(fā)現(xiàn)以下差異
// 測(cè)試代碼
#import <objc/runtime.h>
NSMutableString *resultStr = [NSMutableString string];
//獲取指定類(lèi)的Ivar列表及Ivar個(gè)數(shù)
unsigned int count = 0;
Ivar *member = class_copyIvarList([[application valueForKeyPath:@"_statusBar"] class], &count);
for(int i = 0; i < count; i++){
Ivar var = member[i];
//獲取Ivar的名稱(chēng)
const char *memberAddress = ivar_getName(var);
//獲取Ivar的類(lèi)型
const char *memberType = ivar_getTypeEncoding(var);
NSString *str = [NSString stringWithFormat:@"key = %s type = %s \n",memberAddress,memberType];
[resultStr appendString:str];
}
NSLog(@"%@", resultStr);
// 其他版本的手機(jī)
key = _inProcessProvider type = @"<UIStatusBarStateProvider>"
key = _showsForeground type = B
key = _backgroundView type = @"UIStatusBarBackgroundView"
key = _doubleHeightLabel type = @"UILabel"
key = _doubleHeightLabelContainer type = @"UIView"
key = _currentDoubleHeightText type = @"NSString"
key = _currentRawData type = {超長(zhǎng)。。}
key = _interruptedAnimationCompositeViews type = @"NSMutableArray"
key = _newStyleBackgroundView type = @"UIStatusBarBackgroundView"
key = _newStyleForegroundView type = @"UIStatusBarForegroundView"
key = _slidingStatusBar type = @"UIStatusBar"
key = _styleAttributes type = @"UIStatusBarStyleAttributes"
key = _waitingOnCallbackAfterChangingStyleOverridesLocally type = B
key = _suppressGlow type = B
key = _translucentBackgroundAlpha type = d
key = _showOnlyCenterItems type = B
key = _foregroundViewShouldIgnoreStatusBarDataDuringAnimation type = B
key = _tintColor type = @"UIColor"
key = _lastUsedBackgroundColor type = @"UIColor"
key = _nextTintTransition type = @"UIStatusBarStyleAnimationParameters"
key = _overrideHeight type = @"NSNumber"
key = _disableRasterizationReasons type = @"NSMutableSet"
key = _timeHidden type = B
key = _statusBarWindow type = @"UIStatusBarWindow"
// iPhone X
key = _statusBar ; type = @"_UIStatusBar"
// 因此可見(jiàn)iPhone X的狀態(tài)欄是多嵌套了一層,多取一次即可,最終適配代碼為:
NSArray *children;
// 不能用 [[self deviceVersion] isEqualToString:@"iPhone X"] 來(lái)判斷,因?yàn)閕Phone X 的模擬器不會(huì)返回 iPhone X
if ([[application valueForKeyPath:@"_statusBar"] isKindOfClass:NSClassFromString(@"UIStatusBar_Modern")]) {
children = [[[[application valueForKeyPath:@"_statusBar"] valueForKeyPath:@"_statusBar"] valueForKeyPath:@"foregroundView"] subviews];
} else {
children = [[[application valueForKeyPath:@"_statusBar"] valueForKeyPath:@"foregroundView"] subviews];
}
警告以上處理,代碼看起來(lái)是不報(bào)錯(cuò)了,然而??!具體看了下代碼發(fā)現(xiàn)并不生效!因?yàn)閺膇Phone X取出來(lái)之后只有view層級(jí)的信息,因此采用以下方法確定2G/3G/4G,從API上目測(cè)是有效的
NSArray *typeStrings2G = @[CTRadioAccessTechnologyEdge,
CTRadioAccessTechnologyGPRS,
CTRadioAccessTechnologyCDMA1x];
NSArray *typeStrings3G = @[CTRadioAccessTechnologyHSDPA,
CTRadioAccessTechnologyWCDMA,
CTRadioAccessTechnologyHSUPA,
CTRadioAccessTechnologyCDMAEVDORev0,
CTRadioAccessTechnologyCDMAEVDORevA,
CTRadioAccessTechnologyCDMAEVDORevB,
CTRadioAccessTechnologyeHRPD];
NSArray *typeStrings4G = @[CTRadioAccessTechnologyLTE];
// 該 API 在 iOS7 以上系統(tǒng)才有效
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) {
CTTelephonyNetworkInfo *teleInfo= [[CTTelephonyNetworkInfo alloc] init];
NSString *accessString = teleInfo.currentRadioAccessTechnology;
if ([typeStrings4G containsObject:accessString]) {
NSLog(@"4G網(wǎng)絡(luò)");
} else if ([typeStrings3G containsObject:accessString]) {
NSLog(@"3G網(wǎng)絡(luò)");
} else if ([typeStrings2G containsObject:accessString]) {
NSLog(@"2G網(wǎng)絡(luò)");
} else {
NSLog(@"未知網(wǎng)絡(luò)");
}
} else {
NSLog(@"未知網(wǎng)絡(luò)");
}
適配點(diǎn)二:解決這個(gè)問(wèn)題后項(xiàng)目跑起來(lái)發(fā)現(xiàn),整個(gè)app界面上下各空出大概40pt的高度

經(jīng)常從 Github 上下載項(xiàng)目把玩的老司機(jī)們都知道,有些老項(xiàng)目在模擬器上跑起來(lái)之后也會(huì)只有 iPhone 4(320480)的布局空間,造成這個(gè)的原因是啟動(dòng)圖使用 Launch Images Source 設(shè)置的時(shí)候沒(méi)有勾選并設(shè)置對(duì)應(yīng)的圖片(11252436),解決方法如下

但是即使按照上面的操作進(jìn)行之后,會(huì)發(fā)現(xiàn)底部 UITabBar 依舊是高出一些高度,查看層級(jí)關(guān)系后發(fā)現(xiàn),同樣是由于安全區(qū)的原因,UITabBar 高度由49pt變成了83pt,因此這里也要對(duì)iPhone X 及其模擬器進(jìn)行適配
適配點(diǎn)三:iPhone X 只有 faceID,沒(méi)有touchID,如果in的應(yīng)用有使用到 touchID 解鎖的地方,這里要根據(jù)機(jī)型進(jìn)行相應(yīng)的適配
適配點(diǎn)四:之前有偷懶的直接使用20替代狀態(tài)欄高度,這些坑都要通過(guò)重新獲取狀態(tài)欄高度,另外沒(méi)有使用自動(dòng)布局的也要默默還債了
CGRectGetHeight([UIApplication sharedApplication].statusBarFrame)
適配點(diǎn)五:然而iPhone X更大的坑是屏幕的適配
首先看下屏幕尺寸

這張圖反映出不少信息:
- iPhone X的寬度雖然和7是一樣的,但是高度多出145pt
- 使用三倍圖是重點(diǎn),而且一般認(rèn)為肉眼所能所能識(shí)別的最高的屏幕密度是300ppi,iPhone X已達(dá)到458ppi(查證發(fā)現(xiàn)三星galaxy系列的屏幕密度是522ppi)
在設(shè)計(jì)方面,蘋(píng)果官方文檔human-interface-guidelines有明確要求,下面結(jié)合圖例進(jìn)行說(shuō)明:



上面這張圖內(nèi)含信息略多
-
頭部導(dǎo)航欄不予許進(jìn)行用戶(hù)交互的,意味著下面這兩種情況 Apple 官方是不允許的
- 底部虛擬區(qū)是替代了傳統(tǒng)home鍵,高度為34pt,通過(guò)上滑可呼起多任務(wù)管理,考慮到手勢(shì)沖突,這部分也是不允許有任何可交互的控件,但是設(shè)計(jì)的背景圖要覆蓋到非安全區(qū)域
- 狀態(tài)欄在非安全區(qū)域,文檔中也提到,除非可以通過(guò)隱藏狀態(tài)欄給用戶(hù)帶來(lái)額外的價(jià)值,否則最好把狀態(tài)欄還給用戶(hù)
- 不要讓 界面中的元素 干擾底部的主屏幕指示器
- 重復(fù)使用現(xiàn)有圖片時(shí),注意長(zhǎng)寬比差異。iPhone X 與常規(guī) iPhone 的屏幕長(zhǎng)寬比不同,因此,全屏的 4.7 寸屏圖像在 iPhone X 上會(huì)出現(xiàn)裁切或適配寬度顯示。所以,這部分的視圖需要根據(jù)設(shè)備做出適配。

適配點(diǎn)六:橫屏適配
關(guān)于 safe area,使用 safeAreaLayoutGuide 和 safeAreaInset就能解決大部分問(wèn)題,但是橫屏下還可能會(huì)產(chǎn)生一些問(wèn)題,需要額外適配

- 產(chǎn)生這個(gè)原因代碼是:
[headerView.contentView setBackgroundColor:[UIColor headerFooterColor]],這個(gè)寫(xiě)法看起來(lái)沒(méi)錯(cuò),但是只有在 iPhone X上有問(wèn)題

- 解決方法:設(shè)置backgroundView顏色
[headerView.backgroundView setBackgroundColor:[UIColor headerFooterColor]]
適配點(diǎn)七:設(shè)備信息
if ([deviceString isEqualToString:@"iPhone10,1"]) return @"國(guó)行(A1863)、日行(A1906)iPhone 8";
if ([deviceString isEqualToString:@"iPhone10,4"]) return @"美版(Global/A1905)iPhone 8";
if ([deviceString isEqualToString:@"iPhone10,2"]) return @"國(guó)行(A1864)、日行(A1898)iPhone 8 Plus";
if ([deviceString isEqualToString:@"iPhone10,5"]) return @"美版(Global/A1897)iPhone 8 Plus";
if ([deviceString isEqualToString:@"iPhone10,3"]) return @"國(guó)行(A1865)、日行(A1902)iPhone X";
if ([deviceString isEqualToString:@"iPhone10,6"]) return @"美版(Global/A1901)iPhone X";
更多新設(shè)備信息詳見(jiàn)Github-iOS-getClientInfo
適配點(diǎn)八:如果是業(yè)務(wù)需求需要隱藏底部Indicator
// 在VC里面重寫(xiě)下面這個(gè)方法即可
- (BOOL)prefersHomeIndicatorAutoHidden{
return YES;
}
史上第二走心的 iOS11-Drag & Drop 教程
2017.10.02補(bǔ)充:iPhone X 中文官方適配文檔
iOS11/iPhone X 適配簡(jiǎn)單,但你的Apple思維適配做好了么?


