引用
导语: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) {
/// 自动模式依赖上一个 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的左边,可以通过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;
//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。在测试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); // 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的个数,如下图: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, //默认值,表示separatorinset是从cell的边缘的偏移量
uitableviewseparatorinsetfromautomaticinsets //表示separatorinset属性值是从一个insets的偏移量
}
下图清晰的展示了这两种参照值的区别:
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了。见下图对比:
总结
大概介绍了ios 11的ui方面的一些更新,大部分内容都用代码测试过了,有些更新确实是很实用,可以适配下ios 11,有的更新可能会给现有app造成bug,所以学习下这些内容还是很有必要的。
引用
参考文献:
1.
2.
3.