iOS 11新特性与适配

作者: 杰嗒嗒的阿杰 | 来源:发表于2017-09-20 13:52 被阅读4273次

    iOS 11正式发布了,下面整理了一些该版本下的特点还有如何进行兼容适配工作。

    1. UIView变化

    1.1. 更加方便的RTL边距设置

    在之前的系统中我们会使用layoutMargins来获取和设置控件显示内容部分的边缘与控件边缘的距离。在iOS 11中,新增directionalLayoutMargins属性来指定边距。这两个属性的结构定义如下:

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

    从结构上看主要是将UIEdgeInsets结构的leftright调整为NSDirectionalEdgeInsets结构的leadingtrailing。这一调整主要是为了Right To Left(RTL)语言下可以进行自动适配,例如:要实现文本每行尾部边距设置为30px,在以前做法则需要判断语言来区分哪些是RTL语言,然后再做设置,如:

    if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.view.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft)
    {
        // Right to left 语言下每行尾部在左边
        self.view.layoutMargins.left = 30;
    }
    else
    {
        self.view.layoutMargins.right = 30;
    }
    

    iOS 11 后则可以一步到位,如:

    self.view.directionalLayoutMargins = NSDirectionalEdgeInsetsMake(0, 0, 0, 30);
    

    注:测试时需要添加RTL本地化语言才能看到效果

    1.2. 安全区域

    在iOS 11中新增了安全区域的概念,目的是告诉开发者在这个区域下绘制的内容的显示才是有效的,否则会存在被遮挡的情况(特别是iPhoneX那帅气的刘海)。在UIView中新增safeAreaLayoutGuidesafeAreaInsets来获取屏幕的安全区域(对于frame布局时是很有用的)。如图所示:

    SafeArea示意图

    举个例子,在一个空白的UIViewController中,分别在viewDidLoadviewDidAppear方法中输出view.safeAreaInsets观察边距情况,代码如下:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSString *edgeStr = NSStringFromUIEdgeInsets(self.view.safeAreaInsets);
        NSString *layoutFrmStr = NSStringFromCGRect(self.view.safeAreaLayoutGuide.layoutFrame);
        NSLog(@"viewDidLoad safeAreaInsets = %@, layoutFrame = %@", edgeStr, layoutFrmStr);=
    }
    
    - (void)viewDidAppear:(BOOL)animated
    {
        [super viewDidAppear:animated];
        
        NSString *edgeStr = NSStringFromUIEdgeInsets(self.view.safeAreaInsets);
        NSString *layoutFrmStr = NSStringFromCGRect(self.view.safeAreaLayoutGuide.layoutFrame);
        NSLog(@"viewDidAppear safeAreaInsets = %@, layoutFrame = %@", edgeStr, layoutFrmStr);
    }
    

    可以看到其输出为:

    2017-09-19 14:45:50.246095+0800 Sample[5608:1365070] viewDidLoad safeAreaInsets = {0, 0, 0, 0}, layoutFrame = {{0, 0}, {375, 667}}
    2017-09-19 14:45:50.257807+0800 Sample[5608:1365070] viewDidAppear safeAreaInsets = {20, 0, 0, 0}, layoutFrame = {{0, 20}, {375, 603}}
    

    可见,在视图显示完成的时候View的顶部边距变为了20px,而这20px正是状态栏的高度。同样原理,如果你的是一个UINavigationController那在显示的时候view.safeAreaInsets就会变成{64, 0, 0, 0}注意:在该VC下所有的UIView及其子类获取到safeAreaInsets的值是相同的。

    如果你想准确地知道安全区域是什么时候被改变的,可以重写UIViewsafeAreaInsetsDidChange方法,在这个方法里面可以监听安全区域的边距调整的事件(如果使用的是UIViewController,其也提供相应方法来实现监听,下一章节会讲述该部分内容),代码如下:

    - (void)safeAreaInsetsDidChange
    {
        //写入变更安全区域后的代码...
    }
    

    如果你不想让safeAreaInsets影响你的视图布局,则可以将insetsLayoutMarginsFromSafeArea设置为NO,所有的视图布局将会忽略safeAreaInsets这个属性了。要注意的是,insetsLayoutMarginsFromSafeArea仅用于使用代码实现AutoLayout(如果你是使用Xib或者SB布局你的视图,那么对该属性的设置是无效的,至少我没有发现怎么可以让布局产生变化),即使该属性为NO,视图的safeAreaInsets还是一样有值,而且安全区域变更方法safeAreaInsetsDidChange一样被调用。可以参考下面示例代码:

    @interface ViewController ()
    
    @property (nonatomic, strong) UITableView *tableView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor yellowColor];
        
        self.view.insetsLayoutMarginsFromSafeArea = NO;
    
        self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
        self.tableView.translatesAutoresizingMaskIntoConstraints = NO;
        [self.view addSubview:self.tableView];
    
        NSArray<__kindof NSLayoutConstraint *> *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[tableView]-|" options:0 metrics:nil views:@{@"tableView" : self.tableView}];
        [self.view addConstraints:constraints];
    
        constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[tableView]|" options:0 metrics:nil views:@{@"tableView" : self.tableView}];
        [self.view addConstraints:constraints];
    }
    
    @end
    

    上面代码在insetsLayoutMarginsFromSafeArea属性尚未设置时其布局受SafeArea影响,效果如下:

    设置前效果

    设置后不再受SafeArea影响,效果如下:

    设置后效果

    2. UIViewController变化

    2.1. 废除API

    2.1.1. automaticallyAdjustsScrollViewInsets方法

    iOS 7中使用该方法来自动调整UIScrollViewcontentInset。在iOS 11之后将会使用UIScrollViewcontentInsetAdjustmentBehavior属性来代替该方法。

    2.1.2. topLayoutGuidebottomLayoutGuide属性

    iOS 7中使用这两个属性来指导带有导航栏(NaviagtionBar)和页签栏(TabBar)的视图排版。其作用如下图所示:

    topLayoutGuide & bottomLayoutGuide

    在iOS 11之后将使用安全区域(Safe Area)来代替该部分功能的实现。

    2.2. 排版

    2.2.1. additionalSafeAreaInsets属性

    iOS 11加入安全区域后,对于VC则可以通过该属性来对该区域附加一个边距信息。如:

    self.additionalSafeAreaInsets = UIEdgeInsetsMake(30, 0, 0, 30);
    

    注意:这里是附加边距,意思就是在原有的safeAreaInsets值中增加对应的边距值。如果原来的是{10, 0, 0, 10}, 则最后得出的边距是{40, 0, 0, 40}。

    2.2.2. systemMinimumLayoutMarginsviewRespectsSystemMinimumLayoutMargins属性

    该属性表示了一个系统最小的边距信息,所有的视图排版都应该遵循这个边距信息的。除非将viewRespectsSystemMinimumLayoutMargins设置为NO。

    2.2.3. viewLayoutMarginsDidChange方法

    根视图的边距变更时会触发该方法的回调。可以通过该方法来处理当边距改变时子视图的布局。

    2.2.4. viewSafeAreaInsetsDidChange方法

    当视图的安全区域发生变更时会触发该方法的回调。可以通过该方法来处理安全区域变更时的子视图布局。

    3. UINavigationBar变化

    iOS 11中加入了大标题模式,其显示效果如下所示:

    大标题效果图

    实现该效果需要将导航栏的prefersLargeTitles设置为YES,如:

    self.navigationController.navigationBar.prefersLargeTitles = YES;
    

    4. UINavigationItem变化

    4.1 控制大标题的显示

    如果你想控制每个视图的大标题是否显示,这需要使用UINavigationItemlargeTitleDisplayMode属性来控制大标题的显示。该属性为枚举类型,定义如下:

    typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode)
     {  
        /// 自动模式,会继承前一个NavigationItem所设置的模式
        UINavigationItemLargeTitleDisplayModeAutomatic,
        /// 当前 Navigationitem 总是启用大标题模式
        UINavigationItemLargeTitleDisplayModeAlways,
        /// 当前 Navigationitem 总是禁用大标题模式
        UINavigationItemLargeTitleDisplayModeNever,
    }
    

    根据上面的描述,可以在VC初始化init或者awakeFromNib方法中设置显示图标模式:

    self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeAlways;
    

    4.2 控制搜索控制器

    iOS 11 中新增了两个属性searchControllerhidesSearchBarWhenScrolling。这两个属性主要用于简化VC对UISearchController的集成以及视觉优化。其中searchController属性用于指定当前VC的一个搜索控制器。而hidesSearchBarWhenScrolling属性则用于控制当视图滚动时是否隐藏搜索栏的UI,当该值为YES时,搜索栏只有在内容视图(UIScrollView及其子类)顶部是才会显示,在滚动过程中会隐藏起来;当该值为NO时,则不受滚动影响一直显示在导航栏中。具体的代码实现如下:

    - (void)awakeFromNib
    {
        [super awakeFromNib];
        
        //设置SearchController到navigationItem
        self.searchController = [[UISearchController alloc] initWithSearchResultsController:self];
        self.navigationItem.searchController = self.searchController;
        self.navigationItem.hidesSearchBarWhenScrolling = YES;
    }
    

    效果如下图所示:

    包含搜索栏的视图效果 搜索栏隐藏后效果

    5. UIScrollView变化

    之前的系统中,如果你的滚动视图包含在一个导航控制器下,系统会自动地调整你的滚动视图的contentInset。而iOS 11新增adjustedContentInset属性取替之前contentInset的处理方式。这两者之间的关系如下图所示:

    adjustedContentInset & contentInset

    通过一个例子来验证这说法,代码如下:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        NSLog(@"viewDidLoad");
        NSLog(@"self.tableView.contentInset = %@", NSStringFromUIEdgeInsets(self.tableView.contentInset));
        NSLog(@"self.tableView.adjustedContentInset = %@", NSStringFromUIEdgeInsets(self.tableView.adjustedContentInset));
    }
    
    - (void)viewDidAppear:(BOOL)animated
    {
        [super viewDidAppear:animated];
        
        NSLog(@"viewDidAppear");
        NSLog(@"self.tableView.contentInset = %@", NSStringFromUIEdgeInsets(self.tableView.contentInset));
        NSLog(@"self.tableView.adjustedContentInset = %@", NSStringFromUIEdgeInsets(self.tableView.adjustedContentInset));
    }
    

    执行后输出下面信息:

    2017-09-20 11:54:09.361348+0800 Sample[1276:375286] viewDidLoad
    2017-09-20 11:54:09.361432+0800 Sample[1276:375286] self.tableView.contentInset = {0, 0, 0, 0}
    2017-09-20 11:54:09.361462+0800 Sample[1276:375286] self.tableView.adjustedContentInset = {0, 0, 0, 0}
    2017-09-20 11:54:09.420000+0800 Sample[1276:375286] viewDidAppear
    2017-09-20 11:54:09.420378+0800 Sample[1276:375286] self.tableView.contentInset = {0, 0, 0, 0}
    2017-09-20 11:54:09.420554+0800 Sample[1276:375286] self.tableView.adjustedContentInset = {20, 0, 0, 0}
    

    可见,tableView的adjustedContentInset自动改变了,但是contentInset的值是保持不变的。注:一定要是VC的根视图为UIScrollView或者其子类才能够得到adjustedContentInset的值,否则获取到的是空值。而且非根视图的滚动视图就会被安全区域所裁剪,看到的样式如下图所示:

    样式效果对比

    通过使用contentInsetAdjustmentBehavior属性可以控制 adjustedContentInset的变化。该属性为枚举类型,其定义如下:

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

    其中UIScrollViewContentInsetAdjustmentAutomaticUIScrollViewContentInsetAdjustmentScrollableAxes一样,ScrollView会自动计算和适应顶部和底部的内边距并且在scrollView 不可滚动时,也会设置内边距;UIScrollViewContentInsetAdjustmentNever表示不计算内边距;UIScrollViewContentInsetAdjustmentAlways则根据视图的安全区域来计算内边距。

    如果需要感知adjustedContentInset的变化,然后根据变化进行不同操作则可以通过重写新增的adjustedContentInsetDidChange方法或者实现UIScrollViewDelegate中的scrollViewDidChangeAdjustedContentInset方法来实现。如:

    //重写方法
    - (void)adjustedContentInsetDidChange
    {
        [super adjustedContentInsetDidChange];
    
        //执行操作...
    }
    
    //实现委托
    - (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView
    {
        //执行操作...
    }
    

    除了新增上述所说的边距相关属性外,还新增了contentLayoutGuideframeLayoutGuide属性,用于描述内容布局和整体布局信息。

    6. UI主线程操作日志提醒

    之前的系统中如果你不小心将UI放入非主线程操作时,Debug日志是没有任何信息反馈的,导致有时候在排错时非常困难。在新的Xcode 9中,如果你处于调试状态,将UI放入非主线程操作,如:

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            self.tv = [[UITableView alloc] initWithFrame:self.view.bounds];
            [self.view addSubview:self.tv];
            NSLog(@"self.tv.adjustedContentInset = %@", NSStringFromUIEdgeInsets(self.tv.adjustedContentInset));
            
        });
    

    Log中会出现下面提示:

    =================================================================
    Main Thread Checker: UI API called on a background thread: -[UIView bounds]
    PID: 16919, TID: 2972321, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 21
    Backtrace:
    4   Sample                              0x00000001004885dc __29-[ViewController viewDidLoad]_block_invoke + 112
    5   libdispatch.dylib                   0x000000010077149c _dispatch_call_block_and_release + 24
    6   libdispatch.dylib                   0x000000010077145c _dispatch_client_callout + 16
    7   libdispatch.dylib                   0x000000010077d56c _dispatch_queue_override_invoke + 980
    8   libdispatch.dylib                   0x0000000100782b54 _dispatch_root_queue_drain + 616
    9   libdispatch.dylib                   0x0000000100782880 _dispatch_worker_thread3 + 136
    10  libsystem_pthread.dylib             0x000000018300b130 _pthread_wqthread + 1268
    11  libsystem_pthread.dylib             0x000000018300ac30 start_wqthread + 4
    

    从日志中了解到一个Main Thread Checker的东西,根据苹果官方文档来看他是作用在AppKit(OSX中)、UIKit还有一些相关API上的后台线程,主要是用来监控这些框架中的接口是否在主线程中进行调用,如果没有则发出警告日志。因此,利用这个功能可以让我们快速地定位那些地方存在问题。

    7. 关于UIButton的设置图片变形问题

    在iOS 11中如果调用UIButtonsetImage或者setBackgrounImage方法,如果图片的尺寸大于按钮尺寸时则会被进行拉伸。如下图:

    变形的按钮图标

    对于上面问题,可以通过对按钮的宽度和高度进行约束来控制图标的大小。处理代码如下:

    if (@available(iOS 11.0, *))
    {
        NSLayoutConstraint *constraint = [btn.widthAnchor constraintEqualToConstant:35];
        constraint.active = YES;
        constraint = [btn.heightAnchor constraintEqualToConstant:35];
        constraint.active = YES;
    }
    

    调整后,图标显示正常:

    正常效果

    注:widthAnchorheightAnchor是iOS9之后增加的

    持续更新

    先写到这,其他同学可以针对iOS 11的问题进行提问,我会根据实际情况来补充文档并回答各位的问题。

    相关文章

      网友评论

      • rain__bow__:insetsLayoutMarginsFromSafeArea 您好,这个属性,怎么使用啊,我设置了怎么不管用
        杰嗒嗒的阿杰:@rain__bow__ 你的问题我研究过了,麻烦看回我的文章说明insetsLayoutMarginsFromSafeArea的那部分,希望能够帮到你:blush:
        rain__bow__:@杰嗒嗒的阿杰 直接用xcode9建立一个新的页面,页面中有一个tableview,用的AutoLayout 约束,并且关掉了useSafeAreaLayoutGuide,设置tableview的insetsLayoutMarginsFromSafeArea为NO,没有看到效果,iphone X上,页面底部还是有留白
        杰嗒嗒的阿杰:就是直接设置就有效果了,你要确认是不是基于AutoLayout来布局,而且要确认你的布局是不是依赖SafArea。
      • Hengry:写得很完善,受益匪浅,感谢
      • xiari1991:你好,非根视图是什么意思呢?我有试过以下两种:
        ```
        //方式1
        [self.view addSubview:self.view1];
        [self.view addSubview:self.tableView];

        //方式2
        [self.view addSubview:self.view1];
        [self.view1 addSubview:self.tableView];
        ```
        这两种范式,adjustedContentInset都是存在的,不知道你说的不是根试图指的是什么意思
        杰嗒嗒的阿杰:@yf_js 我又重新确认了一下,貌似不作为根视图adjustedContentInset属性返回值只会是{0, 0, 0, 0}。具体代码如下:

        self.tv = [[UITableView alloc] initWithFrame:self.view.bounds];
        [self.view addSubview:self.tv];
        NSLog(@"self.tv.adjustedContentInset = %@", NSStringFromUIEdgeInsets(self.tv.adjustedContentInset));

        输出:
        2017-09-26 09:34:42.765116+0800 Sample[16883:2961612] self.tv.adjustedContentInset = {0, 0, 0, 0}
        xiari1991:@杰嗒嗒的阿杰 在viewdidload中创建的
        杰嗒嗒的阿杰:@yf_js 我说的根视图是指vc的说代理的视图。例如UITableView中的tableView就属于根视图。我想问一下你示例中的tableView是属于后期在VC中创建的?
      • 鬼丶白:button的文字有下划线怎么解决
        杰嗒嗒的阿杰:@XCode_Boy :+1::+1:
        XCode_Boy:@soime 通用_辅助功能_按钮形状_关闭
        杰嗒嗒的阿杰:@soime 怎么出现下划线的,我这边没有模拟出来:flushed:

      本文标题:iOS 11新特性与适配

      本文链接:https://www.haomeiwen.com/subject/gfoisxtx.html