美文网首页
iOS UITableView和UICollectionView

iOS UITableView和UICollectionView

作者: rome753 | 来源:发表于2021-12-10 15:19 被阅读0次

    列表是最常用的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<D>类型的,但它的具体类型是不确定的,用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协议与继承的特性。

    相关文章

      网友评论

          本文标题:iOS UITableView和UICollectionView

          本文链接:https://www.haomeiwen.com/subject/kqabtrtx.html