Updating Your App for iOS 11 - P
一、Safe Area
从 iOS 7 开始,我们就在操作系统里提供这样的半透明的栏,并且鼓励你把要显示的内容布局延伸过这些栏,就像下图中照片 App 中做的那样。(注意顶部和底部都带有 Bar,且内容都被 Bar 所覆盖,产生出模糊效果)
image.png
之所以又这样的效果是利用了 UIViewController 的属性 edgesForExtendedLayout,它可以让作为 Container 的ViewControllers 定义这些(translucent bars)栏下View 的大小
edgesForExtendedLayout控制 View 的大小 | 让 translucent bars 覆盖其上之后带有模糊效果 |
---|---|
默认情况下edgesForExtendedLayout 适用于所有的边缘,你可以通过topLayoutGuide 和 bottomLayoutGuide 两个属性来定义悬浮栏的大小。
从 iOS 11开始,系统将取消topLayoutGuide 和 bottomLayoutGuide属性,引入新的布局结构概念,SafeArea。
取消topLayoutGuide 和 bottomLayoutGuide属性 | 新的布局结构概念,SafeArea。 |
---|---|
safeArea是描述你的视图部分不被任何内容遮挡的方法。 它提供两种方式:safeAreaInsets 或 safeAreaLayoutGuide 来提供给你 safeArea 的参照值,这两个属性定义在 UIView 中,它们分别对应 insets 或者 layout guide类型。
例如在你自定义的 ViewController 中添加一些自定义的栏样式 View,此时就需要改变 safeAreaInsets 的值。要想增加或减少safeAreaInsets的值,你可以通过调用 UIViewController 的新属性 additionalSafeAreaInsets (UIEdgeInsets 类型)在对应的位置增加 inset 值进而改变 safeAreaInsets。当你的viewController改变了它的safeAreaInsets值时,有两种方式获取到回调:
UIView.safeAreaInsetsDidChange()
UIViewController.viewSafeAreaInsetsDidChange()
每个 view 都可以改变 safeAreaInsets 的值,包括 UIViewController。
image.png二、Scroll Views
下面例子中的结构是 UIVIewController + UIScrollView 包在
UINavigationController 里面。
以前如果一个 VIewController 中含有 ScrollView的话, 被
NavigationController 包住的这个 ViewController 会自动地调整 ScrollView 的 contentInset 值(增加64)如下
iOS 11之后这个行为已取消,取而代之的是,使用一个新的属性adjustedContentInset代替。而 contentInset 这个属性代表的概念简单明了,单单是内容的区域的 inset,不再与外界布局有关。
UIScrollView 支持自动布局,让scrollView可以根据所添加的sub-view的大小自动处理其可滚动区域的大小。iOS 11下更是添加了一些新的属性来协助开发中更快速的布局,其中包括 frameLayoutGuide 和 contentLayoutGuide 以及 contentInsetAdjustmentBehavior。
- frameLayoutGuide 负责scrollView在屏幕中的大小和位置,也就是你可以约束 scrollView 中的 sub-view 如下图中的 Page 1 labelView。当你滚动时,该 page 1 labelview 是固定不动的。
约束 scrollView 中的 sub-view Page 1 labelView | 当内容滚动后,Page 1 位置不变 |
---|---|
- contentLayoutGuide,你可以约束 sub-view 来控制器 scrollView 中可滚动区域的大小或者让内容随着滚动而移动。
指定 contentLayoutGuide | 发生滚动时 |
---|---|
- contentInsetAdjustmentBehavior属性用来配置adjustedContentInset的行为,该结构体有以下几种类型:
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
UIScrollViewContentInsetAdjustmentAutomatic, // Similar to .scrollableAxes, but for backward compatibility will also adjust the top & bottom contentInset when the scroll view is owned by a view controller with automaticallyAdjustsScrollViewInsets = YES inside a navigation controller, regardless of whether the scroll view is scrollable
UIScrollViewContentInsetAdjustmentScrollableAxes, // Edges for scrollable axes are adjusted (i.e., contentSize.width/height > frame.size.width/height or alwaysBounceHorizontal/Vertical = YES)
UIScrollViewContentInsetAdjustmentNever, // contentInset is not adjusted
UIScrollViewContentInsetAdjustmentAlways, // contentInset is always adjusted by the scroll view's safeAreaInsets
} API_AVAILABLE(ios(11.0),tvos(11.0));
/* Configure the behavior of adjustedContentInset.
Default is UIScrollViewContentInsetAdjustmentAutomatic.
*/
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));
/* When contentInsetAdjustmentBehavior allows, UIScrollView may incorporate
its safeAreaInsets into the adjustedContentInset.
*/
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE(ios(11.0),tvos(11.0));
当adjustedContentInset 值被改变后回调的代理方法有:
/* Also see -[UIScrollView adjustedContentInsetDidChange]
*/
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView API_AVAILABLE(ios(11.0), tvos(11.0));
三、Table Views
- 我们知道在iOS 8引入Self-Sizing 之后,可以通过实现estimatedRowHeight 相关的属性来展示动态的内容,当实现了estimatedRowHeight 属性后,tableview 会得到的初始 contenSize ,这是一个估算值,是通过estimatedRowHeight * cell的个数得到的,并不是最终的 contenSize。
因为有估算的 contentSize 值,所以tableView就不会一次性计算所有的cell的高度了,只会计算当前屏幕能够显示的cell个数再加上几个。
滑动时,tableView 不停地得到新的 cell,更新自己的 contenSize,在滑到最后的时候,会得到正确的 contenSize 。在测试Demo中,创建tableView 到显示出来的过程中,contentSize 的计算过程如下图:
- Self-Sizing
在iOS 11中默认启用 Self-Sizing, 也就是说你 cell、header、footer对应的 estimated heights 默认值都从 iOS 11 之前的0 变为UITableViewAutomaticDimension。
因为默认开启了 Self-Sizing,你在布局 cell 时需要确保内部子控件具备完整约束来让 tableview 自动计算出其需要的大小或者你在对应的 delegate 方法中返回每一个 cell 的真实高度值。同理也需要处理对应 header 和 footer 问题。
如果目前项目中没有使用estimateRowHeight属性,在iOS11的环境下就要注意了,因为开启Self-Sizing之后,tableView是使用estimateRowHeight属性的,这样就会造成contentSize和contentOffset值的变化,如果是有动画是观察这两个属性的变化进行的,就会造成动画的异常,因为在估算行高机制下,contentSize的值是一点点地变化更新的,所有cell显示完后才是最终的contentSize值。因为不会缓存正确的行高,tableView reloadData的时候,会重新计算contentSize,就有可能会引起contentOffset的变化。
如果你想 link 到 iOS 11 而不想使用这个默认开启的新特性(Self-Sizing)的话,你可以取消它,代码如下:
override func viewDidLoad() {
//取消 estimated sizes 功能和 tableview 的 Self-Sizing 功能
tableView.estimatedRowHeight = 0
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0
}
iOS11下,如果没有设置estimateRowHeight的值,也没有设置rowHeight的值,那contentSize计算初始值是 44 * cell的个数,如下图:rowHeight和estimateRowHeight都是默认值UITableViewAutomaticDimension 而rowNum = 15;则初始contentSize = 44 * 15 = 660;
- separatorInset
tableView 的 readable content guide 概念,它是 View 内的一部分,也是内容布局的推荐区域。即使在大屏幕的 iPad 下,在 readable content guide 内布局的内容都能够获得不错的用户阅读体验。
默认情况下 tableview 在 readable content guide 内有一个 separatorInset,它可以影响 cell 的默认分隔线位置 和 在 cell 内 labels 的位置。
separator.left = 0 | separator.left = 30 |
---|---|
可见 separatorInset 是对 readable content view 的 inset 处理。
iOS 11 之后,separatorInset 值影响的是,tableview 边框与屏幕的边缘的间隔大小,当设置左右为0时,效果如下
iPad 横屏下,separatorInset.left = 0 和 separatorInset.right = 0如下是 separatorInset 值使用对别,其中在 iOS 11后添加可设置参照的属性UITableViewSeparatorInsetReference
typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {
UITableViewSeparatorInsetFromCellEdges, //默认值,表示separatorInset是从cell的边缘的偏移量
UITableViewSeparatorInsetFromAutomaticInsets //表示separatorInset属性值是从一个insets的偏移量
}
对比使用如下:
UITableViewSeparatorInsetFromCellEdges | UITableViewSeparatorInsetFromAutomaticInsets |
---|---|
- tableview 与 Safe Area 交互需要注意几点:
- separatorInset 被自动地关联到 safe area insets,因此,默认情况下,tabelview的整个内容区域避免了ViewController安全区域的插入。
- UITableviewCell 和 UITableViewHeaderFooterView的 content view 在安全区域内;因此你应该始终在 content view 中使用add-subviews操作
- 你应该使用带有 content view 的 UITableViewHeaderFooterView类实例作为table headers 和 footers、section headers 和 footers。
- Swipe Actions
- 新的滚动条:带有 time stamps 时码的滚动条
- 实现 full swipe-to-delete 功能
- 添加了又滑功能
测试默认开启Self-Sizing的 iOS 11问题。
问题1:如下代码是运行在 iOS 10下正常,但运行在 iOS 11则在 tabelView 上下有留白问题
//
// ViewController.m
// ios10TabelView
//
// Created by Jacob_Liang on 2017/9/21.
// Copyright © 2017年 Jacob. All rights reserved.
//
#import "ViewController.h"
static NSString * const CELLID = @"CELLID";
@interface ViewController ()<UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, weak) UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setUpInit];
[self setUpNav];
[self setUpTableView];
}
- (void)setUpInit {
self.automaticallyAdjustsScrollViewInsets = NO; //iOS 11下被废弃了,写了也没用
self.view.backgroundColor = [UIColor purpleColor];
}
- (void)setUpNav {
self.navigationItem.title = @"出席统计";
}
- (void)setUpTableView {
CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
CGFloat screenH = [UIScreen mainScreen].bounds.size.height;
UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, screenW, screenH - 64) style:UITableViewStyleGrouped];
[self.view addSubview:tableView];
_tableView = tableView;
tableView.backgroundColor = [UIColor lightGrayColor];
tableView.delegate = self;
tableView.dataSource = self;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CELLID];
}
#pragma mark - UITableViewDelegate & UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 15;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CELLID forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"%@",indexPath];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 50;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 0.01;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return 0.01;
}
@end
上述代码运行情况,注意此时 tableview 的 Style 为 UITableViewStyleGrouped
在 iOS 10 下 | iOS 11下 | iOS 11下 |
---|---|---|
self.automaticallyAdjustsScrollViewInsets = NO 有效 | self.automaticallyAdjustsScrollViewInsets = NO 无效 | self.automaticallyAdjustsScrollViewInsets = NO 无效 |
没有调用viewForFooterInSection和viewForHeaderInSection运行正常 | 没有调用viewForFooterInSection和viewForHeaderInSection运行有留白 | 调用viewForFooterInSection和viewForHeaderInSection运行正常 |
iOS10NOReturnViewFooterHeader.gif | NOReturnHeaderOrFooterViewQuestion.gif | wihtReturnHeaderOrFooterView.gif |
另一宗办法就是,关闭 iOS 11默认打开的 Self-Sizing 功能
tableView.estimatedRowHeight = 0;
tableView.estimatedSectionFooterHeight = 0;
tableView.estimatedSectionHeaderHeight = 0;