一般如何使用iOS 14个单元格内容配置?

how to use iOS 14 cell content configurations in general?

WWDC 2020“现代蜂窝配置”我看了又看,但启蒙不罢休。

我了解内置的 ListContentConfiguration 具有类似于 内置单元格样式 组件的属性,例如文本标签和图像视图。但我从不使用内置的单元格样式;我总是创建一个子单元 class 并在 xib 或故事板原型单元中从头开始设计单元子视图,甚至在代码中构造它们。

那么如何使用 Apple 的配置来填充单元格的子视图?

视频说:

In addition to the list content configuration we're also giving you access to the associated list content view which implements all of the rendering. You just create or update this view using the configuration, and then you can add it as a subview right alongside your own custom views. This lets you take advantage of all the content configuration features and combine the ListContentView with your own additional custom views next to it, such as an extra image view or a label.

好吧,不,那不是我想做的。我不想要 any 的内置单元格样式子视图。

然后视频说:

Even when you're building a completely custom view hierarchy inside your cells, you can still use the system configurations to help. Because configurations are so lightweight, you can use them as a source of default values for things like fonts, colors, and margins that you copy over to your custom views, even if you never apply the configuration directly itself. And for more advanced use cases you can create a completely custom content configuration type with a paired content view class that renders it, and then use your custom configuration with any cell the same way that you would use a list content configuration.

我的斜体,斜体是我要问的部分。我在问:如何?

那么我如何将它与我的自定义单元格子class 结合使用,以将信息从数据源传递到单元格并填充单元格的子视图?

像往常一样,感觉 Apple 向我们展示了玩具示例,完全忽略了有关这在现实世界中如何工作的细节。有人解决了吗?

编辑我现在已经发表了一系列关于这个主题的文章,从https://www.biteinteractive.com/cell-content-configuration-in-ios-14/开始。


这里的关键——我认为 Apple 并没有在视频中清楚地说明这一点——这些单元配置的工作方式是从字面上撕掉单元的 contentView 并替换它将配置提供的视图作为其 makeContentView.

的输出

因此您所要做的就是手动构建整个内容视图,运行时会为您将其放入单元格中。

这是一个例子。我们需要提供自己的配置类型,采用 UIContentConfiguration,这样我们就可以定义自己的属性;它还必须实现 makeContentView()updated(for:)。假设我们有四个文本要显示在单元格中:

struct Configuration : UIContentConfiguration {
    let text1: String
    let text2: String
    let text3: String
    let text4: String
    func makeContentView() -> UIView & UIContentView {
        let c = MyContentView(configuration: self)
        return c
    }
    func updated(for state: UIConfigurationState) -> MyCell.Configuration {
        return self
    }
}

在现实生活中,我们可能会通过更改 returning 此配置的一个版本并更改一些 属性 来响应状态更改,但在这种情况下无事可做,所以我们只是 return self.

我们假设了MyContentView 的存在,它是一个采用UIContentView 的UIView 子类,也就是说它有一个configuration 属性。这是我们配置视图的子视图并应用配置的地方。在这种情况下,应用配置意味着简单地设置四个标签的文本。我将把这两项任务分开:

class MyContentView: UIView, UIContentView {
    var configuration: UIContentConfiguration {
        didSet {
            self.configure()
        }
    }
    private let lab1 = UILabel()
    private let lab2 = UILabel()
    private let lab3 = UILabel()
    private let lab4 = UILabel()
    init(configuration: UIContentConfiguration) {
        self.configuration = configuration
        super.init(frame: .zero)
        // ... configure the subviews ...
        // ... and add them as subviews to self ...
        self.configure()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    private func configure() {
        guard let config = self.configuration as? Configuration else { return }
        self.lab1.text = config.text1
        self.lab2.text = config.text2
        self.lab3.text = config.text3
        self.lab4.text = config.text4
    }
}

您可以看到该架构的意义所在。如果在未来的某个时候我们被分配了一个新的 configuration,我们只需调用 configure 来再次设置标签的文本,而不需要重建子视图本身。在现实生活中,我们可以通过检查传入的配置来进一步提高效率;如果它与当前配置相同,则无需再次调用 self.configure()

结果是我们现在可以在我们的 tableView(_:cellForRowAt:) 实现中这样说:

override func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(
            withIdentifier: self.cellID, for: indexPath) as! MyCell
        let config = MyCell.Configuration(
            text1: "Harpo",
            text2: "Groucho",
            text3: "Chico",
            text4: "Zeppo"
        )
        cell.contentConfiguration = config
        return cell
}

所有这些都非常聪明,但不幸的是,内容视图界面似乎必须在代码中创建 — 我们无法从 nib 加载现成的单元格,因为从 nib 加载的内容视图,连同它的所有子视图,将被 return 从我们的 makeContentView 实现中编辑的内容视图所取代。因此,Apple 的配置架构不能用于您在故事板或 .xib 文件中设计的单元格。真可惜,但我看不出有什么办法。

Project on GitHub

从Xcode12,iOS14 Table 查看单元配置:

struct CityCellConfiguration: UIContentConfiguration, Hashable {
var image: UIImage? = nil
var cityName: String? = nil
var fafourited: Bool? = false

func makeContentView() -> UIView & UIContentView {
    return CustomContentView(configuration: self)
}

func updated(for state: UIConfigurationState) -> Self {
    guard let state = state as? UICellConfigurationState else { return self }
    let updatedConfig = self

    return updatedConfig
}}

应用配置:

private func apply(configuration: CityCellConfiguration) {
    guard appliedConfiguration != configuration else { return }
    appliedConfiguration = configuration
    
    imageView.isHidden = configuration.image == nil
    imageView.image = configuration.image
    textLabel.isHidden = configuration.cityName == nil
    textLabel.text = configuration.cityName
    favouriteButton.isFavourited = configuration.fafourited ?? false
}

更新单元格内的配置:

override func updateConfiguration(using state: UICellConfigurationState) {
    var content = CityCellConfiguration().updated(for: state)
    content.image = "".image()
    if let item = state.item {
        content.cityName = item.name
        if let data = item.imageData {
            content.image = UIImage(data: data)
        }
    }
    contentConfiguration = content
}

实施Table查看数据源:

extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return cities.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = (tableView.dequeueReusableCell(withIdentifier: Configuration.cellReuseIdentifier) ?? CityTableViewCell(style: .value1, reuseIdentifier: Configuration.cellReuseIdentifier)) as? CityTableViewCell else {
        return UITableViewCell(style: .value1, reuseIdentifier: Configuration.cellReuseIdentifier)
    }
    
    let city = cities[indexPath.row]
    cell.updateWithItem(city)

    return cell
}}