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