iOS自动cell配置

2019-07-25  本文已影响0人  半心_忬

能解决什么

当一个table存在多种类型的cell,判断逻辑势必会非常繁杂

1. 所有cell都有部分内容相同,使用的数据结构一致
2. cell样式完全不同,使用的数据结构也不一样
3. 多种cell,但其中某个cell,需要根据不同的数据结构,展示不一样的样式(暂时还没有实际场景,但是可以支持)

除了第3种,场景很少,1和2还是比较常见的,第2种有的时候会为了方便网络请求,强行把两个不同的数据结构合并成一个数据结构,导致数据结构非常臃肿。这几类情况,无论是tabview的配置还是cell的展示,按常规套路进行配置的话,判断逻辑都会非常复杂。这里提供一个自动配置的方案,仅供参考,欢迎一起探讨,有更好的实现方式请多指教,多谢。

实现思路

先说说主要的思路,思路来源于现在Leader实现的某APP的搜索功能的源码,然后适当做了一些改进,既能实现很复杂的配置,也能实现一些说复杂又不复杂但代码会写的非常恶心的tableView。

一、CellBuilder - Cell 创建者

首先,看Builder必须要遵守的protocol:

/// cell 创建 protocol
protocol SZCellBuilderProtocol {
    
    // dataSource
    var dataSource: SZCellBuilderDataSource? {get set}
    
    /// cell 数据类型(classString 或者 某个指定的key)
    ///
    /// - Returns: 类型
    func supportDataType() -> String
    
    /// cell 高度
    ///
    /// - Parameter model: cell model
    /// - Returns: cell 高度
    func cellHeightWithModel(model: Any) -> CGFloat
    
    /// cell 注册ID
    ///
    /// - Returns: 注册ID
    func cellReuseId() -> String
    
    /// cell class
    ///
    /// - Returns: class
    func cellClass() -> AnyClass
}

CellBuilder,cell的创建者,不同类型的cell都有对应的builder且都必须实现该protocol,实现该protocol我们获取了几个Cell渲染的必须值:

1. cell的数据类型,渲染cell必须的model;
2. cell 高度,渲染cell必须的cell高度;
3. cell 注册ID,cell重用的标识;
4. cell 的类型,需要渲染哪个cell;
5. dataSource,这个是个可选值,目前dataSource只定义了一个方法,就是获取当前的tableView,我们可能需要在Builder中使用tableView,其他的方式不好传入。(eg:DEMO缓存cell高度时需要使用tableView)

我们看一下这个dataSource:

    /// cell 创建 dataSource
@objc protocol SZCellBuilderDataSource {
    
    /// 获取cell的tableView (可能需要用到table,例:自动缓存行高时)
    @objc optional func tableViewForCellBuilder() -> UITableView
}

我们来看一个具体的Builder的实现:

/// 第一种类型 cell builder
class SZEncapsulationTypeOneCellBuilder: SZCellBuilderProtocol {
    
    var dataSource: SZCellBuilderDataSource?
    
    func supportDataType() -> String {
        
        return DifferentDataModelType.one.rawValue
    }
    
    func cellHeightWithModel(model: Any) -> CGFloat {
        
        return 104
    }
    
    func cellReuseId() -> String {
        
        return "SZEncapsulationTypeOneCellReuseId"
    }
    
    func cellClass() -> AnyClass {
        
        return DifferentDataModelOneCell.self
    }
}

二、CellControl - Cell的事件管理者

首先说说CellControl是干什么用的,CellControl需要做的事情有就是处理cell上的各种操作(自身的点击事件和Cell上元素的点击事件)。

这里,control需要实现一个protocol和一个delegate,一个是对于cell级别的控制protocol和针对cell内事件的base cell 代理,CellControlProtocol需要获取到当前的tableview,当前cell的数据model,响应多个类型cell所对应的数据类型的 唯一标识,viewController(需要的话,由于一些操作是需要知道当前VC的),还有处理cell的点击事件;base cell 代理,则是处理cell上元素的一些操作,主要是各种的点击事件。

先来看看定义好的 protocol:

/// cell 控制 protocol
@objc protocol SZCellControlProtocol {
    
    /// dataSource
    var dataSource: SZCellControlDataSource? {get set}
    
    /// 响应多种类型的cell
    ///
    /// - Returns: 响应多个类型cell所对应的数据类型的 classString
    func supportDataTypes() -> [String]
}

上述 protocol中的dataSource:

/// cell 控制 DataSource
@objc protocol SZCellControlDataSource {
    
    // MARK: - 可选扩展
    
    /// 配置cell的数据模型(需要拿到数据做一些事情)
    ///
    /// - Parameter indexPath: indexPath
    /// - Returns: 数据模型
    @objc optional func dataModelForCell(indexPath: NSIndexPath) -> Any?
    
    /// 获取cell的tableView(可能需要用到table,例:刷新cell的某一cell 或 某一个 section)
    ///
    /// - Returns: tableView
    @objc optional func tableViewForCellControl() -> UITableView
    
    /// 获取cell的当前 VC (可能需要用到vc,例:执行跳转)
    ///
    /// - Returns: 当前 VC
    @objc optional func currentViewControllerForCell() -> UIViewController
}

在看看另外的需要实现的 delegate:

/// base cell 代理 (自定义的 delegate 继承 base)
@objc protocol SZBaseCellDelegate {
    
    // MARK: - 可选扩展
    
    /// 选择了某个item
    ///
    /// - Parameter indexPath: indexPath tableView indexPath
    @objc optional func didSelectedItemAtIndexPath(indexPath: IndexPath)
}

基类的代理,只定义了一个可选的 cell 点击事件方法,如需要更多的时间处理,继承该 SZBaseCellDelegate 就可以了。

eg:

/// 封装 cell 代理
@objc protocol SZEncapsulationCellDelegate: SZBaseCellDelegate {
    
    // MARK: - optional
    
    /// 点击了Cell上的名称
    ///
    /// - Parameter cell: 对应的cell
    @objc optional func tapedNameOnCell(cell: UITableViewCell)
    
    /// 点击了Cell上的图片item
    ///
    /// - Parameter cell: 对应的cell
    @objc optional func tapedImageItemOnCell(cell: UITableViewCell)
    
    /// 点击了Cell上的天气图标
    ///
    /// - Parameter cell: 对应的cell
    @objc optional func tapedWeatherIconOnCell(cell: UITableViewCell)
    
    /// 点击了Cell上的门店
    ///
    /// - Parameter cell: 对应的cell
    @objc optional func tapedShopOnCell(cell: UITableViewCell)
    
    /// 点击了Cell上的知识
    ///
    /// - Parameter cell: 对应的cell
    @objc optional func tapedKnowledgeOnCell(cell: UITableViewCell)
    
    /// 点击了Cell上的素材
    ///
    /// - Parameter cell: 对应的cell
    @objc optional func tapedMaterialOnCell(cell: UITableViewCell)
}

Tips:目前暂时没有支持SZCellControlProtocol的继承。

我可与看一个具体的实现类:

/// 封装 事件处理基类
class SZEncapsulationBaseActionControl: NSObject, SZCellControlProtocol, SZDifferentDataModelCellDelegate {
    
    // MARK: - SZCellControlProtocol
    
    var dataSource: SZCellControlDataSource?
    
    func supportDataTypes() -> [String] {
        
        return [DifferentDataModelType.one.rawValue,
                DifferentDataModelType.two.rawValue,
                DifferentDataModelType.three.rawValue,
                DifferentDataModelType.four.rawValue]
    }
    
    /*
     需要给子类重写,所以不放在 extension
     */
    
    // MARK: - SZDifferentDataModelCellDelegate
    
    func didSelectedItemAtIndexPath(indexPath: IndexPath) {
        
        print("点击了 cell")
    }
}

有些属性和方法是每个cell都需要使用的,所以可以写一个基类。

然后看子类的实现,重写只属于自己的属性,并且加上只有自己才有的代理事件:

/// 封装类型二 事件处理
class SZEncapsulationTypeTwoActionControl: SZEncapsulationBaseActionControl {
    
    override func supportDataTypes() -> [String] {
        
        return [DifferentDataModelType.two.rawValue]
    }
    
    // MARK: - SZDifferentDataModelCellDelegate
    
    func tapedShopOnCell(cell: UITableViewCell) {
        
        print("点击了 门店")
    }
    
    func tapedMaterialOnCell(cell: UITableViewCell) {
        
        print("点击了 素材")
    }
    
    func tapedKnowledgeOnCell(cell: UITableViewCell) {
        
        print("点击了 知识")
    }
}

三、SZCellModelExtentionProtocol - Cell的数据类型管理者(也是分发的管理者)

先看这个Protocol的定义:

/// 模型转换 protocol
protocol SZCellModelExtentionProtocol {
    
    /// 需要转换的model 的 key
    ///
    /// - Returns: key
    func modelExtentionItemType() -> String
}

每个cell对应的model,需要实现该protocol,在模型转换和cell渲染分发时,需要使用。模型转换,目前使用的HandyJSON,是不支持一个数组中不同的数据模型一次性转换的,所以需要做循环遍历,单个转换,eg:

/// 转换
    ///
    /// - Parameter originAry: 原始的dic ary
    private func convert(originAry: [[String: Any]]) -> [Any] {
        
        var showAry = [Any]()
        
        /*
         data 中存在 不同的数据类型,故而手动转换,
         同时也存在一个过滤的操作,如果不是指定的 type ,则忽略
         */
        for itemDic in originAry {
            
            guard let itemType = itemDic["type"] as? String,
                let cls = cellModelExtensionFactory.getModelWithDataType(itemType: itemType) as? HandyJSON,
                let model = type(of: cls).deserialize(from: itemDic) else {
                    
                    continue
            }
            
            showAry.append(model)
        }
        
        return showAry
    }

分发的话,后续会讲到。

这里支持四种不同的cell类型:

enum DifferentDataModelType: String {
    
    case one   = "DifferentDataModelTypeOne"
    case two   = "DifferentDataModelTypeTwo"
    case three = "DifferentDataModelTypeThree"
    case four  = "DifferentDataModelTypeFour"
}

看一个具体model实现:

/// 不同数据类型 base
class DifferentDataModelBaseModel: HandyJSON, SZCellModelExtentionProtocol {
    
    /// 类型
    var type: DifferentDataModelType = .one
    
    required init() {}
    
    // MARK: - SZCellModelExtentionProtocol
    
    func modelExtentionItemType() -> String {
        
        return DifferentDataModelType.one.rawValue
    }
}

/// 不同数据类型 第二种类型 model
class DifferentDataModelTwoModel: DifferentDataModelBaseModel {
    
    // id
    var id: Int?
    /// 图片名称
    var imageUrl: String?
    /// 名字
    var name: String?
    /// 合作门店类型名
    var cooperationShopStr: String?
    /// 合作门店数
    var cooperationShopCount: Int = 0
    
    // MARK: - SZCellModelExtentionProtocol
    override func modelExtentionItemType() -> String {
        
        return DifferentDataModelType.two.rawValue
    }
}

四、如何串联起来:

首先,需要初始化Builder和Control的工厂类,把每个cell的对应Builder和Control的实现类注册进工厂:

/// 初始化 cell builder 工厂
    private func initCellBuilderFactory() {
        
        if let builders = dataSource?.cellBuildersForAutoConfig() {
            
            for builder in builders {
                
                if let b = builder as? SZCellBuilderProtocol {
                    
                    cellBuildFactory.registerCellBuilder(cellBuilder: b)
                }
            }
        }
    }
    
    /// 初始化 cell control 工厂
    private func initCellControlFactory() {
        
        if let controls  = dataSource?.cellControlsForAutoConfig() {
            
            for control in controls {
                
                if let c = control as? (SZCellControlProtocol & SZBaseCellDelegate) {
                    
                    cellControlFactory.registerCellControl(cellController: c)
                }
            }
        }
    }

其次,UITableViewDataSource 和 UITableViewDelegate的渲染cell和cell点击事件中进行分发

获取cell和cell高度:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        // 保护代码,防止切换过快,导致的数组越界
        if showArray.count > 0 && indexPath.section < showArray.count {
            
            let item = showArray[indexPath.section]
            
            // 根据数据类型,获取对应的cell
            if let itemTypePtl = item as? SZCellModelExtentionProtocol,
                let cellBuilder = cellBuildFactory.getCellBuilderWithRegisterKey(registerKey: itemTypePtl.modelExtentionItemType()),
                var cell = tableView.dequeueReusableCell(withIdentifier: cellBuilder.cellReuseId()) as? SZBaseCellProtocol,
                let cellControlDelegate = cellControlFactory.getCellControlWithDataTypeRegisterKey(registerKey: itemTypePtl.modelExtentionItemType()) {
                
                cell.delegate = cellControlDelegate
                
                cell.configWithData(data: item)
                
                // 测试代码
                // TODO: 没有定义在基类的配置,即自定义配置有问题
//                cell.doSomethingConfigElse()
                
                return cell as? UITableViewCell ?? UITableViewCell()
            }
        }
        
        return UITableViewCell()
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        
        // 保护代码,防止切换过快,导致的数组越界
        if showArray.count > 0 && indexPath.section < showArray.count {
            
            let item = showArray[indexPath.section]
            
            // 根据数据类型,获取对应的cell
            if let itemTypePtl = item as? SZCellModelExtentionProtocol,
                let cellBuilder = cellBuildFactory.getCellBuilderWithRegisterKey(registerKey: itemTypePtl.modelExtentionItemType()) {
                
                return cellBuilder.cellHeightWithModel(model: item)
            }
        }
        
        return 0
    }

cell的点击事件:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        // 保护代码,防止切换过快,导致的数组越界
        if showArray.count > 0 && indexPath.section < showArray.count {
            
            let item = showArray[indexPath.section]
            
            // 根据数据类型,获取对应的cell
            if let itemTypePtl = item as? SZCellModelExtentionProtocol,
                let cellControlDelegate = cellControlFactory.getCellControlWithDataTypeRegisterKey(registerKey: itemTypePtl.modelExtentionItemType()) {
                
                cellControlDelegate.didSelectedItemAtIndexPath?(indexPath: indexPath)
            }
        }
    }

Tips:这里获取Builder和Control时,使用的就是对应的model实现上述模型转换的Protocol而获取到的唯一标识。。

具体的实现详见 DEMO:

1. SameDataModelListViewController,该类是相同数据模型不同cell的拆解实现类;
2. DifferentDataModelListViewController,该类是不同数据模型不同cell的拆解实现类;
3. EncapsulationListViewController,该类是自动配置cell封装后的使用类。

五、如何使用:

每个需要配置的cell均需要实现SZBaseCellProtocol:

/// 不同数据类型 第一种 Cell
class DifferentDataModelOneCell: UITableViewCell, SZBaseCellProtocol {
    
    // MARK: - ShopVisitBaseCellProtocol
    
    var delegate: (SZBaseCellDelegate & SZCellControlProtocol)?
    
    func configWithData(data: Any) {
        
        itemModel = data as? DifferentDataModelOneModel
        
        configData()
    }
}

目前自动配置的table支持的自定义属性不多,如果有更多需要,@本人或自行自定义。

/// 背景颜色
    var sz_backgroundColor: UIColor = UIColor.colorWithHex(hexString: "f5f5f5") {
        
        didSet {
            
            self.tableView.backgroundColor = sz_backgroundColor
        }
    }
    /// section header 高度
    var sectionHeaderHeight: CGFloat = 12
    /// section footer 高度(tableView的bug,设置0的话,不生效)
    var sectionFooterHeight: CGFloat = 0.01
    /// 是否需要首个section header
    var isNeedFirstSectionHeader: Bool = true
    /// 是否需要最后一个section footer
    var isNeedLastSectionFooter: Bool  = true

dataSource的定义,Builder和Control必须实现,其他根据需要可选:

/// 自动配置 cell view dataSource
@objc protocol SZAutoCofigCellTableViewDataSource {
    
    /// 获取自动配置的 cellBuilders
    ///
    /// - Returns: cellBuilders
    @objc func cellBuildersForAutoConfig() -> [Any]
    
    /// 获取自动配置的 cellControls
    ///
    /// - Returns: cellBuilders
    @objc func cellControlsForAutoConfig() -> [Any]
    
    /// 获取table的 tableHaderView
    ///
    /// - Returns: tableHaderView
    @objc optional func tableHeaderViewForAutoConfig() -> UIView
    
    /// 获取table的 tableFooterView
    ///
    /// - Returns: tableFooterView
    @objc optional func tableFooterViewForAutoConfig() -> UIView
    
    /// 获取table的 refreshHeader
    ///
    /// - Returns: refreshHeader
    @objc optional func refreshHeaderForAutoConfig() -> MJRefreshHeader
    
    /// 获取table的 refreshFooter
    ///
    /// - Returns: refreshFooter
    @objc optional func refreshFooterForAutoConfig() -> MJRefreshFooter
}

delegate的定义,其中方法均是可选的:

/// 自动配置 cell view delegate
@objc protocol SZAutoCofigCellTableViewDelegate {
    
    /// 列表滚动
    @objc optional func sz_autoCofigScrollViewDidScroll()
    
    /// 重新加载数据
    ///
    /// - Parameter isRefresh: 是否是刷新
    @objc optional func sz_reloadData(isRefresh: Bool)
}

将自动配置的table定义好:

/// cell自动配置view
private var autoConfigView: SZAutoCofigCellTableView = SZAutoCofigCellTableView()

并实现自动配置的table的 dataSource 和 delegate,Builder和Control的实现类,参考DEMO实现就OK:

// MARK: - SZAutoCofigCellTableViewDataSource
extension EncapsulationListViewController: SZAutoCofigCellTableViewDataSource {
    
    func cellBuildersForAutoConfig() -> [Any] {
        
        /// 需要支持的 cellBuilders
        let supportBuilders: [SZCellBuilderProtocol] = [SZEncapsulationTypeOneCellBuilder(),
                                                        SZEncapsulationTypeTwoCellBuilder(),
                                                        SZEncapsulationTypeThreeCellBuilder(),
                                                        SZEncapsulationTypeFourCellBuilder()]
        
        // 设置 dataSource
        for var builder in supportBuilders {
            
            builder.dataSource = self
        }
        
        return supportBuilders
    }
    
    func cellControlsForAutoConfig() -> [Any] {
        
        /// 需要支持的 cellControls
        let supportControls: [SZCellControlProtocol] = [SZEncapsulationTypeOneActionControl(),
                                                        SZEncapsulationTypeTwoActionControl(),
                                                        SZEncapsulationTypeThreeActionControl(),
                                                        SZEncapsulationTypeFourActionControl()]
        
        // 设置 dataSource
        for control in supportControls {
            
            control.dataSource = self
        }
        
        return supportControls
    }
    
    /*
     可选方法的测试
     */
    
//    func tableFooterViewForAutoConfig() -> UIView {
//
//        return UIView().then {
//
//            $0.frame           = CGRect(x: 0,
//                                        y: 0,
//                                        width: UIScreen.main.sz_screenWidth,
//                                        height: 30)
//            $0.backgroundColor = UIColor.red
//        }
//    }
//
//    func tableHeaderViewForAutoConfig() -> UIView {
//
//        return UIView().then {
//
//            $0.frame           = CGRect(x: 0,
//                                        y: 0,
//                                        width: UIScreen.main.sz_screenWidth,
//                                        height: 30)
//            $0.backgroundColor = UIColor.blue
//        }
//    }
    
    func refreshHeaderForAutoConfig() -> MJRefreshHeader {
        
        return MJRefreshNormalHeader()
    }
    
    func refreshFooterForAutoConfig() -> MJRefreshFooter {
        
        return MJRefreshAutoStateFooter()
    }
}

具体的使用参考 EncapsulationListViewController 的实现。

最后附上demo地址:
https://github.com/banxin/SZ_TableViewCellAutoConfig

上一篇 下一篇

猜你喜欢

热点阅读