iOS14 的 Modern cell configuration 给我们带来了什么?

本贴最后更新于 1511 天前,其中的信息可能已经斗转星移

背景

我的上一篇文章《UITableView 和 UICollectionView 的新渲染方式 DiffableDataSource》中提到了,在 iOS13 中推出了 DiffableDataSource 代替了使用了将近 10 年之久的那几个渲染单元格视图的代理方法。同样的,随着 iOS14 的发布,苹果推出了全新的视图的外观和内容的设置方法 Configuration,用以取代也已经使用了 10 多年的直接操作单元格元素属性的方法。

最近项目开始逐步适配 iOS14。将项目跑在 iOS14 的环境中会发现,以前项目中设置了圆角的 tableViewCell 在 iOS14 环境下,圆角全部失效了。让我们来探究一下,iOS14 在 cell 的内容和样式的配置部分给我们带来了什么样的新特性。

现状

在 iOS13 以及 13 以前,通常我们会像下面这样配置一个 tableView 的 cell

guard let cell = tableView.dequeueReusableCell(withIdentifier: identifier) else {
    return UITableViewCell()
}
cell.imageView?.image = UIImage(systemName: "imageName")
cell.textLabel?.text = "text"
cell.layer.cornerRadius = 9.0
return cell

这个大家应该非常熟悉了,但是,当中的 cornerRadius 的设置,在 iOS14 中是无效的。那么在 iOS14 中,如果想要给 cell 设置圆角,应该怎么做呢?

Configurations

新的配置 API

在 iOS14 中,引入了一个新的概念叫 Configuration,内容有内容的 Configuration,样式有样式的 Configuration。系统提供了两种配置:BackgroundConfiguration 和 ContentConfiguration。

  • BackgroundConfiguration
    如上图所示,BackgroundConfiguration 包含了一些列与 Background 有关的属性,例如:backgroundColor、cornerRadius 等等。
  • ContentConfiguration
    如上图所示,ListContentConfiguration 包含了一些列与列表相关的属性,例如:Image、Text,可选的辅助文本等等。

如果想要在 iOS14 中实现和上面代码相同的效果,可以这样写:

var content = cell.defaultContentConfiguration()
content.image = UIImage(systemName: "imageName")
content.text = "text"
var backgroundConfig = cell.backgroundConfiguration
backgroundConfig?.cornerRadius = 9.0

return cell

如上代码中这样通过设置 cell 的 backgroundConfiguration 的 cornerRadius 属性,来实现 cell 的圆角效果。如果是自定义的 cell,你可以这样写:

override func updateConfiguration(using state: UICellConfigurationState) {
    var configuration = UIBackgroundConfiguration.listPlainCell()
    configuration.cornerRadius = 9.0
    backgroundConfiguration = configuration
}

通过重写自定义 cell 里的 updateConfiguration 方法来添加圆角属性。
这里面的 UIBackgroundConfiguration 结构体通过不同的构造方法返回不同样式 cell 的 BackgroundConfiguration,所以需要我们根据 cell 的样式来决定采用哪个构造方法来生成对应 cell 的 BackgroundConfiguration。

public struct UIBackgroundConfiguration : Hashable {

    /// Returns a clear configuration, with no default styling.
    public static func clear() -> UIBackgroundConfiguration

    /// Returns the default configuration for a plain list cell.
    public static func listPlainCell() -> UIBackgroundConfiguration

    /// Returns the default configuration for a plain list header or footer.
    public static func listPlainHeaderFooter() -> UIBackgroundConfiguration

    /// Returns the default configuration for a grouped list cell.
    public static func listGroupedCell() -> UIBackgroundConfiguration

    /// Returns the default configuration for a grouped list header or footer.
    public static func listGroupedHeaderFooter() -> UIBackgroundConfiguration

    /// Returns the default configuration for a sidebar list header.
    public static func listSidebarHeader() -> UIBackgroundConfiguration

    /// Returns the default configuration for a sidebar list cell.
    public static func listSidebarCell() -> UIBackgroundConfiguration

    /// Returns the default configuration for an accompanied sidebar list cell.
    public static func listAccompaniedSidebarCell() -> UIBackgroundConfiguration

    ...

通过注释,我们可以看到每个构造方法对应的 cell 或 view 的样式。照着选就行。

新的配置的优点

通过上面的代码,我们了解到,通过给单元格或视图设置配置属性的方法能够实现和 iOS14 之前通过直接操作单元格或视图上的元素获得一样的渲染效果。从设计模式的角度来说,符合单一职责原则。通过一个 Configuration 来管理之前散落在代码各个角落的内容和背景样式。然后,通过 Configuration 来和视图元素本身进行交互,隐藏了不必要的细节。同时,独立出来的 Configuration 不再依附于任何视图元素,它和视图元素之间是可以组合的。一个 Configuration 可以适用于所有支持内容配置或背景配置的视图,即使这个视图不是单元格,比如表视图的页眉和页脚。
另外,Configuration 是值类型,在你将 Configuration 设置到对应的视图上之前,你创建或修改的任何 Configuration 实例都不会影响到其他 Configuration 的内容,不需要关心之前已经有的配置。而且在创建 Configuration 的时候所需要的系统资源是很轻量级的,所以 Apple 官方鼓励使用者 always start with a fresh configuration,鼓励大家始终从全新的配置开始。

Configurations state

默认更新配置

Configurations state 用于配置单元格和视图的各种输入,如下图,表格视图中的页眉和单元格当中都有自己对应的 Configurations state 类型。

而这些 state 状态包含以下几种状态:

你可以通过 updating configurations 来更新这些状态,如下图所示:

当我们在更新配置的时候,原始配置是不会更改的,所以你在原始配置上设置的属性是不会变动的,直到你用新的属性值来代替它。
系统提供两个属性来控制当配置更改的时候是否能够自动应用新的配置,来为每个状态获取默认样式。它们分别是 automaticallyUpdatesContentConfigurationautomaticallyUpdatesBackgroundConfiguration,是默认开启的。

/// When YES, the cell will automatically call -updatedConfigurationForState: on its `contentConfiguration` when the cell's
/// configuration state changes, and apply the updated configuration back to the cell. The default value is YES.
@available(iOS 14.0, *)
open var automaticallyUpdatesContentConfiguration: Bool

/// When YES, the cell will automatically call -updatedConfigurationForState: on its `backgroundConfiguration` when the cell's
/// configuration state changes, and apply the updated configuration back to the cell. The default value is YES.
@available(iOS 14.0, *)
open var automaticallyUpdatesBackgroundConfiguration: Bool

如果你想要针对不同的状态进行外观的自定义,你可以关掉上面对应的属性,自己更新相应的配置。

自己更新配置

自己如果想更新配置的话,拿 cell 举例,可以重写 cell 在 iOS14 开始新推出的 @objc(_bridgedUpdateConfigurationUsingState:) dynamic open func updateConfiguration(using state: UICellConfigurationState) 方法,将想要更新的配置根据 cell 的不同状态写在重写的方法里就行。比如设置 cell 不同状态下的背景颜色。示例写法如下:

override func updateConfiguration(using state: UICellConfigurationState) {
    var configuration = UIBackgroundConfiguration.listPlainCell()
    if state.isHighlighted || state.isSelected {
        configuration.backgroundColor = .yellow
    }
    configuration.cornerRadius = 9.0
    backgroundConfiguration = configuration
}

updateConfiguration(using state:) 这个方法会在 cell 的首次展示之前调用,并且在 cell 的配置状态发生变化时再次调用。这里还是一样不需要关心旧的配置,每次只需要获取一个新的配置,设置好属性并将其应用到 cell 上就行了。如果想要手动重新配置一次 cell,则只需要手动调用 setNeedsUpdateConfiguration() 即可。

颜色转换器

上面提到了根据 cell 的不同状态设置背景色,iOS14 后推出了一种新的设置颜色的方法:使用一种叫 UIConfigurationColorTransformer 的新类型,即颜色转换器。颜色转换器工作流程如下图所示:

颜色转换器接受一种颜色并通过某种方式修改原始颜色,返回不同的颜色。某些状态下的某些配置具有预设的颜色转换器,用来为该状态生成特定的外观。
使用示例如下:

override func updateConfiguration(using state: UICellConfigurationState) {
    var background = UIBackgroundConfiguration.clear()
    background.cornerRadius = 10
    if state.isHighlighted || state.isSelected {
        // Set nil to use the inherited tint color of the cell when highlighted or selected
        background.backgroundColor = nil
      
        if state.isHighlighted {
            // Reduce the alpha of the tint color to 30% when highlighted
            background.backgroundColorTransformer = .init { $0.withAlphaComponent(0.3) }
        }
    }
    backgroundConfiguration = background
  
    var content = self.defaultContentConfiguration().updated(for: state)
    content.image = image
    contentConfiguration = content
}

Background & Content Configurations 的一些细节

  • 集合视图列表单元格、表单视图的单元格和表视图页眉和页脚都会根据包含列表或表视图的样式自动设置默认背景配置。因此,通常情况下,你无需执行任何操作即可获得所需的背景外观。
  • 内容配置的工作方式与默认情况下自动应用内容的单元格不同,你可以对单元格使用 defaultContentConfiguration 方法,来根据单元格的样式获取新的配置。就像在前面示例中看到的一样。但是对于背景和内容配置,可以直接通过 UIBackgroundConfigurationUIListContentConfiguration 来请求任何样式的默认配置。对于其他不同样式的单元格、页眉和页脚也有类似的方法。
  • 我们应该使用内容配置里面的相关属性来调整整个 cell 的布局,并且让 cell 使用动态高度调整,而不是给 cell 指定固定高度。
  • iOS14 之前原有的某些属性与新推出的配置是互斥的。设置背景配置始终会将背景颜色和背景视图属性重置为零。反过来也是一样的。所以请确保不要将背景配置与仍在同一单元格上设置这些其他背景属性的其他代码混合使用。特别是在使用 UITableView 时,内容配置将取代单元格、页眉和页脚的内置子视图。如 imageViewtextLabel 等,这些旧的内容属性将在未来的版本中弃用。

总结

通过配置,我们可以专注于要显示的内容,而不必关心如何更新视图。我们只需要每次都从一个新的配置开始,按照我们想要的方式设置这个配置里的属性,然后应用到单元格上,UIKit 会自动而高效的帮我们处理完剩下的工作。从大的方向来看,苹果不断的在优化其 UIKit 的架构,朝着高内聚低耦合的方向上进化,让我们使用 UIKit 构建视图变得越来越方便快捷。

  • iOS

    iOS 是由苹果公司开发的移动操作系统,最早于 2007 年 1 月 9 日的 Macworld 大会上公布这个系统,最初是设计给 iPhone 使用的,后来陆续套用到 iPod touch、iPad 以及 Apple TV 等产品上。iOS 与苹果的 Mac OS X 操作系统一样,属于类 Unix 的商业操作系统。

    85 引用 • 139 回帖
  • UIKit
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...