前言
众所周知,iOS中tableView是十分重要的,我们随着业务的迭代,会使得tableView中的Cell越来越繁杂,这就会导致在tableView的cellForRow方法中使用越来越多的if else。即使你使用简单工厂模式,通过枚举去构造不同的Cell,但实际上你每次的改动Cell都要去动cellForRow这个方法,这个是十分恶心的事情。所以为了避免这些繁杂的操作,我们需要在tableView中使用抽象工厂这种设计模式。
其实在OC中实现tableView的抽象工厂是比较容易的,因为OC相对于Swift就是无法无天,而Swift想要实现一些动态特性却有些麻烦而且也不够Swifty。本文记录了我使用Swift去实现一个tableView的抽象工厂模式
实现
提到抽象工厂,我们第一个想到的就是创建一个基类,然后创建一系列相关抽象子类需要的接口。其实我们可以换个想法,我们用协议去定抽象的接口,要求实现类实现这些协议,而不是去定义一个抽象类,再创建抽象子类去实现父类的方法。
Model
class BaseModel: NSObject {
required init(with data: SwiftyJSON.JSON) {
super.init()
}
}
一般情况下,我们通过网络请求的方式获取数据,为了方面解析JSON转化为Model,所以我们使用SwiftyJSON这个库。因为需要利用到OC的动态特性,所以使用protocol定义接口的情况下是必须加上@objc的,但是protocol没办法在使用SwiftyJSON的情况下加上@objc,所以这里的BaseModel不使用协议而是以类的形式来定义,相当于所有Model的基类。
这里的自定义初始化方法的目的是要求所有继承了这个BaseModel的Model必须实现这个初始化方法。
CellModel
@objc protocol BaseTableViewCellModel {
func cellHeight() -> CGFloat
func cellClassName() -> String
init(with model: BaseModel?)
}
cellHeight()
一般我们为了tableView的可靠性能,我们会在Model中计算Cell的整体高度,甚至是计算布局,所以需要实现这样一个获取高度的方法。
cellClassName()
这个方法的目的是为了能够让工厂类动态的创建Cell
initWithModel:
这个方法要求实现该协议的类必须根据BaseModel去创建生成CellModel,这样就提供了机会去做CellModel该做的事情,比如说计算高度、计算布局和处理一些额外的数据。
BaseTableViewCell
@objc protocol BaseTableViewCell {
func updateCell(with cellModel: BaseTableViewCellModel)
}
Cell是需要机会通过CellModel去刷新数据的,所以要求这个协议的实现类必须实现updateCell这个方法。
Adapter
这个类之所以叫做Adapter是因为它除了实现工厂的作用,它还需要去转发不同Cell的事件给Controller。
先放上全部的代码,
protocol TableViewAdapterDelegate: class {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
func tableViewCell(_ cell: UITableViewCell, forRowAt indexPath: IndexPath)
func scrollViewDidScroll(_ scrollView: UIScrollView)
}
extension TableViewAdapterDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0
}
func tableViewCell(_ cell: UITableViewCell, forRowAt indexPath: IndexPath) {}
func scrollViewDidScroll(_ scrollView: UIScrollView) {}
}
class TableViewAdapter: NSObject, BaseTableViewAdapter {
weak var tableView: UITableView?
public var isNeedSection = false
public var sectionDataArray: [[BaseTableViewCellModel]] = []
public var dataAray: [BaseTableViewCellModel] = []
public weak var adapterDelegate: TableViewAdapterDelegate?
required init(with tableView: UITableView) {
super.init()
self.tableView = tableView
self.tableView?.delegate = self
self.tableView?.dataSource = self
}
public func reloadData() {
tableView?.reloadData()
}
}
extension TableViewAdapter: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return isNeedSection ? sectionDataArray.count : 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return isNeedSection ? sectionDataArray[section].count : dataAray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = isNeedSection ? sectionDataArray[indexPath.section][indexPath.row] : dataAray[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!, forRowAt: indexPath)
cell!.perform(#selector(BaseTableViewCell.updateCell(with:)), with: model)
return cell!
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return isNeedSection ? sectionDataArray[indexPath.section][indexPath.row].cellHeight() : dataAray[indexPath.row].cellHeight()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
adapterDelegate?.tableView(tableView, didSelectRowAt: indexPath)
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return adapterDelegate?.tableView(tableView, viewForHeaderInSection: section)
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return adapterDelegate?.tableView(tableView, viewForFooterInSection: section)
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return adapterDelegate?.tableView(tableView, heightForHeaderInSection: section) ?? 0
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return adapterDelegate?.tableView(tableView, heightForFooterInSection: section) ?? 0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
adapterDelegate?.scrollViewDidScroll(scrollView)
}
}
这个类我们从重要到次要逐步分析
工厂
首先我们先来看最重要的cellForRow方法
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = isNeedSection ? sectionDataArray[indexPath.section][indexPath.row] : dataAray[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!, forRowAt: indexPath)
cell!.perform(#selector(BaseTableViewCell.updateCell(with:)), with: model)
return cell!
}
方法实现步骤:
- 根据是否有section取不同的CellModel
- 通过CellModel的cellClassName方法取缓存池中的Cell
- 如果Cell取出为空,则动态的通过字符串构建一个Cell对象
- 此时Cell一定不为空,所以需要提供机会给外部持有adapter的类对Cell进行一些操作
- 调用实现BaseTableViewCell这个协议的Cell的updateCell方法
我们可以发现在步骤3中去动态的创建了一个Cell对象,所以就要求实现BaseTableViewCellModel协议的CellModel在cellClassName这个方法中必须给出正确的Cell的类名
获取Cell的机会
TableViewAdapterDelegate中有一个方法是
func tableViewCell(_ cell: UITableViewCell, forRowAt indexPath: IndexPath)
很多时候我们的Cell是有一些block和delegate是需要外部去处理的,这个时候就有机会去绑定这个Cell的事件。使用的时候只要通过as!强制转换为你需要的类型即可。
转发UITableViewDelegate
很多情况下我们还需要实现很多UITableViewDelegate方法,这个时候需要用TableViewAdapterDelegate去转发,但是考虑到很多tableView的代理方法是可选的,所以我们需要extension TableViewAdapterDelegate去让TableViewAdapterDelegate中的方法变得可选。本文只实现了部分UITableViewDelegate方法,具体情况还需要具体添加不同的方法。
例子
说了这么多,没有一个使用例子也不行,下面来写一个例子吧。
首先我们需要有一个Model
class ExamModel: BaseModel {
var name: String?
var type: String?
var times: String?
var address: String?
var id: String?
required init(with data: SwiftyJSON.JSON) {
super.init(with: data)
name = data["name"].string ?? ""
type = data["type"].string ?? ""
address = data["address"].string ?? ""
times = data["period"].string ?? ""
id = data["course_id"].string ?? ""
times = times?.replacingOccurrences(of: "年", with: "-")
.replacingOccurrences(of: "月", with: "-")
.replacingOccurrences(of: "日(", with: " ")
.replacingOccurrences(of: ")", with: "")
}
}
接着我们需要构造一个CellModel
class ExamCellModel: NSObject, BaseTableViewCellModel {
public var examModel: ExamModel?
public var finished: Bool = false
private var height: CGFloat = 0
func cellHeight() -> CGFloat {
return height
}
func cellClassName() -> String {
return "ExamCell"
}
required init(with model: BaseModel?) {
super.init()
examModel = model as? ExamModel
calculateCellHeight()
}
func calculateCellHeight() {
height += 32
height += 20
height += examModel?.name?.ce_height(font: UIFont.init(name: "Helvetica Neue", size: 16)!, width: SCREEN_WIDTH - 80 - 15) ?? 0
height += examModel?.timesWeek?.ce_height(font: UIFont.init(name: "Helvetica Neue", size: 13)!, width: SCREEN_WIDTH - 80 - 15) ?? 0
height += examModel?.times?.ce_height(font: UIFont.init(name: "Helvetica Neue", size: 12)!, width: SCREEN_WIDTH - 80 - 15) ?? 0
}
}
可以看到我在里面计算了Cell的高度
接着是一个Cell
class ExamCell: UITableViewCell, BaseTableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setupSubviews() {
selectionStyle = .none
// 忽略
}
func updateCell(with cellModel: BaseTableViewCellModel) {
let m = cellModel as? ExamScheduleBodyCellModel
stateImageView.image = m?.finished ?? false ? UIImage.init(named: "finish_circle_1") : UIImage.init(named: "unfinish_circle_1")
courseLabel.text = m?.examModel?.name
typeLabel.text = m?.examModel?.type
timeLabel.text = m?.examModel?.times
addressLabel.text = m?.examModel?.address
if m?.finished ?? false {
typeLabel.backgroundColor = UIColor(named: "iron")
weakLabel.backgroundColor = UIColor(named: "iron")
} else {
typeLabel.backgroundColor = UIColor(named: "dodger_blue")
weakLabel.backgroundColor = UIColor(named: "dodger_blue")
}
updateCellLayout()
}
func updateCellLayout() {
// 更新布局
}
lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.init(named: "silver_sand")
return view
}()
// 还有很多,也都忽略
}
接着只需要去构造一个CellModel组成的一维或者二维数组,赋值给adapter的dataArray,再去reloadData就行了,这里就不详细展开了。
结语
本文只提供了一个思路去快速的构建一个tableView,通过这样的方法你以CellModel的方式驱动一个Cell,能够快速的在一个tableView中创建不同的Cell,完全不需要去考虑cellForRow中繁杂的问题。