Как я должен реорганизовать свой пользовательский UITableView для улучшения удобства обслуживания - PullRequest
0 голосов
/ 11 февраля 2019

У меня есть 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 и т. Д., Но я удалил весь этот код, чтобы избежать проблем.

Ответы [ 4 ]

0 голосов
/ 20 февраля 2019

Ответ Sahil Manchanda охватывает подход OOD к решению этой проблемы, но в качестве недостатка вы должны определить свои модели как класс.

Первое, что нам нужно учесть, это то, что мы обсуждаем возможность обслуживания.здесь, по моему скромному мнению, Model не должна знать о представлении (или о том, с какими представлениями оно совместимо), это ответственность Контролера.(что, если мы хотим использовать ту же модель для другого представления где-то еще?)

Во-вторых, если мы хотим абстрагировать ее до более высоких уровней, она определенно потребует понижающего / принудительного приведения в некоторыхТочка, так что есть компромисс с тем, насколько это может быть абстрагировано.

Таким образом, ради удобства обслуживания, мы можем повысить читабельность и разделение интересов / локальных рассуждений.

Я предлагаюиспользовать enum со связанным значением для ваших моделей:

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

Ну, сейчас наши модели разделены, и мы можем действовать по-разному, основываясь на них.

Теперь мы должны прийтис решением для ячеек я обычно использую этот протокол в своем коде:

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)
    }
}

Я верю, что в будущем это будет более читабельным / поддерживаемым, чем преобразование в нисходящий поток в представлениях или моделях.

Предложение

Я также предлагаю отделить 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 использует его следующим образом:

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))
    }
}

При текущем подходе компилятор всегда будет знать, что происходит, и будет защищать вас от ошибок, и в будущем, читая этот код, вы будете точно знать, что происходит.

Примечание

Причина, по которой в какой-то момент потребуется понижение / принудительное приведение, если мы попытаемся абстрагировать его до более высоких уровней (точно так же, как ответ Сахила), тот факт, что dequeue не происходит в то же время, когда мы хотим заполнить / настроить нашу ячейку.dequeue должен возвращать тип, известный компилятору.это либо UITableViewCell, PersonCell, либо AnimalCell.В первом случае мы должны понижать его, и невозможно абстрагировать PersonCell и AnimalCell (если мы не попробуем понижающее / принудительное приведение в их моделях).Мы можем использовать такой тип, как GenericCell<Row>, а также cell.fill(with: row), но это означает, что наша настраиваемая ячейка должна обрабатывать все случаи внутренне (она должна обрабатывать PersonCell и AnimalCell одновременно, что также невозможно обслуживать).

Без приведения / принудительного приведения это лучшее, что я получил за эти годы.Если вам нужно больше абстракций (одна строка для dequeue и одна строка для fill), ответ Сахиля - лучший путь.

0 голосов
/ 14 февраля 2019

Я бы создал протокол для строк, которые будут использоваться в массиве источника данных

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)
0 голосов
/ 20 февраля 2019

Я понимаю, что вы хотите реализовать.Для этого есть небольшая библиотека в Свифте.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)
}
0 голосов
/ 14 февраля 2019

Взгляните на следующую структуру:

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 -используется для выполнения действий «Tap». Перечисления CellTypes используются для идентификации типа ячеек и для удаления из очереди. Все классы Model будут наследовать BaseModel, что весьма полезно и устраняет необходимость ввода типа в cellForRow для функции.и все tableViewCells унаследовали BaseCell, который содержит две переменные, т.е. модель и делегат.они переопределяются в ячейке «Человек и животное».

Редактировать : Риск потери типа Безопасность, безусловно, может быть уменьшена, если указать «тип ячейки» непосредственно в super.init () в классе модели.например,

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

Поскольку ячейки снимаются с переменной 'type'. Правильная модель будет предоставлена ​​для правильной ячейки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...