本文为作者原创,未经作者允许不得转载。该文同时发表在腾讯bugly公众号:https://mp.weixin.qq.com/s/AZFrqL9dnlgA6Vt2sVhxIw 及腾讯WeTest公众号:http://wetest.qq.com/lab/view/326.html
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,如下图所示:
image.png
image.png
(command+向左的箭头让模拟器横屏)
在iPhone上,tab上的图标较小,tab bar较小,这样垂直空间可多放置内容。如果有人看不清楚tab bar上的图标或文字,可以通过长按tab bar上的任意item,会将该item显示在HUD上,这样可以清楚的看清icon和text。对tool bar 和 navigation bar同理,长按item也会放大显示。如下图显示:
image.png
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属性;
navigationItem.largeTitleDisplayMode
typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode) {
/// 自动模式依赖上一个 item 的特性
UINavigationItemLargeTitleDisplayModeAutomatic,
/// 针对当前 item 总是启用大标题特性
UINavigationItemLargeTitleDisplayModeAlways,
/// Never
UINavigationItemLargeTitleDisplayModeNever,
}
Navigation 集成 UISearchController
把你的UISearchController赋值给navigationItem,就可以实现将UISearchController集成到Navigation。
navigationItem.searchController //iOS 11 新增属性
navigationItem.hidesSearchBarWhenScrolling //决定滑动的时候是否隐藏搜索框;iOS 11 新增属性
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,可以从以下方面做:
-
UINavigationBar 和 UIToolbar 提供位置
-
开发者则必须提供视图的size,有三种方式:
-
对宽度和高度的约束;
-
实现 intrinsicContentSize;
-
通过约束关联你的子视图;
二. 管理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的左边,可以通过layoutMargin的left属性读出该值。如下图所示:
image.png
还有其他一些更新。自从引入
layout margins,当将一个view添加到viewController时,viewController会修复view的的layoutMargins为UIKit定义的一个值,这些调整对外是封闭的。从iOS11开始,这些不再是一个固定的值,它们实际是最小值,你可以改变view的layoutMargins为任意一个更大的值。而且,viewController新增了一个属性:viewRespectsSystemMinimumLayoutMargins,如果你设置该属性为"false",你就可以改变你的layoutMargins为任意你想设置的值,包括0,如下图所示:
image.png
安全区域(Safe Area)
如下图:照片应用程序
image.png
从iOS 7以来,我们在整个操作系统中都有这些半透明的bars,苹果鼓励我们通过这些bars绘制内容,我们是通过viewController 的edgesForExtendedLayout属性来做这些的。
iOS 7 开始,在
UIViewController中引入的 topLayoutGuide 和 bottomLayoutGuide 在 iOS 11 中被废弃了!取而代之的就是safeArea的概念,safeArea是描述你的视图部分不被任何内容遮挡的方法。 它提供两种方式:safeAreaInsets或safeAreaLayoutGuide来提供给你safeArea的参照值,即 insets 或者 layout guide。 safeArea区域如图所示:
image.png
如果有一个自定义的
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表示的区域:
image.png
image.png
新增的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;
Table Views :在iOS 11中默认启用Self-Sizing
这个应该是UITableView最大的改变。我们知道在iOS8引入Self-Sizing 之后,我们可以通过实现estimatedRowHeight相关的属性来展示动态的内容,实现了estimatedRowHeight属性后,得到的初始contenSize是个估算值,是通过estimatedRowHeight x cell的个数得到的,并不是最终的contenSize,tableView不会一次性计算所有的cell的高度了,只会计算当前屏幕能够显示的cell个数再加上几个,滑动时,tableView不停地得到新的cell,更新自己的contenSize,在滑到最后的时候,会得到正确的contenSize。创建tableView到显示出来的过程中,contentSize的计算过程如下图:
image.png
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
如果目前项目中没有使用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 * cell的个数,如下图:
contentSize.png
Table Views:separatorInset 扩展
iOS 7 引入separatorInset属性,用以设置 cell 的分割线边距,在 iOS 11 中对其进行了扩展。可以通过新增的UITableViewSeparatorInsetReference枚举类型的separatorInsetReference属性来设置separatorInset属性的参照值。
typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {
UITableViewSeparatorInsetFromCellEdges, //默认值,表示separatorInset是从cell的边缘的偏移量
UITableViewSeparatorInsetFromAutomaticInsets //表示separatorInset属性值是从一个insets的偏移量
}
下图清晰的展示了这两种参照值的区别:
separatorInsetReference.png
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:)代理方法:
// 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有两种类型,如果是置顶、已读等按钮就使用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了。见下图对比:
image.png
总结
大概介绍了iOS 11的UI方面的一些更新,大部分内容自己代码测试过了,有些更新确实是很实用,可以适配下iOS 11,有的更新可能会给现有APP造成bug,所以学习下这些内容还是很有必要的。
参考
Updating Your App for iOS 11 - WWDC 2017 - Session 204 - iOS









网友评论
所以,如果你使用navigation bar,组装一些整个push和pop体验,你不会得到searchController的集成、大标题的控制更新和Rubber banding效果,因为这些都是由UINavigationController控制的。
if #available(iOS 11.0, *) {
collectionview.contentInsetAdjustmentBehavior = .never
}
这样就可以了
searchV.backgroundColor = [UIColor clearColor];
_searchController.searchBar.barTintColor = ThemeColors;
_searchController.searchBar.frame = CGRectMake(0, - 5, 300 * ScalW, 40);
[searchV addSubview:_searchController.searchBar];
[_searchController.searchBar sizeToFit];
//去黑线
UIImageView *barImageView = [[[_searchController.searchBar.subviews firstObject] subviews] firstObject];
barImageView.layer.borderColor = ThemeColors.CGColor;
barImageView.layer.borderWidth = 1;
self.navigationItem.titleView = searchV;
点击搜索框,搜索框会变宽超出屏幕,不知道什么原因?
方法
- (CGSize)intrinsicContentSize{
return UILayoutFittingExpandedSize;
}
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;后执行[SelfView.MyTableView setContentOffset:CGPointMake(0, kwidth*(Count-1)) animated:NO] ,ContentOffset偏移量还是不对,相差很大
项目中有调用 UIImageWriteToSavedPhotosAlbum 或者 UIActivityViewController的地方,iOS11为此新增一个权限,需要在info.plist加上 NSPhotoLibraryAddUsageDescription
NSPhotoLibraryUsageDescription (String - iOS) This key lets you describe the reason your app accesses the user’s photo library. When the system prompts the user to allow access, this string is displayed as part of the alert.
Although this keys governs read and write access to the user’s photo library, it’s best to use NSPhotoLibraryAddUsageDescription if your app needs only to add assets to the library and does not need to read any assets.
你可以再看看cocoa keys上面对NSPhotoLibraryAddUsageDescription属性的描述
- (void)layoutSubviews {
[super layoutSubviews];
self.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), 64);
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 = 20;
frame.size.height = self.bounds.size.height - frame.origin.y;
view.frame = frame;
}
}
}
if (@available(iOS 11.0, *)) {
_tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
解决了,加了这句代码解决的,不过不知道会不会引起其他问题。
self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
Table Views:separatorInset 扩展 这个是不是放错图片了