导语:iOS 11 为整个生态系统的 UI 元素带来了一种更加大胆、动态的新风格。 本文介绍iOS11中在UI方面做了哪些更新,有些更新可以为用户提供更加完美的体验,但也有的可能会给目前的APP带来异常bug
前言
前几天发现在做的APP在 iOS11 系统上动画有异常,在其他系统的设备上都是正常的,动画的操作是观察
tableView
的
contentOffset
变化后执行的,异常动画发生在
tableView reloadData
之后,也就是说
tableView reloadData
之后,
tableView
的
contentOffset
发生了几次变化。查了下资料发现原因是 iOS11 中默认开启了
Self-Sizing
,在WWDC 2017 session204 Updating Your App for iOS 11 中有介绍,因此研究了下这个session,本文作为一个总结,下文的第三部分会有对上述的动画异常的原因分析及解决方式。
本文内容包括
:集成了搜索的大标题栏、横向选项卡栏、Margins 和 Insets以及
UIScrollView
和
UITableView
的更新和功能更强大的滑动操作。
一. 在UIKit’s Bars中加入的新功能
WWDC通过iOS新增的文件管理App:Files开始介绍,在Files这个APP中能够看到iOS11中UIKit’s Bars的一些新特性:在浏览功能上的大标题视图(向上滑动后标题会回到原来的UI效果)、横屏状态下tab上的文字和icon会变为左右排列。我用iOS11的模拟器体验了一下Files这个APP的竖屏和横屏,如下图所示:
(command+向左的箭头让模拟器横屏)
横屏时,在iPhone上,tab上的图标较小,tab bar较小,这样垂直空间可多放置内容。如果有人看不清楚tab bar上的图标或文字,可以通过长按tab bar上的任意item,会将该item显示在HUD上,这样可以清楚的看清icon和text。对tool bar 和 navigation bar同理,长按item也会放大显示。如下图显示:
UIBarItem
UIBarItem是UI tab bar item和UI bar button item的父类,要想实现上面介绍的效果,只需要为UIBarItem 设置
landscapeImagePhone
属性,在storyboard中也支持这个设置,对于HUD的image需要设置另一个iOS11新增的属性:
largeContentSizeImage
,关于这部分更详细的讨论,可以参考 WWDC2017 Session 215:What’s New in Accessibility
控制大标题的显示
在UI navigation bar中新增了一个BOOL属性
prefersLargeTitles
,将该属性设置为ture,
navigation bar
就会在整个APP中显示大标题,如果想要在控制不同页面大标题的显示,可以通过设置当前页面的
navigationItem
的
largeTitleDisplayMode
属性;
typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode) {
UINavigationItemLargeTitleDisplayModeAutomatic,
UINavigationItemLargeTitleDisplayModeAlways,
UINavigationItemLargeTitleDisplayModeNever,
}
Navigation 集成 UISearchController
把你的
UISearchController
赋值给
navigationItem
,就可以实现将
UISearchController
集成到
Navigation
。
navigationItem.searchController
navigationItem.hidesSearchBarWhenScrolling
UINavigationController和滚动交互
滚动的时候,以下交互操作都是由
UINavigationController
负责调动的:
-
UIsearchController搜索框效果更新
-
大标题效果的控制
-
Rubber banding效果 //当你开始往下拉,大标题会变大来回应那个滚轮
所以,如果你使用
navigation bar
,组装push和pop体验,你不会得到
searchController
的集成、大标题的控制更新和
Rubber banding
效果,因为这些都是由
UINavigationController
控制的。
UIToolbar and UINavigationBar— Layout
在 iOS 11 中,当苹果进行所有这些新特性时,也进行了其他的优化,针对 UIToolbar 和 UINavigaBar 做了新的自动布局扩展支持,自定义的bar button items、自定义的title都可以通过layout来表示尺寸。 需要注意的是,你的
constraints
需要在view内部设置,所以如果你有一个自定义的标题视图,你需要确保任何约束只依赖于标题视图及其任何子视图。当你使用自动布局,系统假设你知道你在做什么。
Avoiding Zero-Sized Custom Views
自定义视图的size为0是因为你有一些模糊的约束布局。要避免视图尺寸为0,可以从以下方面做:
二. 管理margins 和 insets
layout margins
基于约束的Auto Layout,使我们搭建能够动态响应内部和外部变化的用户界面。Auto Layout为每一个view都定义了
margin
。
margin
指的是控件显示内容部分的边缘和控件边缘的距离。 可以用
layoutMargins
或者
layoutMarginsGuide
属性获得view的
margin
,
margin
是视图内部的一部分。
layoutMargins
允许获取或者设置
UIEdgeInsets
结构的
margin
。
layoutMarginsGuide
则获取到只读的
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));
layoutMargins
是
UIEdgeInsets
结构体类型的属性:
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的左边,可以通过layout margins的left属性读出该值。如下图所示:
还有其他一些更新。自从引入layout margins,当将一个view添加到
viewController
时,
viewController
会修复view的
layoutMargins
为UIKit定义的一个值,这些调整对外是封闭的。从iOS11开始,这些不再是一个固定的值,它们实际是最小值,你可以改变你的view的
layoutMargins
为任意一个更大的值。而且,
viewController
新增了一个属性:
viewRespectsSystemMinimumLayoutMargins
,如果你设置该属性为”false”,你就可以改变你的layout margins为任意你想设置的值,包括0,如下图所示:
安全区域(Safe Area)
如图:照片应用程序
从iOS 7以来,我们在整个操作系统中都有这些半透明的bars,苹果鼓励我们通过这些bars绘制内容,我们是通过viewController 的edgesForExtendedLayout属性来做这些的。
iOS 7 开始,在
UIViewController
中引入的
topLayoutGuide
和
bottomLayoutGuide
在 iOS 11 中被废弃了,取而代之的就是
safeArea
的概念,
safeArea
是描述你的视图部分不被任何内容遮挡的方法。 它提供两种方式:
safeAreaInsets
或
safeAreaLayoutGuide
来提供给你
safeArea
的参照值,即 insets 或者 layout guide。 safeArea区域如下图所示:
如果有一个自定义的
viewController
,你可能要添加你自己的bars,增加
safeAreaInsets
的值,可以通过一个新的属性:
addtionalSafeAreaInsets
来改变
safeAreaInsets
的值,当你的
viewController
改变了它的
safeAreaInsets
值时,有两种方式获取到回调:
UIView.safeAreaInsetsDidChange()
UIViewController.viewSafeAreaInsetsDidChange()
三. UIScrollView and UITableView的新特性
Scroll Views
如果有一些文本位于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;
- (void)adjustedContentInsetDidChange;
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView;
Table Views :在iOS 11中默认启用Self-Sizing
这个应该是UITableView最大的改变。我们知道在iOS8引入Self-Sizing 之后,我们可以通过实现
estimatedRowHeight
相关的属性来展示动态的内容,实现了
estimatedRowHeight
属性后,得到的初始contenSize是个估算值,是通过
estimatedRowHeight
x
cell
的个数得到的,并不是最终的
contenSize
,
tableView
就不会一次性计算所有的
cell
的高度了,只会计算当前屏幕能够显示的cell个数再加上几个,滑动时,
tableView
不停地得到新的cell,更新自己的contenSize,在滑到最后的时候,会得到正确的contenSize。在测试Demo中,创建tableView到显示出来的过程中,contentSize的计算过程如下图:
Self-Sizing
在iOS11下是默认开启的,Headers, footers, and cells都默认开启
Self-Sizing
,所有estimated 高度默认值从iOS11之前的 0 改变为
UITableViewAutomaticDimension
:
@property (nonatomic) CGFloat estimatedRowHeight NS_AVAILABLE_IOS(7_0);
如果目前项目中没有使用estimateRowHeight属性,在iOS11的环境下就要注意了,因为开启
Self-Sizing
之后,tableView是使用
estimateRowHeight
属性的,这样就会造成contentSize和contentOffset值的变化,如果是有动画是观察这两个属性的变化进行的,就会造成动画的异常,因为在估算行高机制下,contentSize的值是一点点地变化更新的,所有cell显示完后才是最终的contentSize值。因为不会缓存正确的行高,tableView reloadData的时候,会重新计算contentSize,就有可能会引起contentOffset的变化。
iOS11下不想使用
Self-Sizing
的话,可以通过以下方式关闭:(前言中提到的问题也是通过这种方式解决的)
self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
iOS11下,如果没有设置
estimateRowHeight
的值,也没有设置rowHeight的值,那contentSize计算初始值是 44 x cell的个数,如下图:rowHeight和estimateRowHeight都是默认值UITableViewAutomaticDimension 而rowNum = 15;则初始contentSize = 44 15 = 660;
Table Views:separatorInset 扩展
iOS 7 引入
separatorInset
属性,用以设置 cell 的分割线边距,在 iOS 11 中对其进行了扩展。可以通过新增的
UITableViewSeparatorInsetReference
枚举类型的
separatorInsetReference
属性来设置
separatorInset
属性的参照值。
typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {
UITableViewSeparatorInsetFromCellEdges,
UITableViewSeparatorInsetFromAutomaticInsets
}
下图清晰的展示了这两种参照值的区别:
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。
滑动操作(Swipe Actions)
在iOS8之后,苹果官方增加了UITableVIew的右滑操作接口,即新增了一个代理方法(tableView: editActionsForRowAtIndexPath:)和一个类(UITableViewRowAction),代理方法返回的是一个数组,我们可以在这个代理方法中定义所需要的操作按钮(删除、置顶等),这些按钮的类就是
UITableViewRowAction
。这个类只能定义按钮的显示文字、背景色、和按钮事件。并且返回数组的第一个元素在UITableViewCell的最右侧显示,最后一个元素在最左侧显示。从iOS 11开始有了一些改变,
首先是可以给这些按钮添加图片了,然后是如果实现了以下两个iOS 11新增的代理方法,将会取代(tableView: editActionsForRowAtIndexPath:)代理方法:
- (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了。见下图对比:
总结
大概介绍了iOS 11的UI方面的一些更新,大部分内容都用代码测试过了,有些更新确实是很实用,可以适配下iOS 11,有的更新可能会给现有APP造成bug,所以学习下这些内容还是很有必要的。
参考文献:
-
Updating Your App for iOS 11 - WWDC 2017 - Session 204 - iOS
-
Mysteries of Auto Layout, Part 1 - WWDC2015
-
Mysteries of Auto Layout, Part 2 - WWDC2015
阅读原文