Swift实现iOS的TableView抽象工厂模式

前言

众所周知,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!
    }

方法实现步骤:

  1. 根据是否有section取不同的CellModel
  2. 通过CellModel的cellClassName方法取缓存池中的Cell
  3. 如果Cell取出为空,则动态的通过字符串构建一个Cell对象
  4. 此时Cell一定不为空,所以需要提供机会给外部持有adapter的类对Cell进行一些操作
  5. 调用实现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中繁杂的问题。

猜你喜欢

转载自blog.csdn.net/weixin_33742618/article/details/86987605