列表是最常用的UI组件,iOS中列表分为UITableView和UICollectionView。UITableView是普通的纵向滑动列表,UICollectionView相当于前者的升级版,可以实现横向滑动等复杂的布局,定义列表item的样式等。

列表的使用相对麻烦一点,除了要操作控件,还要操作数据源,尤其当列表需要展示多种类型item时,需要在很多地方判断类型,加很多if-else代码。大部分的类型判断是固定代码,只有类型是变化的,因此可以想办法利用泛型等特性,把这些固定代码封装起来,方便使用。

image.png

1.UITableView封装

首先对UITableView封装一下,主要代码如下

// 获取变量的类型,用object_getClass,不能用type(of:),
// 后者在某些情况下会失效 (release模式,放进[AnyObject]数组中的变量会被识别成AnyObject,获取不到真正类型)
func className(_ any: Any?) -> String {
    return "\(String(describing: object_getClass(any)))"
}

// D:数据格式
class OneTableView<D: AnyObject> : UITableView, UITableViewDelegate, UITableViewDataSource {
    
    var list: [D] = []
    
    override init(frame: CGRect, style: Style) {
        super.init(frame: frame, style: style)
        registerCells()
        self.separatorStyle = .none
        self.backgroundColor = .clear
        self.delegate = self
        self.dataSource = self
        if #available(iOS 15.0, *) {
            self.sectionHeaderTopPadding = 0
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 子类复写
    // 定义列表中的数据类型和cell类型,数据类型要用className()包起来,以用作Map的key
    // 数据类型与cell类型一对一,或多对一
    open var dataCellDict:[AnyHashable: UITableViewCell.Type] {
        return [:]
    }
    
    // 1.注册类型
    // 根据dataCellDict自动注册,一般不需要复写。除非特殊情况一个数据类型对应多种Cell类型
    open func registerCells() {
        for cellType in dataCellDict.values {
            register(cellType, forCellReuseIdentifier: className(cellType))
        }
    }
    
    // 2.获取数量
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return list.count
    }
    
    // 3.获取高度
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let data = list[indexPath.row]
        if let cellType = dataCellDict[className(data)] as? BaseOneTableViewCell.Type {
            return cellType.cellHeight
        } else {
            print("heightForRowAt cellType = nil \(data)")
        }
        return 0
    }
    
    // 4.获取cell
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let data = list[indexPath.row]
        if let cellType = dataCellDict[className(data)] {
            return dequeueReusableCell(withIdentifier: className(cellType), for: indexPath)
        } else {
            print("cellForRowAt cellType = nil \(data)")
        }
        return UITableViewCell()
    }
    
    // 5.cell即将展示,刷新数据
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        cell.selectionStyle = .none
        if let cell = cell as? BaseOneTableViewCell {
            cell.setAnyObject(model: list[indexPath.row])
        }
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // no op
    }

}

数据源
数据源用一个列表保存var list: [D] = [],数据类型的泛型是AnyObject的子类D: AnyObject>。因为要支持多种类型数据,所以用AnyObject;为什么不直接用AnyObject,还要加泛型呢?这是考虑到大多数列表是单类型的,使用泛型可以避免跟AnyObject之间的转换。

cell类型
需要注册的cell类型保存在一个dict中dataCellDict:[AnyHashable: UITableViewCell.Type],key是数据类型,value是cell的类型。因为UI是由数据驱动的,列表中大部分方法提供indexPath位置参数,根据位置可以获取对应数据类型,有了数据类型就可以从dict中查到cell的类型了。

单类型列表
单类型列表在OneTableView基础上简单封装一下,数据类型和cell类型作为泛型参数直接写到类的定义上,重写一下dataCellDict。

// 只有一种cell的简单列表 D:数据格式,C:Cell格式
class OneSimpleTableView<D: AnyObject, C: OneTableViewCell<D>>: OneTableView<D> {
    
    override var dataCellDict: [AnyHashable : UITableViewCell.Type] {
        return [className(D.self): C.self]
    }
}

注意:数据不能直接作为dict的key,因为不是AnyHashable的,需要获取它的类型。一般来说用swift的type(of:)方法,但这里有一个巨坑,在debug模式下它没问题,但是release模式下往[AnyObject]中放的不同类型的数据,有一定概率获取到的类型还是AnyObject,换成oc的object_getClass()方法就没问题,所以这有可能是swift的一个bug。

这样用list和dataCellDict封装后,UITableView的几个步骤:1.注册类型 2.获取数量 3.获取高度 4.获取cell 都可以在基类中统一实现了,还剩下给cell填充数据。

2. UICollectionViewCell的封装


class BaseOneCollectionViewCell: UICollectionViewCell {
    
    class var cellHeight: CGFloat {
        return 66
    }
    
    func setAnyObject(model: AnyObject?) {
        // no op
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        initView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    open func initView() {
        // no op
    }
}

class OneCollectionViewCell<D>: BaseOneCollectionViewCell {
    
    var model: D? {
        didSet {
            if let model = model {
                didSetModel(model: model)
            }
        }
    }
    
    override func setAnyObject(model: AnyObject?) {
        self.model = model as? D
    }
    
    open func didSetModel(model: D) {
        // no op
    }
}

一般来说,cell里面保存一个数据model,类型是泛型就可以了。给cell填充数据时,需要判断这个cell是OneCollectionViewCell类型的,但它的具体类型是不确定的,用is或者as操作符就没法判断,这是因为swift不支持泛型的不确定类型,也就是Java里面的<?>。只好想了一个有点tricky的方法,抽象一个没有泛型的BaseOneCollectionViewCell基类出来,调用它的setAnyObject()方法填充数据,然后在子类OneCollectionViewCell中进行类型转换。

3. 基本使用

简单列表
类型写到类定义中,自动注册

class MyData {
    var text: String?
    init(_ text: String) {
        self.text = text
    }
}

class MyCell: OneTableViewCell<MyData> {
    
    override func didSetModel(model: MyData) {
        self.textLabel?.text = model.text
    }
}

// MARK: 只有一种类型的简单列表
class MyTableView: OneSimpleTableView<MyData, MyCell> {
    override init(frame: CGRect, style: Style) {
        super.init(frame: frame, style: style)
        
        var res:[MyData] = []
        for i in 0...10 {
            let d = MyData("row \(i)")
            res.append(d)
        }
        self.list = res
        self.reloadData()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

多类型列表
复写dataCellDict注册多种类型

class MyMultiTableView: OneTableView<AnyObject> {
    
    override var dataCellDict: [AnyHashable : UITableViewCell.Type] {
        return [
            className(MyData.self): MyCell.self,
            className(MyData1.self): MyCell1.self,
        ]
    }

   override init(frame: CGRect, style: Style) {
        super.init(frame: frame, style: style)
        
        var res:[MyData] = []
            for i in 0...5 {
                let d = MyData("type0 \(i)")
                res.append(d)
                
                let d1 = MyData1("type1 \(i)")
                res.append(d1)
            }
        self.list = res
        self.reloadData()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

4. 下拉刷新和加载更多

使用MJRefresh库,在pod中添加 pod 'MJRefresh',封装成OneMJTableView

// 带下拉刷新和上滑加载更多功能
class OneMJTableView<D: AnyObject>: OneTableView<D> {
    
    var pageIndex = 0
    
    override init(frame: CGRect, style: Style) {
        super.init(frame: frame, style: style)
        if hasRefresh() {
            self.mj_header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(loadNewData))
        }
        if hasLoadMore() {
            self.mj_footer = MJRefreshAutoStateFooter(refreshingTarget: self, refreshingAction: #selector(loadMoreData))
            self.mj_footer?.isHidden = true
        }
        DispatchQueue.main.async {
            if self.willRequestOnInit() {
                self.mj_header?.beginRefreshing()
                self.loadNewData()
            }
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @objc func loadNewData() {
        pageIndex = 0
        doRequest()
    }
    
    @objc func loadMoreData() {
        doRequest()
    }
    
    open func hasRefresh() -> Bool {
        return true
    }
    
    open func hasLoadMore() -> Bool {
        return true
    }
    
    open func willRequestOnInit() -> Bool {
        return true
    }
    
    open func doRequest() {
        // no op
    }
    
    open func handleSuccess(list: [D]?, isNoMore: Bool) {
        guard let list = list else {
            handleFail()
            return
        }
        if pageIndex == 0 {
            self.list = list
            self.reloadData()
            pageIndex += 1
        } else {
            self.list.append(contentsOf: list)
            self.reloadData()
            pageIndex += 1
        }
        self.mj_header?.endRefreshing()
        
        self.mj_footer?.isHidden = self.list.isEmpty
        if isNoMore {
            self.mj_footer?.endRefreshingWithNoMoreData()
        } else {
            self.mj_footer?.endRefreshing()
        }
    }

    open func handleFail() {
        self.mj_header?.endRefreshing()
        self.mj_footer?.endRefreshingWithNoMoreData()
        self.mj_footer?.isHidden = self.list.isEmpty
    }
}

模拟调接口数据,使用如下

class MyMultiTableView: OneMJTableView<AnyObject> {
    
    override var dataCellDict: [AnyHashable : UITableViewCell.Type] {
        return [
            className(MyData.self): MyCell.self,
            className(MyData1.self): MyCell1.self,
        ]
    }
    
    override func doRequest() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            var res:[AnyObject] = []
            for i in 0...5 {
                let d = MyData("type0 \(i)")
                res.append(d)
                
                let d1 = MyData1("type1 \(i)")
                res.append(d1)
            }
            self.handleSuccess(list: res, isNoMore: self.pageIndex > 2)
        }
    }
}

5. UICollectionView

UICollectionView封装和用法与UITableView基本相同,不再赘述,我都放到项目里了。
截屏2021-12-10 下午3.11.23.png

6. Github

Github地址

注意:如果列表要添加更多方法,如点击事件func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)、左滑删除等,需要在基类OneTableView中添加空的实现才行,直接在子类中实现是不会被系统调用的。这应该是swift协议与继承的特性。

版权声明:本文为rome753原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/rome753/p/16491405.html