前言
众所周知,iOS中tableView是十分重要的,我们随着业务的迭代,会使得tableView中的Cell越来越繁杂,这就会导致在tableView的cellForRow方法中使用越来越多的if else。尽管你使用简单工厂模式,通过枚举去构造不同的Cell,但你每次新增或修改cell样式时仍然要去改动cellForRow这个方法,这个是十分恶心的事情。所以为了避免这些繁杂的操作,我们需要将cellForRow中的处理逻辑进行拆分。
实现
通过以Provider实现TableViewCellProvider协议的方式去创建一个Cell,不同的Provider组成的数据源能够在一个列表里展示不同类型的Cell。
CellProvider
public protocol TableViewCellProvider {
/// 返回cell的高度
func cellHeight() -> CGFloat
/// 返回cell的类名
func cellClassName() -> String
}
cellHeight()
一般我们为了tableView的可靠性能,我们会在Model中计算Cell的整体高度,甚至是计算布局,所以需要实现这样一个获取高度的方法。
cellClassName()
这个方法的目的是为了能够让adapter动态的创建Cell
其他
这里补充一点,如果需要进一步的优化Cell,可以在Provider中计算每个控件的布局。
TableViewCellDataReceiver
public protocol TableViewCellDataReceiver {
func updateCell(with provider: TableViewCellProvider)
}
Cell是需要机会通过Provider去刷新数据的,所以要求这个协议的实现类必须实现updateCell这个方法。
Adapter
Adapter是因为它除了实现组装不同Cell的作用,它还需要去转发不同Cell的事件给Controller。实现原理也很简单,根据实现了TableViewCellProvider协议的类来动态创建cell
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = useSection ? sectionCellModelArray[indexPath.section][indexPath.row] : cellModelArray[indexPath.row]
var cell = tableView.dequeueReusableCell(withIdentifier: model.cellClassName())
if cell == nil {
let appName = Bundle.main.infoDictionary!["CFBundleExecutable"] as! String
let cellType = NSClassFromString(appName + "." + model.cellClassName()) as! UITableViewCell.Type
cell = cellType.init(style: .default, reuseIdentifier: model.cellClassName())
}
adapterDelegate?.tableViewCell(cell!, willUpdateRowAt: indexPath)
(cell as! TableViewCellDataReceiver).updateCell(with: model)
adapterDelegate?.tableViewCell(cell!, didUpdateRowAt: indexPath)
return cell!
}
转发UITableViewDelegate和UITableViewDataSource
很多情况下我们还需要实现很多tableView的delegate和dataSource的其他方法,这个时候需要用到iOS中的消息转发机制了。因为是swift项目,所以NSProxy并不好使,只能继承一个NSObject来模拟NSProxy,本文这里实现的类是BaseProxy,这里就不详细展开说明消息转发的机制,大家可以自行百度。
所以TableViewAdapter中提供的的tableViewDelegate和tableViewDataSource属性就相当于tableView中的的delegate和dataSource。如果你又实现了TableViewAdapter实现过的代理,那么你自己实现的代理会被优先处理,因为本文在BaseProxy做消息转发时优先判断持有tableViewDelegate和tableViewDataSource属性的对象有没有实现这个方法。
例子
说了这么多,没有一个使用例子也不行,下面来写一个例子吧。
首先我们需要有一个Model
class InfoModel {
let image: UIImage? = nil
let title = "我是标题"
let content = "我是长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长的文案"
}
接着我们需要构造一个Provider
class FirstCellProvider: TableViewCellProvider {
var color = UIColor.red
var model: InfoModel?
var height: CGFloat = 40
func cellHeight() -> CGFloat {
return height
}
func cellClassName() -> String {
return "FirstCell"
}
init(with model: InfoModel) {
self.model = model
// 在这可以计算cell高度
let contentHeight = model.content.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: 0), options: [.usesLineFragmentOrigin], attributes: nil, context: nil).height
height += contentHeight
height += 21
}
}
可以看到我在里面计算了Cell的高度
接着是一个Cell
class FirstCell: UITableViewCell, TableViewCellDataReceiver {
func updateCell(with provider: TableViewCellProvider) {
let p = provider as! FirstCellProvider
backgroundColor = p.color
let model = p.model!
titleLabel.text = model.title
contentLabel.text = model.content
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(titleLabel)
contentView.addSubview(contentLabel)
titleLabel.frame = CGRect.init(x: 11, y: 11, width: 120, height: 21)
contentLabel.frame = CGRect.init(x: 11, y: 42, width: 300, height: bounds.height - 42)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
lazy var titleLabel: UILabel = {
let view = UILabel()
view.textColor = .black
return view
}()
lazy var contentLabel: UILabel = {
let view = UILabel()
view.textColor = .black
return view
}()
}
接着只需要去构造一个实现TableViewCellProvider协议组成的一维或者二维数组,再让adapter使用如下方法去reloadData就行了
public func reloadRowsData(by array: [TableViewCellProvider])
public func reloadSectionsData(by array: [[TableViewCellProvider]])
结语
本文只提供了一个思路去快速的构建一个tableView,通过这样的方法你以Provider的方式驱动一个Cell,能够快速的在一个tableView中创建不同的Cell,完全不需要去考虑cellForRow中繁杂的问题。
最后,我把这一套都封装好了,欢迎点击下面链接使用。