ios - 我应该如何重构我的自定义 UITableView 以提高可维护性

标签 ios swift uitableview

我有一个包含许多不同类型 View 的 UITableView。在 UITableView 数据源的每个方法中,我需要检查单元格的类型和对象的类型,转换它们并正确操作。这不是很干净(它可以工作)但不是很容易维护。

所以我正在做一些事情来抽象这部分,但我有点卡住了。下面的代码是简化的,可能不是很有用,但它是为了演示我当前的问题:

extension UITableView {
    func dequeue<T: UITableViewCell>(_ type: T.Type,
                                     for indexPath: IndexPath) -> T {
        let cell = dequeueReusableCell(withIdentifier: String(describing: type),
                                       for: indexPath)
        guard let cellT = cell as? T else {
            fatalError("Dequeue failed, expect: \(type) was: \(cell)")
        }

        return cellT
    }
}

struct Row<Model, Cell> {
    let view: Cell.Type
    let model: Model

    var fill: ((Model, Cell) -> Void)
}

// Completly unrelated models
struct Person {
    let name: String
}

struct Animal {
    let age: Int
}

// Completely unrelated views
class PersonView: UITableViewCell {

}

class AnimalView: UITableViewCell {

}


// Usage:
let person = Person(name: "Haagenti")
let animal = Animal(age: 12)

let personRow = Row(view: PersonView.self, model: person) { person, cell in
    print(person.name)
}

let animalRow = Row(view: AnimalView.self, model: animal) { animal, cell in
    print(animal.age)
}

let rows = [
//    personRow
    animalRow
]



let tableView = UITableView()
for row in rows {
    tableView.register(row.view, forCellReuseIdentifier: String(describing: row.view))


    let indexPath = IndexPath(row: 0, section: 0)
    let cell = tableView.dequeue(row.view, for: indexPath)

    row.fill(row.model, cell)
}

代码有效,但是当我启用 animalRow 时,Swift 会报错。这并不奇怪,因为它无法解析类型。我不知道如何解决这个问题。

通过使用以下代码,我可以一次声明所有内容并在需要时执行所有部分,例如“填充”。我还将添加 onTap 等代码,但我删除了所有这些代码以保持问题清晰。

最佳答案

Sahil Manchanda 的回答涵盖了解决此问题的 OOD 方法,但缺点是您必须将模型定义为类。

我们首先要考虑的是我们在这里讨论的是可维护性,所以在我看来,Model 不应该知道 View (或者它与哪些 View 兼容),这是 Controller 的责任。 (如果我们想在其他地方对另一个 View 使用相同的模型怎么办?)

其次,如果我们要把它抽象到更高的层次,它肯定会在某些时候需要向下转换/强制转换,所以它可以抽象多少是一个权衡。

所以为了可维护性,我们可以增加可读性和关​​注点/本地推理的分离。

我建议使用 enum为您的模型关联值(value):

enum Row {
    case animal(Animal)
    case person(Person)
}

好吧,现在我们的模型是分开的,我们可以根据它们采取不同的行动。

现在我们必须为 Cells 提出一个解决方案,我通常在我的代码中使用这个协议(protocol):

protocol ModelFillible where Self: UIView {
    associatedtype Model

    func fill(with model: Model)
}

extension ModelFillible {
    func filled(with model: Model) -> Self {
        self.fill(with: model)
        return self
    }
}

所以,我们可以让我们的细胞符合ModelFillible :

extension PersonCell: ModelFillible {
    typealias Model = Person

    func fill(with model: Person) { /* customize cell with person */ }
}

extension AnimalCell: ModelFillible {
    typealias Model = Animal

    func fill(with model: Animal) { /* customize cell with animal */ }
}

现在我们必须将它们粘合在一起。我们可以重构我们的委托(delegate)方法 tableView(_, cellForRow:_)就像这样:

var rows: [Row] = [.person(Person()), .animal(Animal())]

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    switch rows[indexPath.row] {
    case .person(let person): return (tableView.dequeue(for: indexPath) as PersonCell).filled(with: person)
    case .animal(let animal): return (tableView.dequeue(for: indexPath) as AnimalCell).filled(with: animal)
    }
}

我相信在未来这比 View 或模型中的向下转换更具可读性/可维护性。

建议

我还建议解耦 PersonCell来自 Person也可以这样使用:

extension PersonCell: ModelFillible {
    struct Model {
        let title: String
    }

    func fill(with model: Model { /* customize cell with model.title */ }
}

extension PersonCell.Model {
    init(_ person: Person) { /* generate title from person */ }
}

然后在您的 tableView 委托(delegate)中像这样使用它:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    switch rows[indexPath.row] {
    case .person(let person): return (tableView.dequeue(for: indexPath) as PersonCell).filled(with: .init(person))
    case .animal(let animal): return (tableView.dequeue(for: indexPath) as AnimalCell).filled(with: .init(animal))
    }
}

使用当前的方法,编译器将始终知道发生了什么,并会阻止您犯错误,并且将来通过阅读这段代码,您将确切地知道发生了什么。

注意

如果我们尝试将其抽象到更高级别(就像 Sahil 的回答),那么在某些时候它需要向下转换/强制转换的原因是 dequeue不会在我们想要填充/自定义我们的单元格的同时发生。 dequeue必须返回编译器已知的类型。要么是 UITableViewCell , PersonCellAnimalCell .在第一种情况下,我们必须向下转换它,并且不可能抽象 PersonCellAnimalCell (除非我们在他们的模型中尝试向下转换/强制转换)。我们可以使用类似 GenericCell<Row> 的类型还有cell.fill(with: row)但这意味着我们的定制单元必须在内部处理所有情况(它应该同时处理 PersonCellAnimalCell View ,这也是不可维护的)。

在没有向下转换/强制转换的情况下,这是我多年来获得的最好结果。如果您需要更多抽象(dequeue 的单行,fill 的单行)Sahil 的答案是最好的方法。

关于ios - 我应该如何重构我的自定义 UITableView 以提高可维护性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54630836/

相关文章:

ios - 此类不符合键的键值编码

ios - 如何在 AFNetworking 3.0 中使用 AFHTTPSessionManager 获取下载进度

ios - swift : Separate classes for network requests

ios - 在 UITableViewCell 内渲染 UIWebView

ios - SwiftUI旋转屏幕使模式不再消失

ios - Xcode 上的 RealmSwift 安装

iphone - 详细披露指标的放置

iphone - 在 App Purchase 用户取消 tx 而应用程序在后台 : tx state stays on purchasing

ios - 提交表单时未使用表达式结果

ios - 在 Swift 中更改我的 UIPickerView 的大小