iOS11、iPhoneX、Xcode9 的注意点汇总

这里写图片描述

参考文章:
WWDC 2017 session204: Updating Your App for iOS 11
Apple 官方文档: Human Interface Guidelines
iPhone X 中文官方适配文档
你可能需要为你的 APP 适配 iOS11
iOS11 导航栏按钮位置问题的解决
iOS11 遇到的坑及解决方法
适配 iOS11&iPhoneX 的一些坑
iOS11 & iPhoneX 适配 & Xcode9 打包注意事项
App 界面适配 iOS11(包括 iPhoneX 的奇葩尺寸)
简书 App 适配 iOS 11
Xcode9 打包报错问题解决
iOS11 访问相册权限变更问题
The Ultimate Guide To iPhone Resolutions
iPhoneX状态条的隐藏与显示

首先,请注意工程中依赖的第三方代码(framework, library或是源码),需要留意其适配 iOS11、iPhoneX 的更新。
自己的代码,可以参照下面整理的适配。

一. 先来热个身,^_^

1. 每次苹果发布新的系统,我们都要注意下新系统相关宏的支持、废弃 API 的替换:
#if (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR)
    const BOOL IOS11_OR_LATER = ( [[[UIDevice currentDevice] systemVersion] compare:@"11.0" options:NSNumericSearch] != NSOrderedAscending );
    const BOOL IOS10_OR_EARLIER = !IOS11_OR_LATER;
#else 
    const BOOL IOS11_OR_LATER = NO;
    const BOOL IOS10_OR_EARLIER = NO;
#endif
2. 每次苹果发布新的设备,我们也要注意下新设备相关宏的支持:

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

@"iPhone10,1" : @"iPhone 8 国行/日版"
@"iPhone10,4" : @"iPhone 8 美版(Global)"
@"iPhone10,2" : @"iPhone 8 Plus 美版(Global)"
@"iPhone10,5" : @"iPhone 8 Plus 美版(Global)"
@"iPhone10,3" : @"iPhone X 国行/日版"
@"iPhone10,6" : @"iPhone X 美版(Global)"
#if (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR)
    const BOOL IS_SCREEN_55_INCH = ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1242, 2208), [[UIScreen mainScreen] currentMode].size) : NO);
    const BOOL IS_SCREEN_58_INCH = ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO);
#else
    const BOOL IS_SCREEN_55_INCH = NO;
    const BOOL IS_SCREEN_58_INCH = NO;
#endif

下图是 iPhone 常见型号的屏幕相关对比:
更详细的信息可查阅:The Ultimate Guide To iPhone Resolutions
图片

iPhone 8 与 iPhone X 的尺寸对比:
图片

二. 状态栏

iPhone X状态条由20px变成了44px,UITabBar由49px变成了83px。设置布局时y直接写成64的就要根据机型设置。可以设置宏

#define Device_Is_iPhoneX ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO)

然后再设置。

三. 导航栏

1. 导航栏高度的变化

iOS11之前导航栏默认高度为64pt(这里高度指statusBar + NavigationBar),iOS11之后如果设置了prefersLargeTitles = YES则为96pt,默认情况下还是64pt,但在iPhoneX上由于刘海的出现statusBar由以前的20pt变成了44pt,所以iPhoneX上高度变为88pt,如果项目里隐藏了导航栏加了自定义按钮之类的,这里需要注意适配一下。

2. 导航栏图层及对titleView布局的影响

iOS11之前导航栏的title是添加在UINavigationItemView上面,而navigationBarButton则直接添加在UINavigationBar上面,如果设置了titleView,则titleView也是直接添加在UINavigationBar上面。iOS11之后,大概因为largeTitle的原因,视图层级发生了变化,如果没有给titleView赋值,则titleView会直接添加在_UINavigationBarContentView上面,如果赋值了titleView,则会把titleView添加在_UITAMICAdaptorView上,而navigationBarButton被加在了_UIButtonBarStackView上,然后他们都被加在了_UINavigationBarContentView上,如图:
图片
所以如果你的项目是自定义的navigationBar,那么在iOS11上运行就可能出现布局错乱的bug,解决办法是重写UINavigationBar的layoutSubviews方法,调整布局,上代码:

- (void)layoutSubviews {
    [super layoutSubviews];

    // 注意导航栏及状态栏高度适配
    self.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), naviBarHeight);
    for (UIView *view in self.subviews) {
        if([NSStringFromClass([view class]) containsString:@"Background"]) {
            view.frame = self.bounds;
        }
        else if ([NSStringFromClass([view class]) containsString:@"ContentView"]) {
            CGRect frame = view.frame;
            frame.origin.y = statusBarHeight;
            frame.size.height = self.bounds.size.height - frame.origin.y;
            view.frame = frame;
        }
    }
}

再补充一点,看了简书App适配iOS11发现titleView支持autolayout,这要求titleView必须是能够自撑开的或实现了- intrinsicContentSize方法

- (CGSize)intrinsicContentSize {
    return UILayoutFittingExpandedSize;
}
3. 控制大标题的显示

在 UI navigation bar 中新增了一个 BOOL 属性prefersLargeTitles,将该属性设置为ture,navigation bar就会在整个APP中显示大标题,如果想要在控制不同页面大标题的显示,可以通过设置当前页面的navigationItemlargeTitleDisplayMode属性;

navigationItem.largeTitleDisplayMode 

typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode) {  
/// 自动模式依赖上一个 item 的特性
UINavigationItemLargeTitleDisplayModeAutomatic,
/// 针对当前 item 总是启用大标题特性
UINavigationItemLargeTitleDisplayModeAlways,
/// Never 
UINavigationItemLargeTitleDisplayModeNever,
}
4. Navigation 集成 UISearchController

把你的UISearchController赋值给navigationItem,就可以实现将UISearchController集成到Navigation

navigationItem.searchController  //iOS 11 新增属性
navigationItem.hidesSearchBarWhenScrolling //决定滑动的时候是否隐藏搜索框;iOS 11 新增属性

使用Xcode9 编译后发现原生的搜索栏样式发生改变,如下图,右边为 iOS 11 样式,搜索区域高度变大,字体变大。

图片

5. UINavigationController 和滚动交互

滚动的时候,以下交互操作都是由UINavigationController负责调动的:

UIsearchController搜索框效果更新
大标题效果的控制
Rubber banding效果 //当你开始往下拉,大标题会变大来回应那个滚轮

所以,如果你使用navigation bar,组装一些整个push和pop体验,你不会得到searchController的集成、大标题的控制更新和Rubber banding效果,因为这些都是由UINavigationController控制的。

6. UIToolbar and UINavigationBar — Layout

在 iOS 11 中,当苹果进行所有这些新特性时,也进行了其他的优化,针对 UIToolbar 和 UINavigaBar 做了新的自动布局扩展支持,自定义的 bar button items、自定义的 title 都可以通过 layout 来表示尺寸。
需要注意的是,你的constraints需要在 view 内部设置,所以如果你有一个自定义的标题视图,你需要确保任何约束只依赖于标题视图及其任何子视图。当你使用自动布局,系统假设你知道你在做什么。

7. Avoiding Zero-Sized Custom Views

自定义视图的size为0是因为你有一些模糊的约束布局。要避免视图尺寸为 0,可以从以下方面做:

  • UINavigationBar 和 UIToolbar 提供位置

  • 开发者则必须提供视图的 size,有三种方式:

    • 对宽度和高度的约束;

    • 实现 intrinsicContentSize;

    • 通过约束关联你的子视图;

8. 导航栏按钮的位置问题

在 iOS7 之后,我们在设置 UINavigationItem 的 leftBarButtonItem,rightBarButtonItem 的时候都会造成位置的偏移,虽然不大,但是跟 UI 的设计或者国人的习惯有点区别,当然也有很好的解决方案,多添加一个消极的宽度为负值的 UIBarButtonItem

+(UIBarButtonItem *)fixedSpaceWithWidth:(CGFloat)width {

    UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
    fixedSpace.width = width;
    return fixedSpace;
}

在我们添加导航栏按钮的时候
我们使用就可以满足将按钮位置调整的需求

[self.navigationItem setRightBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], rightBarButtonItem]];

但是这些在iOS 11中都无效了!!!!
但是这些在iOS 11中都无效了!!!!
但是这些在iOS 11中都无效了!!!!
重要的事情说3遍。

iOS 11改动相当大的就是导航栏的部分,在原来的已经复杂的不要的图层中又新增了新的图层!
图片
是的你没有看做,_UINavigationBarContentView和_UIButtonBarStackView和_UITAMICAdaptorView
而我们之前的leftBarButtonItem什么的现在都在UIButtonBarStackView中了.更无语的是这些

<_UIButtonBarStackView: 0x7ff988074290; frame = (12 0; 48 44); layer = <CALayer: 0x60000042bc80>>
Printing description of $11:
<UIView: 0x7ff9880764a0; frame = (0 22; 8 0); layer = <CALayer: 0x60000042b7c0>>
Printing description of $12:
<_UITAMICAdaptorView: 0x7ff988076790; frame = (8 2; 40 40); autoresizesSubviews = NO; layer = <CALayer: 0x60000042b8a0>>

我们可以看到一个_UIButtonBarStackView占掉了12个像素的左边约束,_UITAMICAdaptorView又占据了8个像素的左边约束,所以说我们很无语的就被占据了20px,更可气的是,都是私有对象,不容易修改!

于是还是老套路,我们设置负值来调整约束,结果却失败了,无效…
迫于无奈,我们只能想新的办法。

  • 放弃UIBarButtonItem,放弃UINavigationBar,使用自定义视图代替
  • 在UINavigationBar中使用添加视图的方式,固定位置固定大小添加按钮
  • UIBarButtonItem.customView 设置偏移(比如按钮设置图片偏移 视图设置tranform等)
  • 修改UIBarButtonItem图层结构(删除图层,或者修改约束)

当然,完全的使用自定义视图代替原生的UINavigationBar和UIBarButtonItem,这里我也不需要说明了.就是自定义视图蛮,肯定都能解决

使用addSubview:添加,之后remove什么的虽然可以,但是这个也不是我想要的

至于这是偏移,结果也嗯惨淡,无效.我尝试了设置旋转都可以,但是设置位置左移就失效了.很无语

为什么非要大动代码呢?在iOS 11之前,我们的项目绝大部分都是使用UINavigationBar和UIBarButtonItem,也就是系统的来管理,现在如果因为一个偏移问题,我们就要修改过多代码,岂不是很麻烦?
能否有较小代价实现?
答案是有的。

我们可能会做这样的一个分类

@implementation UINavigationItem (SXFixSpace)

+(void)load {
    [self swizzleInstanceMethodWithOriginSel:@selector(setLeftBarButtonItem:)
                                 swizzledSel:@selector(sx_setLeftBarButtonItem:)];
    [self swizzleInstanceMethodWithOriginSel:@selector(setRightBarButtonItem:)
                                 swizzledSel:@selector(sx_setRightBarButtonItem:)];
}

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem{
    if (leftBarButtonItem.customView) {
        [self sx_setLeftBarButtonItem:nil];
        [self setLeftBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], leftBarButtonItem]];
    }else {
        [self setLeftBarButtonItems:nil];
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

-(void)sx_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem{
    if (rightBarButtonItem.customView) {
        [self sx_setRightBarButtonItem:nil];
        [self setRightBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], rightBarButtonItem]];
    }else {
        [self setRightBarButtonItems:nil];
        [self sx_setRightBarButtonItem:rightBarButtonItem];
    }
}
@end

在我们iOS11之前,我们使用这样的一个分类来扩展,
使得我们在vc中就能这样使用

self.navigationItem.leftBarButtonItem = [UIBarButtonItem itemWithTarget:self action:@selector(sx_pressBack:) image:[UIImage imageNamed:@"nav_back"]];

就能调整好我们的按钮位置

那么能不能不懂这些代码也满足iOS 11呢?

那么只有在加一点点东西了,在分类中

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem{
    if (leftBarButtonItem.customView) {
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 11) {
            //如果调整,在这里实现,这样就能达到不影响代码的效果
        }else {
            [self sx_setLeftBarButtonItem:nil];
            [self setLeftBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], leftBarButtonItem]];
        }
    }else {
        [self setLeftBarButtonItems:nil];
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

在什么地方写我们都能想明白,接下来是怎么写的问题了
我的思路是既然他是一个customView,那么我能否扩展这个customView呢?
我们原来将一个按钮直接用作customView,比如这样

[[UIBarButtonItem alloc] initWithCustomView:button];

但是现在我想的是按钮添加在一个我们定义的view中,view作为customView
这样view作为一个位置调整的视图,就可以相对自由的定义了

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem{
    if (leftBarButtonItem.customView) {
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 11) {
            UIView *customView = leftBarButtonItem.customView;
            BarView *barView = [[BarView alloc]initWithFrame:customView.bounds];
            [barView addSubview:customView];
            customView.center = barView.center;
            [barView setPosition:SXBarViewPositionLeft];//说明这个view需要调整的是左边
            [self setLeftBarButtonItems:nil];
            [self sx_setLeftBarButtonItem:[[UIBarButtonItem alloc]initWithCustomView:barView]];
        }else {
            [self sx_setLeftBarButtonItem:nil];
            [self setLeftBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], leftBarButtonItem]];
        }
    }else {
        [self setLeftBarButtonItems:nil];
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

那么这个view我们也能干些事情了

typedef NS_ENUM(NSInteger, SXBarViewPosition) {
    SXBarViewPositionLeft,
    SXBarViewPositionRight
};

@interface BarView : UIView
@property (nonatomic, assign) SXBarViewPosition position;
@property (nonatomic, assign) BOOL applied;
@end

@implementation BarView

- (void)layoutSubviews {
    [super layoutSubviews];
    if (self.applied || [[[UIDevice currentDevice] systemVersion] floatValue]  < 11) return;
    UIView *view = self;
    while (![view isKindOfClass:UINavigationBar.class] && view.superview) {
        view = [view superview];
        if ([view isKindOfClass:UIStackView.class] && view.superview) {
            if (self.position == SXBarViewPositionLeft) {
                for (NSLayoutConstraint *constraint in view.superview.constraints) {
                    if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] &&
                     constraint.firstAttribute == NSLayoutAttributeTrailing) {
                        [view.superview removeConstraint:constraint];
                    }
                }
                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view
                                                                           attribute:NSLayoutAttributeLeading
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:view.superview
                                                                           attribute:NSLayoutAttributeLeading
                                                                          multiplier:1.0
                                                                            constant:0]];
                self.applied = YES;
            } else if (self.position == SXBarViewPositionRight) {
                for (NSLayoutConstraint *constraint in view.superview.constraints) {
                    if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] &&
                     constraint.firstAttribute == NSLayoutAttributeLeading) {
                        [view.superview removeConstraint:constraint];
                    }
                }
                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view
                                                                           attribute:NSLayoutAttributeTrailing
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:view.superview
                                                                           attribute:NSLayoutAttributeTrailing
                                                                          multiplier:1.0
                                                                            constant:0]];
                self.applied = YES;
            }
            break;
        }
    }
}

@end

代码其实不复杂,就是遍历view的父视图,当其实UIStackView的时候,我们修改其左右约束,但是仅仅修改的话会造成约束冲突,所以我们还需要提前移除约束冲突的左右约束(如果担心影响问题,不移除没有关系,仅仅是编译器会报约束冲突log,代码洁癖的话会感觉不舒服)

于是在原来的分类中稍作扩展,我们的新的分类就完成了

#import "UINavigationItem+SXFixSpace.h"
#import "NSObject+SXRuntime.h"
#import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, SXBarViewPosition) {
    SXBarViewPositionLeft,
    SXBarViewPositionRight
};

@interface BarView : UIView
@property (nonatomic, assign) SXBarViewPosition position;
@property (nonatomic, assign) BOOL applied;
@end

@implementation BarView

- (void)layoutSubviews {
    [super layoutSubviews];
    if (self.applied || [[[UIDevice currentDevice] systemVersion] floatValue]  < 11) return;
    UIView *view = self;
    while (![view isKindOfClass:UINavigationBar.class] && view.superview) {
        view = [view superview];
        if ([view isKindOfClass:UIStackView.class] && view.superview) {
            if (self.position == SXBarViewPositionLeft) {
                for (NSLayoutConstraint *constraint in view.superview.constraints) {
                    if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] && 
                    constraint.firstAttribute == NSLayoutAttributeTrailing) {
                        [view.superview removeConstraint:constraint];
                    }
                }
                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view
                                                                           attribute:NSLayoutAttributeLeading
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:view.superview
                                                                           attribute:NSLayoutAttributeLeading
                                                                          multiplier:1.0
                                                                            constant:0]];
                self.applied = YES;
            } else if (self.position == SXBarViewPositionRight) {
                for (NSLayoutConstraint *constraint in view.superview.constraints) {
                    if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] && 
                    constraint.firstAttribute == NSLayoutAttributeLeading) {
                        [view.superview removeConstraint:constraint];
                    }
                }
                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view
                                                                           attribute:NSLayoutAttributeTrailing
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:view.superview
                                                                           attribute:NSLayoutAttributeTrailing
                                                                          multiplier:1.0
                                                                            constant:0]];
                self.applied = YES;
            }
            break;
        }
    }
}

@end

@implementation UINavigationItem (SXFixSpace)

+(void)load {
    [self swizzleInstanceMethodWithOriginSel:@selector(setLeftBarButtonItem:)
                                 swizzledSel:@selector(sx_setLeftBarButtonItem:)];
    [self swizzleInstanceMethodWithOriginSel:@selector(setRightBarButtonItem:)
                                 swizzledSel:@selector(sx_setRightBarButtonItem:)];
}

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem{
    if (leftBarButtonItem.customView) {
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 11) {
            UIView *customView = leftBarButtonItem.customView;
            BarView *barView = [[BarView alloc]initWithFrame:customView.bounds];
            [barView addSubview:customView];
            customView.center = barView.center;
            [barView setPosition:SXBarViewPositionLeft];
            [self setLeftBarButtonItems:nil];
            [self sx_setLeftBarButtonItem:[[UIBarButtonItem alloc]initWithCustomView:barView]];
        }else {
            [self sx_setLeftBarButtonItem:nil];
            [self setLeftBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], leftBarButtonItem]];
        }
    }else {
        [self setLeftBarButtonItems:nil];
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

-(void)sx_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem{
    if (rightBarButtonItem.customView) {
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 11) {
            UIView *customView = rightBarButtonItem.customView;
            BarView *barView = [[BarView alloc]initWithFrame:customView.bounds];
            [barView addSubview:customView];
            customView.center = barView.center;
            [barView setPosition:SXBarViewPositionRight];
            [self setRightBarButtonItems:nil];
            [self sx_setRightBarButtonItem:[[UIBarButtonItem alloc]initWithCustomView:barView]];
        } else {
            [self sx_setRightBarButtonItem:nil];
            [self setRightBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], rightBarButtonItem]];
        }
    }else {
        [self setRightBarButtonItems:nil];
        [self sx_setRightBarButtonItem:rightBarButtonItem];
    }
}

@end

使用前:
图片

使用后:
图片

我不需要需改任何界面上的代码,在iOS 11下解决了导航栏按钮位置问题
当然你也能在做扩展,是偏移多少,修改约束的值即可
上面部分代码省略,完整demo请移步下载

使用中可能会遇到的问题及解决方法:
1. 某一个界面在push一个新界面之后再返回回来之后位置就还原了
解决方案其实很简单,只要将设置leftItem的方法写在viewWillAppear中即可,这样即可保证约束不会被系统重置
2. demo中的删除约束的判断仅仅是我个人项目中的判断,每个开发者的项目因为各种因素可能会有不同的影响,大家可以根据项目自行判断需要删除的约束条件,亦或者是不删除约束也是可以的

上面的问题另外一个解决方法:
使用layoutMargins这个属性
图片
我们遍历图层大致可以看到这样的

<_UINavigationBarContentView: 0x7fc141607250; frame = (0 0; 414 44); layer = <CALayer: 0x608000038cc0>>

这个UINavigationBarContentView平铺在导航栏中作为iOS11的各个按钮的父视图,该视图的所有的子视图都会有一个layoutMargins被占用,也就是系统调整的占位,我们只要把这个置空就行了.那样的话该视图下的所有的子视图的空间就会变成我们想要的那样,当然为了保险起见,该视图的父视图也就是bar的layoutMargins也置空,这样 整个bar就会跟一个普通视图一样了 左右的占位约束就不存在了

于是就出现了这样的代码

@implementation UINavigationBar (SXFixSpace)
+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethodWithOriginSel:@selector(layoutSubviews)
                                     swizzledSel:@selector(sx_layoutSubviews)];
    });
}

-(void)sx_layoutSubviews{
    [self sx_layoutSubviews];

    if (deviceVersion >= 11) {
        self.layoutMargins = UIEdgeInsetsZero;
        for (UIView *subview in self.subviews) {
            if ([NSStringFromClass(subview.class) containsString:@"ContentView"]) {
                subview.layoutMargins = UIEdgeInsetsZero;//可修正iOS11之后的偏移
            }
        }
    }
}

@end

是的,这一次的修正方式何其的轻松,之前饶了太多的弯路….

于是在结合iOS11之前的特性,和并出新的解决导航栏按钮问题的新的解决方案,
这一次,修正的更加彻底
相较于上一次的优势,
1.可以使用itmes方式设置多个按钮
2.可以不写在viewWillAppear中也可以满足push和pop不更改约束的问题
3.不对约束进行修改,修改的是layoutMargins,使其默认的20变成0,这样不影响导航栏中其他视图的约束冲突问题
4.代码量不重,和之前不通,这次仅仅是调整layoutMargins,不需要为了修改约束等再添加图层等,具体可以看我之前的,比较写法差异
5.最后代码也更加简洁.

@implementation UINavigationBar (SXFixSpace)

+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethodWithOriginSel:@selector(layoutSubviews)
                                     swizzledSel:@selector(sx_layoutSubviews)];
    });
}

-(void)sx_layoutSubviews{
    [self sx_layoutSubviews];

    if (deviceVersion >= 11) {
        self.layoutMargins = UIEdgeInsetsZero;
        CGFloat space = sx_tempFixSpace !=0 ? sx_tempFixSpace : sx_defaultFixSpace;
        for (UIView *subview in self.subviews) {
            if ([NSStringFromClass(subview.class) containsString:@"ContentView"]) {
                subview.layoutMargins = UIEdgeInsetsMake(0, space, 0, space);//可修正iOS11之后的偏移
                break;
            }
        }
    }
}

@end

@implementation UINavigationItem (SXFixSpace)

+(void)load {
    [self swizzleInstanceMethodWithOriginSel:@selector(setLeftBarButtonItem:)
                                 swizzledSel:@selector(sx_setLeftBarButtonItem:)];

    [self swizzleInstanceMethodWithOriginSel:@selector(setLeftBarButtonItems:)
                                 swizzledSel:@selector(sx_setLeftBarButtonItems:)];

    [self swizzleInstanceMethodWithOriginSel:@selector(setRightBarButtonItem:)
                                 swizzledSel:@selector(sx_setRightBarButtonItem:)];

    [self swizzleInstanceMethodWithOriginSel:@selector(setRightBarButtonItems:)
                                 swizzledSel:@selector(sx_setRightBarButtonItems:)];
}

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem {
    if (leftBarButtonItem.customView) {
        if (deviceVersion >= 11) {
            sx_tempFixSpace = 0;
            [self sx_setLeftBarButtonItem:leftBarButtonItem];
        } else {
            [self setLeftBarButtonItems:@[leftBarButtonItem]];
        }
    } else {
        sx_tempFixSpace = 20;
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

-(void)sx_setLeftBarButtonItems:(NSArray<UIBarButtonItem *> *)leftBarButtonItems {
    NSMutableArray *items = [NSMutableArray arrayWithObject:[self fixedSpaceWithWidth:sx_defaultFixSpace-20]];//可修正iOS11之前的偏移
    [items addObjectsFromArray:leftBarButtonItems];
    [self sx_setLeftBarButtonItems:items];
}

-(void)sx_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem{
    if (rightBarButtonItem.customView) {
        if (deviceVersion >= 11) {
            sx_tempFixSpace = 0;
            [self sx_setRightBarButtonItem:rightBarButtonItem];
        } else {
            [self setRightBarButtonItems:@[rightBarButtonItem]];
        }
    } else {
        sx_tempFixSpace = 20;
        [self sx_setRightBarButtonItem:rightBarButtonItem];
    }
}

-(void)sx_setRightBarButtonItems:(NSArray<UIBarButtonItem *> *)rightBarButtonItems{
    NSMutableArray *items = [NSMutableArray arrayWithObject:[self fixedSpaceWithWidth:sx_defaultFixSpace-20]];//可修正iOS11之前的偏移
    [items addObjectsFromArray:rightBarButtonItems];
    [self sx_setRightBarButtonItems:items];
}

-(UIBarButtonItem *)fixedSpaceWithWidth:(CGFloat)width {
    UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
                                                                               target:nil
                                                                               action:nil];
    fixedSpace.width = width;
    return fixedSpace;
}

@end

效果和之前的解决方案几乎一样,只能说这次是换了思路实现的
图片
可以很明显的看到间距不是20,至于是多少?
图片
我用宏定义的方式设置的,你也可以自定义,或者使用其他的方式确定其大小。

layoutMargins解决方法的demo地址

9. 导航栏的边距变化

在iOS11对导航栏里面的item的边距也做了调整:
(1)如果只是设置了titleView,没有设置barbutton,把titleview的宽度设置为屏幕宽度,则titleview距离屏幕的边距,iOS11之前,在iPhone6p上是20p,在iPhone6p之前是16p;iOS11之后,在iPhone6p上是12p,在iPhone6p之前是8p。

(2)如果只是设置了barbutton,没有设置titleview,则在iOS11里,barButton距离屏幕的边距是20p和16p;在iOS11之前,barButton距离屏幕的边距也是20p和16p。

(3)如果同时设置了titleView和barButton,则在iOS11之前,titleview和barbutton之间的间距是6p,在iOS11上titleview和barbutton之间无间距,如下图:
图片
图片

10. 导航栏返回按钮

之前的代码通过下面的方式自定义返回按钮(可以隐藏返回按钮的标题)

UIImage *backButtonImage = [[UIImage imageNamed:@"icon_tabbar_back"]
    resizableImageWithCapInsets:UIEdgeInsetsMake(0, 18, 0, 0)];
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:backButtonImage
                                                  forState:UIControlStateNormal
                                                barMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60)
                                                     forBarMetrics:UIBarMetricsDefault];

iOS 11 中setBackButtonTitlePositionAdjustment:UIOffsetMake没法把按钮移出navigation bar。
解决方法是设置navigationController的backIndicatorImage和backIndicatorTransitionMaskImage

UIImage *backButtonImage = [[UIImage imageNamed:@"icon_tabbar_back"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
self.navigationBar.backIndicatorImage = backButtonImage;
self.navigationBar.backIndicatorTransitionMaskImage = backButtonImage;

iOS 11 想通过setBackButtonTitlePositionAdjustment:UIOffsetMake隐藏返回按钮文字,可以像下面这样做适配:

   // 隐藏导航栏返回按钮文字
    if (@available(iOS 11, *)) {
        [[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(-200, 0)
                                                             forBarMetrics:UIBarMetricsDefault];
    } else {
        [[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60)
                                                             forBarMetrics:UIBarMetricsDefault];
    }

四. 管理 margins 和 insets

1. layout margins

基于约束的Auto Layout,使我们搭建能够动态响应内部和外部变化的用户界面。Auto Layout为每一个view都定义了marginmargin指的是控件显示内容部分的边缘和控件边缘的距离。
可以用layoutMargins或者layoutMarginsGuide属性获得view的margin, margin是视图内部的一部分。layoutMargins允许获取或者设置UIEdgeInsets结构的marginlayoutMarginsGuide则获取到只读的UILayoutGuide对象。

在iOS11新增了一个属性:directional layout margins,该属性是NSDirectionalEdgeInsets结构体类型的属性:

typedef struct NSDirectionalEdgeInsets {  
    CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets API_AVAILABLE(ios(11.0),tvos(11.0),watchos(4.0));

layoutMarginsUIEdgeInsets结构体类型的属性:

typedef struct UIEdgeInsets {  
CGFloat top, left, bottom, right;
} UIEdgeInsets;

从上面两种结构体的对比可以看出,NSDirectionalEdgeInsets属性用 leading 和 trailing 取代了之前的 left 和 right。

directional layout margins属性的说明如下:

directionalLayoutMargins.leading is used on the left when the user interface direction is LTR and on the right for RTL.
Vice versa for directionalLayoutMargins.trailing.

例子:当你设置了trailing = 30;当在一个right to left 语言下trailing的值会被设置在view的左边,可以通过layoutMargin的left属性读出该值。如下图所示:
图片
还有其他一些更新。自从引入layout margins,当将一个view添加到viewController时,viewController会修复view的layoutMargins为UIKit定义的一个值,这些调整对外是封闭的。从iOS11开始,这些不再是一个固定的值,它们实际是最小值,你可以改变view的layoutMargins为任意一个更大的值。而且,viewController新增了一个属性:viewRespectsSystemMinimumLayoutMargins,如果你设置该属性为”false”,你就可以改变你的layoutMargins为任意你想设置的值,包括0,如下图所示:
图片

五. 安全区域(Safe Area)

在 iOS 11 上运行 tableView 向下偏移 64px 或者 20px,因为 iOS 11 废弃了 automaticallyAdjustsScrollViewInsets,而是给 UIScrollView 增加了 contentInsetAdjustmentBehavior 属性。避免这个坑的方法是要判断

if (@available(iOS 11.0, *)) {
_tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}else {
self.automaticallyAdjustsScrollViewInsets = NO;
}
#define  adjustsScrollViewInsets(scrollView)\
do {\
_Pragma("clang diagnostic push")\
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"")\
if ([scrollView respondsToSelector:NSSelectorFromString(@"setContentInsetAdjustmentBehavior:")]) {\
    NSMethodSignature *signature = [UIScrollView instanceMethodSignatureForSelector:@selector(setContentInsetAdjustmentBehavior:)];\
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];\
    NSInteger argument = 2;\
    invocation.target = scrollView;\
    invocation.selector = @selector(setContentInsetAdjustmentBehavior:);\
    [invocation setArgument:&argument atIndex:2];\
    [invocation retainArguments];\
    [invocation invoke];\
}\
_Pragma("clang diagnostic pop")\
} while (0)

还有的发现某些界面tableView的sectionHeader、sectionFooter高度与设置不符的问题,在iOS11中如果不实现 -tableView: viewForHeaderInSection:和-tableView: viewForFooterInSection: ,则-tableView: heightForHeaderInSection:和- tableView: heightForFooterInSection:不会被调用,导致它们都变成了默认高度,这是因为tableView在iOS11默认使用Self-Sizing,tableView的estimatedRowHeight、estimatedSectionHeaderHeight、 estimatedSectionFooterHeight三个高度估算属性由默认的0变成了UITableViewAutomaticDimension,解决办法简单粗暴,就是实现对应方法或把这三个属性设为0。

如果你使用了Masonry,那么你需要适配safeArea

if (@available(iOS 11.0, *)) {
    make.edges.equalTo()(self.view.safeAreaInsets)
} else {
    make.edges.equalTo()(self.view)
}

如下图:照片应用程序
图片
从iOS 7以来,我们在整个操作系统中都有这些半透明的bars,苹果鼓励我们通过这些bars绘制内容,我们是通过viewController 的edgesForExtendedLayout属性来做这些的。
iOS 7 开始,在UIViewController中引入的topLayoutGuidebottomLayoutGuide在 iOS 11 中被废弃了!取而代之的就是safeArea的概念,safeArea是描述你的视图部分不被任何内容遮挡的方法。 它提供两种方式:safeAreaInsetssafeAreaLayoutGuide来提供给你safeArea的参照值,即 insets或者layout guidesafeArea区域如图所示:
图片
如果有一个自定义的viewController,你可能要添加你自己的bars,增加safeAreaInsets的值,可以通过一个新的属性:addtionalSafeAreaInsets来改变safeAreaInsets的值,当你的viewController改变了它的safeAreaInsets值时,有两种方式获取到回调:

UIView.safeAreaInsetsDidChange()
UIViewController.viewSafeAreaInsetsDidChange()

六. UIScrollView

如果有一些文本位于UI滚动视图的内部,并包含在导航控制器中,现在一般navigationContollers会传入一个contentInset给其最顶层的viewController的scrollView,在iOS11中进行了一个很大的改变,不再通过scrollView的contentInset属性了,而是新增了一个属性:adjustedContentInset,通过下面两种图的对比,能够表示adjustContentInset表示的区域:
图片

图片
新增的contentInsetAdjustmentBehavior属性用来配置adjustedContentInset的行为,该结构体有以下几种类型:

typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {  
    UIScrollViewContentInsetAdjustmentAutomatic, 
    UIScrollViewContentInsetAdjustmentScrollableAxes,
    UIScrollViewContentInsetAdjustmentNever,
    UIScrollViewContentInsetAdjustmentAlways,
}

@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset;

//adjustedContentInset值被改变的delegate
- (void)adjustedContentInsetDidChange; 
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView;

七. UITableView

1. 在iOS 11中默认启用Self-Sizing

这个应该是UITableView最大的改变。我们知道在iOS8引入Self-Sizing 之后,我们可以通过实现estimatedRowHeight相关的属性来展示动态的内容,实现了estimatedRowHeight属性后,得到的初始contenSize是个估算值,是通过estimatedRowHeight x cell的个数得到的,并不是最终的contenSize,tableView不会一次性计算所有的cell的高度了,只会计算当前屏幕能够显示的cell个数再加上几个,滑动时,tableView不停地得到新的cell,更新自己的contenSize,在滑到最后的时候,会得到正确的contenSize。创建tableView到显示出来的过程中,contentSize的计算过程如下图:
图片

Self-Sizing在iOS11下是默认开启的,Headers, footers, and cells都默认开启Self-Sizing,所有estimated 高度默认值从iOS11之前的 0 改变为UITableViewAutomaticDimension:

@property (nonatomic) CGFloat estimatedRowHeight NS_AVAILABLE_IOS(7_0); // default is UITableViewAutomaticDimension, set to 0 to disable

如果目前项目中没有使用 estimateRowHeight 属性,在 iOS11 的环境下就要注意了,因为开启 Self-Sizing 之后,tableView 是使用 estimateRowHeight 属性的,这样就会造成 contentSize 和 contentOffset 值的变化,如果是有动画是观察这两个属性的变化进行的,就会造成动画的异常,因为在估算行高机制下,contentSize 的值是一点点地变化更新的,所有 cell 显示完后才是最终的 contentSize 值。因为不会缓存正确的行高,tableView reloadData的时候,会重新计算 contentSize,就有可能会引起 contentOffset 的变化。此外,也看到有开发者被此变化影响到 MJRefresh 上拉刷新功能。
iOS11 下不想使用 Self-Sizing 的话,可以通过以下方式关闭:

self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
if#available(iOS11.0, *) {
self.contentInsetAdjustmentBehavior= .never
self.estimatedRowHeight=0
self.estimatedSectionHeaderHeight=0
self.estimatedSectionFooterHeight=0
}else{
}

iOS11下,如果没有设置estimateRowHeight的值,也没有设置rowHeight的值,那contentSize计算初始值是 44 * cell的个数,如下图:
图片

2. separatorInset 扩展

iOS 7 引入separatorInset属性,用以设置 cell 的分割线边距,在 iOS 11 中对其进行了扩展。可以通过新增的UITableViewSeparatorInsetReference枚举类型的separatorInsetReference属性来设置separatorInset属性的参照值。

typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {  
    UITableViewSeparatorInsetFromCellEdges,   //默认值,表示separatorInset是从cell的边缘的偏移量
    UITableViewSeparatorInsetFromAutomaticInsets  //表示separatorInset属性值是从一个insets的偏移量
}

下图清晰的展示了这两种参照值的区别:
图片

3. Table Views 和 Safe Area

有以下几点需要注意:

  • separatorInset 被自动地关联到 safe area insets,因此,默认情况下,表视图的整个内容避免了其根视图控制器的安全区域的插入。
  • UITableviewCell 和 UITableViewHeaderFooterView的 content view 在安全区域内;因此你应该始终在 content view 中使用add-subviews操作。
  • 所有的 headers 和 footers 都应该使用UITableViewHeaderFooterView,包括 table headers 和 footers、section headers 和 footers。
4. 滑动操作(Swipe Actions)

在iOS8之后,苹果官方增加了UITableVIew的右滑操作接口,即新增了一个代理方法(tableView: editActionsForRowAtIndexPath:)和一个类(UITableViewRowAction),代理方法返回的是一个数组,我们可以在这个代理方法中定义所需要的操作按钮(删除、置顶等),这些按钮的类就是UITableViewRowAction。这个类只能定义按钮的显示文字、背景色、和按钮事件。并且返回数组的第一个元素在UITableViewCell的最右侧显示,最后一个元素在最左侧显示。从iOS 11开始有了一些改变,首先是可以给这些按钮添加图片了,然后是如果实现了以下两个iOS 11新增的代理方法,将会取代(tableView: editActionsForRowAtIndexPath:)代理方法:

// Swipe actions
// These methods supersede -editActionsForRowAtIndexPath: if implemented
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView leadingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath

这两个代理方法返回的是UISwipeActionsConfiguration类型的对象,创建该对象及赋值可看下面的代码片段:

- ( UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath {
    //删除
    UIContextualAction *deleteRowAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:@"delete" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
        [self.titleArr removeObjectAtIndex:indexPath.row];
        completionHandler (YES);
    }];
    deleteRowAction.image = [UIImage imageNamed:@"icon_del"];
    deleteRowAction.backgroundColor = [UIColor blueColor];

    UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[deleteRowAction]];
    return config;
}

创建UIContextualAction对象时,UIContextualActionStyle有两种类型,如果是置顶、已读等按钮就使用UIContextualActionStyleNormal类型,delete操作按钮可使用UIContextualActionStyleDestructive类型,当使用该类型时,如果是右滑操作,一直向右滑动某个cell,会直接执行删除操作,不用再点击删除按钮,这也是一个好玩的更新。

typedef NS_ENUM(NSInteger, UIContextualActionStyle) {
    UIContextualActionStyleNormal,
    UIContextualActionStyleDestructive
} NS_SWIFT_NAME(UIContextualAction.Style)

滑动操作这里还有一个需要注意的是,当cell高度较小时,会只显示image,不显示title,当cell高度够大时,会同时显示image和title。我写demo测试的时候,因为每个cell的高度都较小,所以只显示image,然后我增加cell的高度后,就可以同时显示image和title了。见下图对比:
图片

八. UIBarItem

WWDC通过iOS新增的文件管理App:Files开始介绍,在Files这个APP中能够看到iOS11中UIKit’s Bars的一些新特性:在浏览功能上的大标题视图(向上滑动后标题会回到原来的UI效果)、横屏状态下tab上的文字和icon会变为左右排列。我用iOS11的模拟器体验了一下Files这个APP,如下图所示:
竖屏

横屏

在iPhone上,tab上的图标较小,tab bar较小,这样垂直空间可多放置内容。如果有人看不清楚tab bar上的图标或文字,可以通过长按tab bar上的任意item,会将该item显示在HUD上,这样可以清楚的看清icon和text。对tool bar 和 navigation bar同理,长按item也会放大显示。如下图显示:
这里写图片描述

UIBarItem是UI tab bar item和UI bar button item的父类,要想实现上面介绍的效果,只需要为UIBarItem 设置landscapeImagePhone属性,在storyboard中也支持这个设置,对于HUD的image需要设置另一个iOS11新增的属性:largeContentSizeImage,关于这部分更详细的讨论,可以参考 WWDC2017 Session 215:What’s New in Accessibility

九. iOS11访问相册权限变更问题

在更新 iOS11 之后,保存到相册出现 crash 现象,大家都知道访问相册需要申请用户权限。

相册权限需要在 info.plist—Property List 文件中添加 NSPhotoLibraryUsageDescription 键值对,描述文字不能为空。

iOS11 之前:访问相册和存储照片到相册(读写权限),需要用户授权,需要添加NSPhotoLibraryUsageDescription(info.plist 显示为 Privacy - Photo Library Usage Description)。

iOS11 之后:默认开启访问相册权限(读权限),无需用户授权,无需添加NSPhotoLibraryUsageDescription,适配 iOS11 之前的还是需要加的。 添加图片到相册(写权限),需要用户授权,需要添加 NSPhotoLibraryAddUsageDescription(info.plist 显示为 Privacy - Photo Library Additions Usage Description),否则可能会崩,可能会崩,可能会崩

十. 全新的 HEIC 格式原图

对于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);

十一. iPhoneX

1. TouchID -> FaceID

iPhone X 只有 faceID,没有touchID,如果你的应用有使用到 touchID 解锁的地方,这里要根据设备机型进行相应的适配。

2. LaunchImage

关于iPhoneX(我就不吐槽刘海了…),如果你的APP在iPhoneX上运行发现没有充满屏幕,上下有黑色区域,那么你应该也像我一样LaunchImage没有用storyboard而是用的Assets,解决办法添加1125x2436尺寸的启动图。

从bundle中取当前的启动图片,图片名字可以直接到.app文件的bundle中查找。

NSDictionary * dict = @{@"320x480" : @"LaunchImage-700", @"320x568" : @"LaunchImage-700-568h", @"375x667" : @"LaunchImage-800-667h", @"414x736" : @"LaunchImage-800-Portrait-736h", @"375x812" : @"LaunchImage-1100-Portrait-2436h", @"414x896" : @"LaunchImage-1200-Portrait-2688h"};
    NSString * key = [NSString stringWithFormat:@"%dx%d", (int)[UIScreen mainScreen].bounds.size.width, (int)[UIScreen mainScreen].bounds.size.height];
    if ([key isEqualToString:@"414x896"] && IS_SCREEN_61_INCH) {
        _launchImageView.imageView.image = [UIImage imageNamed:@"LaunchImage-1200-Portrait-1792h"];
    } else {
        _launchImageView.imageView.image = [UIImage imageNamed:dict[key]];
    }
3. 状态栏 和 导航栏

图片

图片

关于状态栏另外两个需要注意的地方:

  • 不要在 iPhone X 下隐藏状态栏,一个原因是显示内容足够高了,另一个是这样内容会被刘海切割。
  • 现在通话或者其它状态下,状态栏高度不会变化了,程序不需要去做兼容。
4. UITabBar

iPhoneX不止多了刘海,底部还有一个半角的矩形,使得tabbar多出来了34p的高度,不过不管导航栏和tabbar一般系统都会自动适配safeArea。

注意横屏下的 iPhoneX 的底部危险区域高度为21,UITabBar高度为32,整个底部占掉了屏幕的53高度。

5. 一些宏和常量
#define kDevice_Is_iPhoneX ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO)
let LL_iPhoneX = (kScreenW == Double(375.0) && kScreenH == Double(812.0) ?true:false)
let kNavibarH = LL_iPhoneX ? Double(88.0) : Double(64.0)
let kTabbarH = LL_iPhoneX ? Double(49.0+34.0) : Double(49.0)
let kStatusbarH = LL_iPhoneX ? Double(44.0) : Double(20.0)
6. 设计原则

在设计方面,苹果官方文档 Human Interface Guidelines 有明确要求,下面结合图例进行说明:

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

图片

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

图片

3. 安全区域以外的部分不允许有任何与用户交互的控件

图片

上面这张图内含信息略多

  • 安全区域以外的部分不允许进行用户交互的,意味着下面这些情况 Apple 官方是不允许的
    • 状态栏在非安全区域,文档中也提到,除非可以通过隐藏状态栏给用户带来额外的价值,否则最好把状态栏还给用户
    • 底部虚拟区是替代了传统home键,高度为34pt,通过上滑可呼起多任务管理,考虑到手势冲突,这部分也是不允许有任何可交互的控件,但是设计的背景图要覆盖到非安全区域
    • 不要让 界面中的元素 干扰底部的主屏幕指示器
4. 安全区域以外的部分不允许有任何与用户交互的控件

在横屏状态下,不能因为刘海的原因将内容向左或者向右便宜,要保证内容的中心对称
图片

图片

5. 重复使用现有图片时,注意长宽比差异。iPhone X 与常规 iPhone 的屏幕长宽比不同,因此,全屏的 4.7 寸屏图像在 iPhone X 上会出现裁切或适配宽度显示。所以,这部分的视图需要根据设备做出适配。

图片

7. 横屏适配

关于 safe area,使用 safeAreaLayoutGuide 和 safeAreaInset 就能解决大部分问题,但是横屏下还可能会产生一些问题,需要额外适配
问题一. 横屏模式下状态栏问题
看了下 iPhoneX 模拟器中,桌面没有横屏模式,但是所有预装 App 横屏都没有状态栏。自己新建了工程,发现代码中重写 - (BOOL)prefersStatusBarHidden 也无法让横屏下出现状态栏。但是看到网上 这篇文章(戳我可看) ,用比较 hacker 的方法实现的,重写 setNeedsStatusBarAppearanceUpdate 不做任何事情,导致竖屏切横屏没有把状态条去掉,高度应该还是横屏下的44,但是这个没有隐藏的状态条并没有影响横屏模式下从最顶部开始的 SafeArea,所以会导致适配变得有点麻烦,不能完全按照 SafeArea 那一套做相对布局了(需要考虑这种特殊 case)。

问题二. TableViewCell 的 contentView 的 frame 问题

图片
产生这个原因代码是:[headerView.contentView setBackgroundColor:[UIColor headerFooterColor]]

这个写法看起来没错,但是只有在 iPhone X 上有问题,之前所有版本的 iPhone 上 tableView 的 cell 和它的 contentView 的大小是相同的,开发者相对 cell 布局和相对 contentView 布局效果上不会有太大区别,但是在 iPhone X 下,由于刘海和圆角的存在,tableView 的 contentView 会被裁切,所以所有的布局都应该被调整为相对 contentView 布局,否则会越界。
图片
解决方法:设置backgroundView颜色 [headerView.backgroundView setBackgroundColor:[UIColor headerFooterColor]]

8. 滑动手势

iPhone X 最大的改变就是底部那个无时无刻不存在的 homeBar了,代替了原来home按键的功能,系统级的任务切换和回到桌面 、、,都是上滑这个细细的长条。
图片
所以苹果爸爸的意思是:

赶紧把你自己写的上滑手势乖乖删掉~

当然如果app确实需要这个手势,可以打开程序开关覆盖系统的手势,但是这样用户就需要滑动两次来回到桌面了,这会让他们非常怀念home键。

9. 键盘区别

首先是 iPhone X 下的键盘和其他系统有区别,会多出来那个很有趣的animateEmoji工具栏,所以在做键盘相关处理的时候要关注兼容性问题,至少:高度不要写死了……

十二. Xcode 手动编译失败

1. 编译出现一堆奇葩的问题

尝试将 Applications 文件夹下的 Xcode.app 重命名为 Xcode9.app 解决了我遇到的问题。

2. Failed to read file attributes for Images.xcassets in Xcode 9

升级到 Xcode9 以后总是遇到这个奇怪的问题,上网查了下,在 stack overflow 这个帖子里面找到了答案。

Removing the reference of Images.xcassets and adding it again in Project resolved the error.

3. 第三方依赖库问题

ReactiveCocoa Unknown warning group ‘-Wreceiver-is-weak’,ignored警告
图片

简书项目开启Treat warning as error,所有警告都会被当成错误,因此必须解决掉。
RACObserve宏定义如下:

#define RACObserve(TARGET, KEYPATH) \
    ({ \
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \
        __weak id target_ = (TARGET); \
        [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
        _Pragma("clang diagnostic pop") \
    })

在之前的Xcode中如果消息接受者是一个weak对象,clang编译器会报receiver-is-weak警告,所以加了这段push&pop,最新的clang已经把这个警告给移除,所以没必要加push&pop了。
ReactiveCocoa已经不再维护OC版本,大多数OC开发者用的都是2.5这个版本,只能自己fork一份了,谁知github上的v2.5代码不包含对应的.podspec文件,只好到CocoaPods/Specs上将对应的json文件翻译成.podspec文件,如果你也有这个需要,可以修改Podfile如下

pod 'ReactiveCocoa', :git => 'https://github.com/zhao0/ReactiveCocoa.git', :tag => '2.5.2'
4. 注意事项
  • Xcode9 打包版本只能是 8.2 及以下版本, 或者 9.0 及更高版本
  • Xcode9 不支持 8.3 和 8.4 版本
  • Xcode9 新打包要在构建版本的时候加入 1024*1024 AppStore Icon
  • 拖动文件或文件夹到工程中,可能会出现代码文件或图片没有加入到 target 中,出现编译不过或者运行时图片总是没显示,需要特别注意
  • Command 键复原。可在 Preferences –> Navigation –> Command-click 中选择 Jumps to Defintion 即可。
5. 一些好玩的新功能
  • 鸡肋的无线调试功能(iPhone的电池…)可在 Window –> Devices and Simulators中勾选那两个选项。前提是此设备已 run 过并处于同一局域网下。
  • 在 Asset 中,可以创建颜色了。右键选择 New image set,填充RGBA值或十六进制值即可。使用中直接使用新的colorwithname,参数填入创建时的名字即可。不过记得区分系统版本。
6. 模拟器新功能


  • 第一时间很多公司都买不到原价的 iPhoneX 的测试机,会给测试带来不方便,可以借助模拟器安装 app 去做测试工作

启动运行模拟器:
xcrun instruments -w ‘iPhone 6 Plus’

在已经启动好的模拟器中安装应用:
xcrun simctl install booted Calculator.app (这里要特别注意,是app,不是ipa 安装时需要提供的是APP的文件路径)

  • 在全屏模式下使用 Xcode 模拟器

  • 一次打开多个模拟器
  • 缩放模拟器就像调整视窗大小一样简单
  • 记录模拟器的视频

    在Xcode 9官方的”What’s new”文档中,苹果声称现在可以录制模拟器屏幕视频,即使在旧版本中,只要使用simctl也可以做到,在界面上找不到地方可以启用视频录制(除了iOS 11中的内置屏幕录制)。
    要获取视频档案,请执行以下代码:

    xcrun simctl io booted recordVideo –type=mp4

    booted– 表示simctl选择当前启动的模拟器,如果你有多个已启动的模拟器,simctl将选择当前正在操作的那一个模拟器。

  • 使用 Finder 共享文件到模拟器

    现在,模拟器有了 Finder 扩展功能,你可以直接从 Finder 窗口共享文件。

    你也可以执行以下simctl命令,使用图像/视频文件进行类似操作:

    xcrun simctl addmedia booted

    很高兴有这样的操作方法,但是对我而言,将文件拖放至模拟器窗口似乎快很多。

  • 模拟器上打开 URL

    这个也能使用simctl,所以你也可以在旧版本的模拟器上打开自定义的URL schemes。
    拖拽
    以你指定的任何URL执行以下命令:

    xcrun simctl openurl booted

    关于Apple所有URL schemes的列表,请查看文档.

  • 快速找到应用程序的文件夹

    再来介绍一个simctl的命令,你可以使用单个命令在文件系统上获取应用程序的资料夹,只需要知道应用程序的bundle identifier并执行以下命令:

    xcrun simctl get_app_container booted

    或者你可以使用open命令在 Finder 中更快打开目标文件夹:

    open xcrun simctl get_app_container booted -a Finder

  • 使用命令行参数(Command Line Args)在模拟器中启动应用程序

    使用simctl,你也可以从终端机上启动应用程序,并在其中传递一些命令列参数(甚至可以设置一些环境变量)。如果你想在应用程序中插入一些除错行为,这将非常有用。
    执行下列命令可以让你完成这项任务:

    xcrun simctl launch –console booted

    你可以从CommandLine.arguments获取这些命令行参数(这里是文件的链接)。

  • 透过Bundle ID获取完整的应用程序消息
    有时找出应用程序的档案或暂存数据位于文件系统上的位置很有用,如果你需要比simctl get_app_container更全面的资讯,simctl还有一个很好用的小工具,名为appinfo,它会以下列格式显示相关资讯:
    执行下面的命令并观察输出结果:

    xcrun simctl appinfo booted
  • 十三. xcodebuild 打包命令修改

    升级到最新的 Xcode9 以后,发现 jenkins 自动化打包失败了,后来看了下来,发现是 xcodebuild 命令签名失败,没有生成 ipa 包。在 这个帖子 中找到了解决方法。

    打包脚本错误提示如下:

    Error Domain=IDEDistributionSigningAssetStepErrorDomain Code=0 “Locating signing assets failed.” UserInfo={NSLocalizedDescription=Locating signing assets failed., IDEDistributionSigningAssetStepUnderlyingErrors=(
    “Error Domain=IDEProvisioningErrorDomain Code=9 \”\”HLCG.app\” requires a provisioning profile with the Associated Domains and Push Notifications features.\” UserInfo={NSLocalizedDescription=\”HLCG.app\” requires a provisioning profile with the Associated Domains and Push Notifications features., NSLocalizedRecoverySuggestion=Add a profile to the \”provisioningProfiles\” dictionary in your Export Options property list.}”
    )}
    error: exportArchive: “HLCG.app” requires a provisioning profile with the Associated Domains and Push Notifications features.

    解决办法:
    编辑 exportOptionsPlist 文件, 在其中添加

    <key>provisioningProfiles</key>
    <dict> <key>com.hula.xxxxxx</key>
    <string>HulaVenueDev</string> (此处名字获得见下文)
    </dict>

    如:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    <key>provisioningProfiles</key>
    <dict>
    <key>com.hula.xxxxxx</key>
    <string>HulaVenueDev</string>
    </dict>
    <key>compileBitcode</key>
    <false/>
    <key>teamID</key>
    <string>teamIDteamIDteamID</string>
    <key>method</key>
    <string>development</string>
    <key>uploadSymbols</key>
    <true/>
    </dict>
    </plist>

    provisioningProfile 名可以在 apple deveploer 后台获得。也可以在 mobileprovision 文件中获得。
    如图:
    这里写图片描述

    或 less dev.mobileprovision 找到Name
    这里写图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值