欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS11、iPhoneX、Xcode9 的注意点汇总

程序员文章站 2022-04-11 15:25:52
...

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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)"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
#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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

下图是 iPhone 常见型号的屏幕相关对比: 
更详细的信息可查阅:The Ultimate Guide To iPhone Resolutions 
iOS11、iPhoneX、Xcode9 的注意点汇总

iPhone 8 与 iPhone X 的尺寸对比: 
iOS11、iPhoneX、Xcode9 的注意点汇总

二. 状态栏

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

然后再设置。

三. 导航栏

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上,如图: 
iOS11、iPhoneX、Xcode9 的注意点汇总 
所以如果你的项目是自定义的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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

- (CGSize)intrinsicContentSize {
    return UILayoutFittingExpandedSize;
}
  • 1
  • 2
  • 3

3. 控制大标题的显示

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

navigationItem.largeTitleDisplayMode 

typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode) {  
/// 自动模式依赖上一个 item 的特性
UINavigationItemLargeTitleDisplayModeAutomatic,
/// 针对当前 item 总是启用大标题特性
UINavigationItemLargeTitleDisplayModeAlways,
/// Never 
UINavigationItemLargeTitleDisplayModeNever,
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

4. Navigation 集成 UISearchController

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

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

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

iOS11、iPhoneX、Xcode9 的注意点汇总

5. UINavigationController 和滚动交互

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

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

所以,如果你使用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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

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

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

iOS 11改动相当大的就是导航栏的部分,在原来的已经复杂的不要的图层中又新增了新的图层! 
iOS11、iPhoneX、Xcode9 的注意点汇总 
是的你没有看做,_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>>
  • 1
  • 2
  • 3
  • 4
  • 5

我们可以看到一个_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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

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

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

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

那么能不能不懂这些代码也满足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];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

[[UIBarButtonItem alloc] initWithCustomView:button];
  • 1

但是现在我想的是按钮添加在一个我们定义的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];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

那么这个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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

代码其实不复杂,就是遍历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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111

使用前: 
iOS11、iPhoneX、Xcode9 的注意点汇总

使用后: 
iOS11、iPhoneX、Xcode9 的注意点汇总

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

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

上面的问题另外一个解决方法: 
使用layoutMargins这个属性  
iOS11、iPhoneX、Xcode9 的注意点汇总
我们遍历图层大致可以看到这样的

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

这个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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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

于是在结合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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92

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

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之间无间距,如下图: 
iOS11、iPhoneX、Xcode9 的注意点汇总
iOS11、iPhoneX、Xcode9 的注意点汇总

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];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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;
  • 1
  • 2
  • 3

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];
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

四. 管理 margins 和 insets

1. layout margins

基于约束的Auto Layout,使我们搭建能够动态响应内部和外部变化的用户界面。Auto Layout为每一个view都定义了marginmargin指的是控件显示内容部分的边缘和控件边缘的距离。 
可以用layoutMargins或者layoutMarginsGuide属性获得view的marginmargin是视图内部的一部分。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));
  • 1
  • 2
  • 3

layoutMarginsUIEdgeInsets结构体类型的属性:

typedef struct UIEdgeInsets {  
CGFloat top, left, bottom, right;
} UIEdgeInsets;
  • 1
  • 2
  • 3

从上面两种结构体的对比可以看出,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.
  • 1
  • 2

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

五. 安全区域(Safe Area)

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

if (@available(iOS 11.0, *)) {
_tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}else {
self.automaticallyAdjustsScrollViewInsets = NO;
}
  • 1
  • 2
  • 3
  • 4
  • 5
#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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

还有的发现某些界面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)
}
  • 1
  • 2
  • 3
  • 4
  • 5

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

UIView.safeAreaInsetsDidChange()
UIViewController.viewSafeAreaInsetsDidChange()
  • 1
  • 2

六. UIScrollView

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

iOS11、iPhoneX、Xcode9 的注意点汇总
新增的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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

七. 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的计算过程如下图: 
iOS11、iPhoneX、Xcode9 的注意点汇总

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
  • 1

如果目前项目中没有使用 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;
  • 1
  • 2
  • 3
if#available(iOS11.0, *) {
self.contentInsetAdjustmentBehavior= .never
self.estimatedRowHeight=0
self.estimatedSectionHeaderHeight=0
self.estimatedSectionFooterHeight=0
}else{
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

iOS11下,如果没有设置estimateRowHeight的值,也没有设置rowHeight的值,那contentSize计算初始值是 44 * cell的个数,如下图: 
iOS11、iPhoneX、Xcode9 的注意点汇总

2. separatorInset 扩展

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

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

下图清晰的展示了这两种参照值的区别: 
iOS11、iPhoneX、Xcode9 的注意点汇总

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
  • 1
  • 2
  • 3
  • 4

这两个代理方法返回的是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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

typedef NS_ENUM(NSInteger, UIContextualActionStyle) {
    UIContextualActionStyleNormal,
    UIContextualActionStyleDestructive
} NS_SWIFT_NAME(UIContextualAction.Style)
  • 1
  • 2
  • 3
  • 4

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

八. UIBarItem

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

iOS11、iPhoneX、Xcode9 的注意点汇总

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

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);
  • 1
  • 2
  • 3

十一. iPhoneX

1. TouchID -> FaceID

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

2. LaunchImage

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

3. 状态栏 和 导航栏

iOS11、iPhoneX、Xcode9 的注意点汇总

iOS11、iPhoneX、Xcode9 的注意点汇总

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

  • 不要在 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)
  • 1
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)
  • 1
  • 2
  • 3
  • 4

6. 设计原则

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

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

iOS11、iPhoneX、Xcode9 的注意点汇总

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

iOS11、iPhoneX、Xcode9 的注意点汇总

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

iOS11、iPhoneX、Xcode9 的注意点汇总

上面这张图内含信息略多

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

在横屏状态下,不能因为刘海的原因将内容向左或者向右便宜,要保证内容的中心对称 
iOS11、iPhoneX、Xcode9 的注意点汇总

iOS11、iPhoneX、Xcode9 的注意点汇总

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

iOS11、iPhoneX、Xcode9 的注意点汇总

7. 横屏适配

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

问题二. TableViewCell 的 contentView 的 frame 问题

iOS11、iPhoneX、Xcode9 的注意点汇总 
产生这个原因代码是:[headerView.contentView setBackgroundColor:[UIColor headerFooterColor]]

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

8. 滑动手势

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

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

当然如果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警告 
iOS11、iPhoneX、Xcode9 的注意点汇总

简书项目开启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") \
    })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在之前的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'
  • 1

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 文件中获得。 
    如图: 
    iOS11、iPhoneX、Xcode9 的注意点汇总

    或 less dev.mobileprovision 找到Name 
    iOS11、iPhoneX、Xcode9 的注意点汇总

  • 相关标签: iOS 11xcode9