iOS十一、iPhone X、Xcode9 适配指南

2017.09.23ios

不断完善中。。。git

2017.10.02 新增 iPhone X 适配官方中文文档github

更新iOS11后,发现有些地方须要作适配,整理后按照优先级分为如下三类:api

  1. 单纯升级iOS11后形成的变化;
  2. Xcode9 打包后形成的变化;
  3. iPhoneX的适配

1、单纯升级iOS11后形成的变化

######1. 升级后,发现某个拥有tableView的界面错乱,组间距和contentInset错乱,由于iOS11中 UIViewControllerautomaticallyAdjustsScrollViewInsets 属性被废弃了,所以当tableView超出安全区域时,系统自动会调整SafeAreaInsets值,进而影响adjustedContentInset缓存

// 有些界面如下使用代理方法来设置,发现并无生效
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;

// 这样的原理是由于以前只是实现了高度的代理方法,却没有实现View的代理方法,iOS10及之前这么写是没问题的,iOS11开启了行高估算机制引发的bug,所以有如下几种解决方法:

// 解决方法一:添加实现View的代理方法,只有实现下面两个方法,方法 (CGFloat)tableView: heightForFooterInSection: 才会生效
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
    return nil;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    return nil;
}

// 解决方法二:直接使用tableView属性进行设置,修复该UI错乱
self.tableView.sectionHeaderHeight = 0;
self.tableView.sectionFooterHeight = 5;

[_optionTableView setContentInset:UIEdgeInsetsMake(-35, 0, 0, 0)];

// 解决方法三:添加如下代码关闭估算行高
self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
复制代码
2. 若是使用了Masonry 进行布局,就要适配safeArea
if ([UIDevice currentDevice].systemVersion.floatValue >= 11.0) {
    make.edges.equalTo(self.view.safeAreaInsets);
} else {
    make.edges.equalTo(self.view);
}
复制代码
3. 对于IM的发送原图功能,iOS11启动全新的HEIC 格式的图片,iPhone7以上设备+iOS11拍出的live照片是.heic格式图片,同一张live格式的图片,iOS10发送就没问题(转成了jpg),iOS11就不行
  • 微信的处理方式是一比一转化成 jpg 格式
  • QQ和钉钉的处理方式是直接压缩,即便是原图也压缩为非原图
  • 最终采起的是微信的方案,使用如下代码转成jpg格式
// 0.83能保证压缩先后图片大小是一致的
// 形成不一致的缘由是图片的bitmap一个是8位的,一个是16位的
imageData = UIImageJPEGRepresentation([UIImage imageWithData:imageData], 0.83);
复制代码
4. 有的页面在侧滑返回或者pop操做后,会出现页面下沉的现象,效果以下图所示

// 这是由于 UIScrollView 的 contentInsetAdjustmentBehavior 属性默认为 automatic,经过如下代码能够修复
if (@available(iOS 11.0, *)) {
   self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
// 固然,若是是使用 Storyboard,能够依次 Size Inspector -> Content Insets -> Set 'Never' 搞定
复制代码

进行修改以后,没有 SearchViewController 的页面是没有问题的,可是拥有searchViewController 的页面,进行搜索文本的输入会形成UI错乱,所以使用如下解决方法安全

- (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. 另外,项目中还使用了【FDTemplateLayoutCell】这个第三方用来缓存行高,孙源大神可能近期太忙,也少有更新,可是在 iOS11 上发现近期报了一个频繁的 crash

报错缘由 Collection <__NSArrayM: 0x1c04473e0> was mutated while being enumerated.
针对这个问题的解决方法,在 issue 中找到了答案:

前往文件 "UITableView+FDTemplateLayoutCell.h" 70if (isSystemVersionEqualOrGreaterThen10_2) {
     // 将这里的 UILayoutPriorityRequired 更改成 UILayoutPriorityDefaultHigh 便可解决问题
     widthFenceConstraint.priority = UILayoutPriorityDefaultHigh - 1;
}
复制代码
6. UITableView 的删除操做,因为iOS11 手感的优化,出现了如下问题:

cell和rowAction 按钮没有同步消失

  • 后来查明缘由是最开始写代码的时候没有注意细节,在定义删除按钮的时候没有设置合适的类型:以前是 UITableViewRowActionStyleNormal,改成UITableViewRowActionStyleDestructive便可
  • 缘由:因为没有设置 删除 所特有的type,所以在UI展现上默认是不删除的,所以适配的是保留cell的UI,只有设置删除属性后,才能和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];
}
复制代码

修改后搞定!多么美的一抹红呀!

2、使用Xcode9 编译后发现的问题

1. 发现fastSocket第三方报错,具体缘由是缺乏C99的头文件,引入#include <sys/time.h>便可

2. 导航栏的新特性
  • 原生的搜索栏样式发生改变

右边为iOS11样式,搜索区域高度变大,字体变大
查看 API 后发现,iOS11后将 searchController赋值给了 NavigationItem,经过属性 hidesSearchBarWhenScrolling 能够控制搜索栏是否在滑动的时候进行隐藏和显示

// 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 来实现下面的效果,并能够经过 largeTitleTextAttributes来设置大标题的文本样式。设置大标题以后,导航栏的高度就会由以前的64pt变成 96pt,若是项目中有直接写死的高度或者隐藏导航栏之类的操做,就须要适配一下 微信

  • 有个界面使用到了导航栏按钮相关的frame,也发生了UI错乱,查看UI层级关系后发现,iOS11之前是直接把按钮加到了UINavigationBar上面,而iOS11则是先将按钮加到了_UITAMICAdaptorView,再加到_UIButtonBarStackView_UINavigationBarContentView,接着才是UINavigationBar。所以若是须要获取导航栏按钮 frame 或者 superView,这里须要专门作下适配网络

iOS10及如下版本导航栏按钮层级关系图

iOS11导航栏按钮层级关系图

3、iPhone X的适配

下载完Xcode9以后,第一件事天然是在 iPhone X(模拟器)上过把瘾,而后编译后就发现报错了 因为iPhone X的状态栏是和其余版本手机差别比较大的,所以api 变化也比较大 前后作了如下适配app

适配点一:项目中使用状态栏中图标判断当前网络的具体状态

出错代码

打印的 Log 报出如下错误: Trapped uncaught exception 'NSUnknownKeyException', reason: '[<UIStatusBar_Modern 0x7fcdb0805770> valueForUndefinedKey:]: this class is not key value coding-compliant for the key foregroundView.'iphone

iPhone X

其余手机

使用 runtime 打印其全部属性,发现如下差别

// 测试代码
#import <objc/runtime.h>
NSMutableString *resultStr = [NSMutableString string];
//获取指定类的Ivar列表及Ivar个数
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的名称
    const char *memberAddress = ivar_getName(var);
    //获取Ivar的类型
    const char *memberType = ivar_getTypeEncoding(var);
    NSString *str = [NSString stringWithFormat:@"key = %s type = %s \n",memberAddress,memberType];
      [resultStr appendString:str];
}
NSLog(@"%@", resultStr);
复制代码
// 其余版本的手机
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 = {超长。。}
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"

// 所以可见iPhone X的状态栏是多嵌套了一层,多取一次便可,最终适配代码为:
NSArray *children;
// 不能用 [[self deviceVersion] isEqualToString:@"iPhone X"] 来判断,由于iPhone X 的模拟器不会返回 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];
    }
复制代码

警告以上处理,代码看起来是不报错了,然而!!具体看了下代码发现并不生效!由于从iPhone X取出来以后只有view层级的信息,所以采用如下方法肯定2G/3G/4G,从API上目测是有效的

NSArray *typeStrings2G = @[CTRadioAccessTechnologyEdge,
                               CTRadioAccessTechnologyGPRS,
                               CTRadioAccessTechnologyCDMA1x];
    
    NSArray *typeStrings3G = @[CTRadioAccessTechnologyHSDPA,
                               CTRadioAccessTechnologyWCDMA,
                               CTRadioAccessTechnologyHSUPA,
                               CTRadioAccessTechnologyCDMAEVDORev0,
                               CTRadioAccessTechnologyCDMAEVDORevA,
                               CTRadioAccessTechnologyCDMAEVDORevB,
                               CTRadioAccessTechnologyeHRPD];
    
    NSArray *typeStrings4G = @[CTRadioAccessTechnologyLTE];
    // 该 API 在 iOS7 以上系统才有效
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) {
        CTTelephonyNetworkInfo *teleInfo= [[CTTelephonyNetworkInfo alloc] init];
        NSString *accessString = teleInfo.currentRadioAccessTechnology;
        if ([typeStrings4G containsObject:accessString]) {
            NSLog(@"4G网络");
        } else if ([typeStrings3G containsObject:accessString]) {
            NSLog(@"3G网络");
        } else if ([typeStrings2G containsObject:accessString]) {
            NSLog(@"2G网络");
        } else {
            NSLog(@"未知网络");
        }
    } else {
        NSLog(@"未知网络");
    }
复制代码
适配点二:解决这个问题后项目跑起来发现,整个app界面上下各空出大概40pt的高度

常常从 Github 上下载项目把玩的老司机们都知道,有些老项目在模拟器上跑起来以后也会只有 iPhone 4(320480)的布局空间,形成这个的缘由是启动图使用 Launch Images Source 设置的时候没有勾选并设置对应的图片(11252436),解决方法以下

可是即便按照上面的操做进行以后,会发现底部 UITabBar 依旧是高出一些高度,查看层级关系后发现,一样是因为安全区的缘由,UITabBar 高度由49pt变成了83pt,所以这里也要对iPhone X 及其模拟器进行适配

适配点三:iPhone X 只有 faceID,没有touchID,若是in的应用有使用到 touchID 解锁的地方,这里要根据机型进行相应的适配
适配点四:以前有偷懒的直接使用20替代状态栏高度,这些坑都要经过从新获取状态栏高度,另外没有使用自动布局的也要默默还债了
CGRectGetHeight([UIApplication sharedApplication].statusBarFrame)
复制代码
适配点五:然而iPhone X更大的坑是屏幕的适配

首先看下屏幕尺寸

这张图反映出很多信息:

  • iPhone X的宽度虽然和7是同样的,可是高度多出145pt
  • 使用三倍图是重点,并且通常认为肉眼所能所能识别的最高的屏幕密度是300ppi,iPhone X已达到458ppi(查证发现三星galaxy系列的屏幕密度是522ppi)

在设计方面,苹果官方文档human-interface-guidelines有明确要求,下面结合图例进行说明:

展现出来的设计布局要求填满整个屏幕

填满的同时要注意控件不要被大圆角和传感器部分所遮挡

安全区域之外的部分不容许有任何与用户交互的控件
上面这张图内含信息略多

  • 头部导航栏不予许进行用户交互的,意味着下面这两种状况 Apple 官方是不容许的
  • 底部虚拟区是替代了传统home键,高度为34pt,经过上滑可呼起多任务管理,考虑到手势冲突,这部分也是不容许有任何可交互的控件,可是设计的背景图要覆盖到非安全区域
  • 状态栏在非安全区域,文档中也提到,除非能够经过隐藏状态栏给用户带来额外的价值,不然最好把状态栏还给用户
  • 不要让 界面中的元素 干扰底部的主屏幕指示器
  • 重复使用现有图片时,注意长宽比差别。iPhone X 与常规 iPhone 的屏幕长宽比不一样,所以,全屏的 4.7 寸屏图像在 iPhone X 上会出现裁切或适配宽度显示。因此,这部分的视图须要根据设备作出适配。

注意相似占位图的适配

适配点六:横屏适配

关于 safe area,使用 safeAreaLayoutGuidesafeAreaInset就能解决大部分问题,可是横屏下还可能会产生一些问题,须要额外适配

  • 产生这个缘由代码是:[headerView.contentView setBackgroundColor:[UIColor headerFooterColor]],这个写法看起来没错,可是只有在 iPhone X上有问题

经过设置该属性,内容视图嵌入到了safe area中,可是contentView没有

  • 解决方法:设置backgroundView颜色 [headerView.backgroundView setBackgroundColor:[UIColor headerFooterColor]]
适配点七:设备信息
if ([deviceString isEqualToString:@"iPhone10,1"])   return @"国行(A1863)、日行(A1906)iPhone 8";
if ([deviceString isEqualToString:@"iPhone10,4"])   return @"美版(Global/A1905)iPhone 8";
if ([deviceString isEqualToString:@"iPhone10,2"])   return @"国行(A1864)、日行(A1898)iPhone 8 Plus";
if ([deviceString isEqualToString:@"iPhone10,5"])   return @"美版(Global/A1897)iPhone 8 Plus";
if ([deviceString isEqualToString:@"iPhone10,3"])   return @"国行(A1865)、日行(A1902)iPhone X";
if ([deviceString isEqualToString:@"iPhone10,6"])   return @"美版(Global/A1901)iPhone X";
复制代码

更多新设备信息详见**Github-iOS-getClientInfo**

适配点八:若是是业务需求须要隐藏底部Indicator
// 在VC里面重写下面这个方法便可
- (BOOL)prefersHomeIndicatorAutoHidden{
    return YES;
}
复制代码
相关文章
相关标签/搜索