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

时间:2019-02-11 12:40:27

标签: ios swift uitableview

我有一个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会抱怨。这并不奇怪,因为它无法解析类型。我不知道该如何解决。

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

4 个答案:

答案 0 :(得分:4)

Sahil Manchanda的答案是解决该问题的OOD方法,但缺点是必须将模型定义为类。

我们首先要考虑的事实是,我们正在这里讨论可维护性,因此,我谦虚地认为,Model不应了解该视图(或与之兼容的视图),这就是Controller的责任。 (如果我们要在其他地方的其他视图中使用相同的模型,该怎么办?)

第二件事是,如果我们想将其抽象到更高的层次,那么肯定在某个时候需要向下转换/强制转换,因此需要对可以抽象多少进行权衡。

因此,出于可维护性考虑,我们可以提高关注点/局部推理的可读性和分离度。

我建议为您的模型使用带有关联值的enum

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

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

现在我们必须为Cells解决方案,我通常在我的代码中使用以下协议:

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 */ }
}

现在,我们必须将它们粘合在一起。我们可以像这样重构我们的委托方法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)
    }
}

我相信,与在视图或模型中向下转换相比,这在将来更具可读性/可维护性。

建议

我还建议将PersonCellPerson分离,并像这样使用它:

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委托中按如下方式使用它:

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必须返回编译器已知的类型。是UITableViewCellPersonCellAnimalCell。在第一种情况下,我们必须向下转换,并且不可能抽象PersonCellAnimalCell(除非我们在其模型中尝试向下转换/强制转换)。我们可以使用类似GenericCell<Row>cell.fill(with: row)的类型,但这意味着我们的自定义单元必须在内部处理所有情况(它应在PersonCellAnimalCell视图同时也是无法维护的。

没有下投/强制投篮,这是我多年来最好的成绩。如果需要更多抽象(dequeue的单行,fill的单行),Sahil的答案是最好的选择。

答案 1 :(得分:3)

看看下面的结构:

protocol MyDelegate {
    func yourDelegateFunctionForPerson(model: Person)
    func yourDelegateFunctionForAnimal(model: Animal)
}


enum CellTypes: String{
    case person = "personCell"
    case animal = "animalCell"
}

基本模型

class BaseModel{
    var type: CellTypes

    init(type: CellTypes) {
        self.type = type
    }
}

人员模型

class Person: BaseModel{
    var name: String
    init(name: String, type: CellTypes) {
        self.name = name
        super.init(type: type)
    }
}

动物模型

class Animal: BaseModel{
    var weight: String
    init(weight: String, type: CellTypes) {
        self.weight = weight
        super.init(type: type)
    }
}

基本单元格

class BaseCell: UITableViewCell{
    var model: BaseModel?
}

人格

class PersonCell: BaseCell{
    override var model: BaseModel?{
        didSet{
            guard let model = model as? Person else {fatalError("Wrong Model")}
            // do what ever you want with this Person Instance
        }
    }
}

动物细胞

class AnimalCell: BaseCell{
    override var model: BaseModel?{
        didSet{
            guard let model = model as? Animal else {fatalError("Wrong Model")}
            // do what ever you want with this Animal Instance
        }
    }
}

视图控制器

    class ViewController: UIViewController{
    @IBOutlet weak var tableView: UITableView!

    var list = [BaseModel]()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupList()
    }

    func setupList(){
        let person = Person(name: "John Doe", type: .person)
        let animal = Animal(weight: "80 KG", type: .animal)
        list.append(person)
        list.append(animal)
        tableView.dataSource = self
    }
}

extension ViewController: UITableViewDataSource{

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let model = list[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: model.type.rawValue, for: indexPath) as! BaseCell
        cell.model = model
        cell.delegate = self
        return cell
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return list.count
    }
}

extension ViewController: MyDelegate{
    func yourDelegateFunctionForPerson(model: Person) {

    }

    func yourDelegateFunctionForAnimal(model: Person) {

    }


}

MyDelegate协议用于执行“点击”操作 CellTypes枚举用于识别Cell Type并用于出队 所有的Model类都将继承BaseModel,这是非常有用的,并且将不需要在cellForRow函数中键入大小写。并且所有tableViewCells都继承了BaseCell,其中包含两个变量,即model和委托。这些在“人和动物细胞”中被覆盖。

编辑:如果直接在模型类的super.init()中指定“ celltype”,则可以降低类型丢失的风险。例如

class Person: BaseModel{
    var name: String
    init(name: String) {
        self.name = name
        super.init(type: .person)
    }
}

当单元格使用'type'变量出队时,将为正确的单元格提供正确的模型。

答案 2 :(得分:0)

我将为数据源数组中要使用的行创建一个协议

protocol TableRow {
    var view: UITableViewCell.Type {get}
    func fill(_ cell: UITableViewCell)
}

然后创建符合此协议的其他行结构

struct PersonRow: TableRow {
    var view: UITableViewCell.Type
    var model: Person

    func fill(_ cell: UITableViewCell) {
        cell.textLabel?.text = model.name
    }
}

struct AnimalRow: TableRow {
    var view: UITableViewCell.Type
    var model: Animal

    func fill(_ cell: UITableViewCell) {
        cell.textLabel?.text = String(model.age)
    }
}

然后将数据源定义为

var rows: [TableRow]()

,并且可以添加符合TableRow协议的任何类型

rows.append(PersonRow(view: PersonView.self, model: person))
rows.append(AnimalRow(view: AnimalView.self, model: animal))

并通过调用fill

设置单元格的值
let cell = tableView.dequeue(row.view, for: indexPath)    
row.fill(cell)

答案 3 :(得分:0)

我了解您要实现的目标。 Swift中有一个小的库用于此操作。 https://github.com/maxsokolov/TableKit

这里最有趣的部分是ConfigurableCell,如果您仅将此协议复制到项目中,它将解决您的问题: https://github.com/maxsokolov/TableKit/blob/master/Sources/ConfigurableCell.swift

基本思路如下:

public protocol ConfigurableCell {

    associatedtype CellData

    static var reuseIdentifier: String { get }
    static var estimatedHeight: CGFloat? { get }
    static var defaultHeight: CGFloat? { get }

    func configure(with _: CellData)
}