我有一个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
等的代码,但为了保持问题的清晰性,我删除了所有这些代码。
答案 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)
}
}
我相信,与在视图或模型中向下转换相比,这在将来更具可读性/可维护性。
建议
我还建议将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))
}
}
使用当前方法,编译器将始终知道发生了什么,并且将阻止您犯错误,并且将来通过阅读此代码,您可以确切了解正在发生的事情。
注意
如果我们尝试将其抽象到更高的级别(就像Sahil的回答一样),则在某个时候需要向下转换/强制转换是因为dequeue
不会同时发生时间,我们要填充/自定义单元格。 dequeue
必须返回编译器已知的类型。是UITableViewCell
,PersonCell
或AnimalCell
。在第一种情况下,我们必须向下转换,并且不可能抽象PersonCell
和AnimalCell
(除非我们在其模型中尝试向下转换/强制转换)。我们可以使用类似GenericCell<Row>
和cell.fill(with: row)
的类型,但这意味着我们的自定义单元必须在内部处理所有情况(它应在PersonCell
和AnimalCell
视图同时也是无法维护的。
没有下投/强制投篮,这是我多年来最好的成绩。如果需要更多抽象(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)
}