从 iOS 15 适配回顾 WWDC 2021

雪球财经

图片

作者:徐少华

引言

iOS 15 正式推送已经有一段时间,雪球 iOS 团队在适配新系统的过程中解决了一些问题。 在其中某些问题产生原因的探究过程中,我们发现了有趣的事情:

图片

下文针对每个问题,结合 WWDC 2021 上的内容进行了回顾和整理,并给出问题解决方案,分享给大家。
以下讨论内容使用环境为 Xcode 13 & iOS 15。

UINavigationBar & UITabbar 显示异常

图片

UITabBar & UINavigationBar异常

如上图所示,本来是白色的 tabBar 和 navigationBar 背景颜色变成了半透明灰色。

scrollEdgeAppearance

让我们来到WWDC 21 “What's new in UIKit” 章节,其中提到了 UIToolBar & UITabBar 改进:

图片

UIToolBar & UITabBar 改进

iOS 15 中页面滑动到底部时会移除 UIToolbar & UITabBar 背景颜色为用户的内容提供无缝衔接样式来增强视觉体验。
在 iOS 15 中 UITabBar  增加了 scrollEdgeAppearance 属性来支持这一特性:

/// Describes the appearance attributes for the tabBar to use when an observable scroll view is scrolled to the bottom. If not set, standardAppearance will be used instead.
@property (nonatomic, readwrite, copy, nullable) UITabBarAppearance *scrollEdgeAppearance UI_APPEARANCE_SELECTOR API_AVAILABLE(ios(15.0));

官方文档中的解释:

When a tab bar controller contains a tab bar and a scroll view, part of the scroll view’s content appears underneath the tab bar. If the edge of the scrolled content reaches that bar, UIKit applies the appearance settings in this property. If the value of this property is nil, UIKit uses the value of the tab bar’s standardAppearance property, modified to have a transparent background.

当 UITabBarController 中包含 UITabBar 和 滑动视图时, 滑动视图的部分内容会出现在 UITabBar 下面,当滑动视图边缘滑动到 UITabBar 的时候,UIKit 将应用此属性来设置 UITabBar 外观。 如果此属性的值为 nil,则 UIKit 使用 UITabBar 的 standardAppearance 属性的值,并修改为透明背景。

UINavigationBar 在 iOS 13 就已经增加了相似属性 :

/// Describes the appearance attributes for the navigation bar to use when an associated UIScrollView has reached the edge abutting the bar (the top edge for the navigation bar). If not set, a modified standardAppearance will be used instead.
@property (nonatomic, readwrite, copy, nullable) UINavigationBarAppearance *scrollEdgeAppearance UI_APPEARANCE_SELECTOR API_AVAILABLE(ios(13.0));

官方文档中的解释:

When a navigation controller contains a navigation bar and a scroll view, part of the scroll view’s content appears underneath the navigation bar. If the edge of the scrolled content reaches that bar, UIKit applies the appearance settings in this property.If the value of this property is nil, UIKit uses the settings found in the standardAppearance property, modified to use a transparent background. If no navigation controller manages your navigation bar, UIKit ignores this property and uses the standard appearance of the navigation bar.When running on apps that use iOS 14 or earlier, this property applies to navigation bars with large titles. In iOS 15, this property applies to all navigation bars.

表达内容和 UITabBar 的 scrollEdgeAppearance 一致。只不过后面补了一句:在 iOS 15 之前的版本只对 LargeTitles 的导航栏生效,iOS 15 之后对所有导航栏生效。 所以这个也是为什么 UINavigationBar 也跟着变化了的原因。

下面通过 demo 更好的理解下 UITabBar & UINavigationBar 的 standardAppearance 及 scrollEdgeAppearance 效果:

图片

当列表头部或者底部边缘达到 UINavigationBar 或者 UITabBar 时,会使用他们不同的 Appearance 来设置样式。

无缝衔接视觉效果是一种什么感受,下面使用 Shortcuts 演示,左图为 iOS 15,请注意观察底部 UITarBar 变化:

图片

Shortcuts 对比

iOS 15 的页面中列表的边缘如果滑动到了 UITabBar 上方,UINavigationBar、中间显示内容、UITabBar 三者在视觉效果中没有了明显的分隔,更加一体化。

以上就是 “What's new in UIKit” 章节中提到了 UIToolBar & UITabBar 改进内容。

解决方法

UITabBar & UINaviationBar 样式问题原因分析完后,我们来解决本节的问题。 解决方式就是去设置一下对应的 scrollEdgeAppearance,也可以指定被观察的 scrollView 来避免系统监听到错误的对象滑动,下面是官方的举例代码:

// Custom scrollEdgeAppearance
 
let appearance = UITabBarAppearance()
appearance.backgroundEffect = nil
appearance.backgroundColor = .blue
 
tabBar.scrollEdgeAppearance = appearance
 
// Specify the content scrollView
let scrollView = ... // Content scroll view in your app
viewController.setContentScrollView(scrollView, for: .bottom)

我们的预期是 standardAppearance 和 scrollEdgeAppearance 状态下 UI 表现一致,所以实际的修改就是把这两个属性设置成一样就可以了。

UITableView section header 默认高度增加

图片

现象为项目中 sectionHeader 的样式都变高了。

sectionHeaderTopPadding

iOS 15 中 UITableview 增加了 sectionHeaderTopPadding 属性,为每个 section 增加了默认值为 UITableViewAutomaticDimension 高度:

/// Padding above each section header. The default value is `UITableViewAutomaticDimension`.
@property (nonatomic) CGFloat sectionHeaderTopPadding API_AVAILABLE(ios(15.0), tvos(15.0), watchos(8.0));

再次回到 WWDC21 “What's new in UIKit” 章节,其中描述到:

We have a new appearance for headers in iOS 15. For plain lists, section headers now display seamlessly in line with the content, and only display a visible background material when becoming pinned to the top as you scroll down. In addition, there's new padding inserted above each section header to visually separate the sections with this new design.

在 iOS 15 中,plain 列表有一个新的标题外观:section headers 跟内容无缝显示,并且仅在向上滑动固定到顶部时才显示特别的背景颜色。 此外在每个 section header 上方还插入了新的填充,用这种新设计在视觉上分隔各个 section。 这个新的填充就是 sectionHeaderTopPadding ,这也就是问题产生的原因了。

解决方法

解决方式很简单,我们不想要这个被增加的高度,全局设置一下问题解决:

if (@available(iOS 15.0, *)) {
    [[UITableView appearance] setSectionHeaderTopPadding:CGFLOAT_MIN];
}

这种新的标题外观和无缝衔接式体验可以在 iOS 15 通讯录应用体验到,下图为 iOS 15 和 iOS 14的对比图:

图片

通讯录对比

结合上文说到的 UINavigationBar,UITabBar 的特性,无缝衔接的页面体验是被苹果妥妥的安排上了。

UITableView 滑动内容闪动

图片图片

现象为 UITableView 滑动的时候 feed 流内容闪动,自选股列表涨跌幅色块闪动。

Cell prefetching

让我们来到 WWDC 21  “Make blazing fast lists and collection views”章节,本章主题是构建顺滑列表和集合视图体验。 其中部分内容是介绍自动单元格预取(Cell prefetching)是如何优化列表滑动体验的。

图片

上图是描述了列表滑动卡顿产生的原因:在滑动列表时,如果需要一个新的 cell 显示,该 cell 所有内容会在当前帧的 deadline 前处理好(第一帧提交的阶段,也就是第一格), 用户继续浏览,如果没有新 cell 入屏,之后每一帧的提交内容非常少,远在当前帧 deadline 之前就已经计算完毕(第二格,第三格)。 当遇到一个计算量较大的 Expensive cell 需要显示的时候,内容提交已经超过了第四帧的 deadline,这时就产生了 Commit hitch,也就是卡顿现象。

当使用了 Cell prefetching 技术后,卡顿问题得到了优化:

图片

一般而言,不是每一帧都需要花费很长的时间计算新内容,有两个图帧的提交时间很短(第二和第三帧),计算量相当少,远在它们对应的 deadline 之前完成。iOS 15 的 Cell prefetching 正是利用这多出来的时间,根据系统推断,在第二帧短暂的提交结束之后立马开始准备下一个 Expensive cell,虽然也占用了第三帧的部分时间,但是不影响第三帧在它的 commit deadline 前正常结束提交。 当要显示这个 Expensive cell 的时候就不会占用第四帧的大量时间计算,直接 Use prefetched cell ,避免了 commit hitch 。

iOS 15 为每个 UITableView 都自动开启了 Cell prefetching 特性,并新增了 prefetchingEnabled 选项来让开发者决定是否使用此功能:

@property (nonatomic, getter=isPrefetchingEnabled) BOOL prefetchingEnabled API_AVAILABLE(ios(15.0), tvos(15.0), watchos(8.0));

问题分析

prefetchingEnabled 开启与否还有一个很明显的差别,当列表需要从 cell 复用池拿 cell 的时候不再是之前预期的那样按顺序取了。 下面 demo 中为每个新 cell 初始化赋值了从0开始唯一编号,当从复用池取出的时候我们可以看出 cell 的取用顺序,下面是在 iOS 15 中 prefetchingEnabled 默认开启的情况下(下文统称为 A 条件):

图片

prefetchingEnabled 开启

每次点击刷新调用 tableView reloadData  后,对应 row 从 cell 复用池中取出的 cell 是不一样的了。

当在 iOS 15 关闭 prefetchingEnabled,或在 iOS 15 以下版本中(下文统称为 B 条件):

图片

prefetchingEnabled 关闭

每次 tableView reloadData 前后,从 cell 复用池中取出的 cell 是有序的,所有正在显示的 cell 编号没有变化,刷新前后对应 row 中使用的 cell 是一致的。

解决方法

分析完 Cell prefetching 的一些特点后,我们回到本章节开头的两个问题中:

feed 流内容闪动
首先 feed 流卡片我们使用了异步绘制来优化滑动体验;由于业务逻辑需要,feed 流下拉刷新后,tableView 会 reload 不止一次,推断问题原因如下:
在 A 条件中,因为每次 reload 前后使用的 cell 不再是一个了, 取出的新 cell 可能已经被 prefetching 机制处理完成的,这时新 cell 需要清除当前内容,并重新异步绘制新内容,从而导致页面内容闪动。
在 B 条件中,如果 reload 前后卡片内容无变化,每个 cell 显示的内容在多次 reload 前后是一致的,并且 cell 不会被预处理,所以感受不到页面刷新和内容闪动; 当我们关闭 cell 异步绘制或者关闭 UITableView 的 prefetchingEnabled 功能时,cell 闪动消失了,推断得到验证。 目前修复问题的方式是关闭了对应 UITableView 的 prefetchingEnabled 功能,采用我们原有的异步绘制方式。

自选股列表涨跌幅色块闪动
自选股列表 cell 没有使用异步绘制。
涨跌幅色块使用 CALayer 展示,当自选股列表 reload 时,reload 前后的 cell 如果不是同一个,或者拿到了被 prefetching 的 cell, 代码中对 CALayer 背景颜色被重新赋值时,没有关闭 CALayer 隐式动画,所以导致了色块颜色变更时候的闪动。
解决这个问题的方案:在对 CALayer 设置颜色时候,关闭隐式动画:

[CATransaction begin];
[CATransaction setDisableActions:YES];       
layer.backgroundColor = bgColor.CGColor;         
[CATransaction commit];

One more little thing

需要注意的是 cellForRowAtIndexPath 方法的返回值也有变更:

Return Value:The cell object at the corresponding index path.In versions of iOS earlier than iOS 15, this method returns nil if the cell isn’t visible or if indexPath is out of range. In iOS 15 and later, this method returns a non-nil cell if the table view retains a prepared cell at the specified index path, even if the cell isn’t currently visible.

在 iOS 15 之前的 iOS 版本中,如果 cell 不可见或 indexPath 超出范围,则此方法返回 nil。 在 iOS 15 及更高版本中,如果 tableView 在指定的 indexPath 下有已经 prefetching 的 cell,即使该 cell 当前不可见,此方法也会返回一个非 nil 的cell 。

ProMotion displays

最后要说的不是一个问题,而是对高刷特性的支持。
iPhone 13 Pro、iPhone 13 Pro Max 和 iPad ProMotion 显示器能够在以下各项之间动态切换:

  • 刷新率高达 120Hz

  • 低至 24Hz 或 10Hz 的较慢刷新率

这种动态切换机制意味着 APP 可以使用高刷新率呈现更流畅、顺滑的体验,并在可能的情况中使用较低刷新率节省电量消耗。 如果要在 iPhone 13 Pro、iPhone 13 Pro Max 中支持更高的刷新率,请将以下键添加到您的 Info.plist 文件:

<key>CADisableMinimumFrameDurationOnPhone</key><true/>

总结

以上就是雪球 iOS 团队在适配 iOS 15 过程中解决的一些有代表性的问题,并结合 WWDC 21 中的相关内容进行了回顾。 希望对正在使用 Xcode 13 适配项目的同学提供帮助。

参考资料

What's new in UIKit:

developer.apple.com/videos/play…

Make blazing fast lists and collection views:

developer.apple.com/videos/play…

Optimizing ProMotion Refresh Rates for iPhone 13 Pro and iPad Pro:

developer.apple.com/documentati…

还有一件事

雪球业务正在突飞猛进的发展,工程师团队期待牛人的加入。如果你对「做中国人首选的在线财富管理平台」感兴趣,希望你能一起来添砖加瓦,点击「阅读原文」查看热招职位,就等你了。

热招岗位:Android/iOS/FE 开发工程师、Java 开发工程师、测试工程师、运维工程师。

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改