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

3,383 阅读8分钟

背景

我的上一篇文章《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构建视图变得越来越方便快捷。