iOS UITableView和UICollectionView實現(xiàn)多類型可刷新列表

列表是最常用的UI組件,iOS中列表分為UITableView和UICollectionView。UITableView是普通的縱向滑動列表,UICollectionView相當于前者的升級版,可以實現(xiàn)橫向滑動等復(fù)雜的布局,定義列表item的樣式等。

列表的使用相對麻煩一點,除了要操作控件,還要操作數(shù)據(jù)源,尤其當列表需要展示多種類型item時,需要在很多地方判斷類型,加很多if-else代碼。大部分的類型判斷是固定代碼,只有類型是變化的,因此可以想辦法利用泛型等特性,把這些固定代碼封裝起來,方便使用。

image.png

1.UITableView封裝

首先對UITableView封裝一下,主要代碼如下

// 獲取變量的類型,用object_getClass,不能用type(of:),
// 后者在某些情況下會失效 (release模式,放進[AnyObject]數(shù)組中的變量會被識別成AnyObject,獲取不到真正類型)
func className(_ any: Any?) -> String {
    return "\(String(describing: object_getClass(any)))"
}

// D:數(shù)據(jù)格式
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")
    }
    
    // 子類復(fù)寫
    // 定義列表中的數(shù)據(jù)類型和cell類型,數(shù)據(jù)類型要用className()包起來,以用作Map的key
    // 數(shù)據(jù)類型與cell類型一對一,或多對一
    open var dataCellDict:[AnyHashable: UITableViewCell.Type] {
        return [:]
    }
    
    // 1.注冊類型
    // 根據(jù)dataCellDict自動注冊,一般不需要復(fù)寫。除非特殊情況一個數(shù)據(jù)類型對應(yīng)多種Cell類型
    open func registerCells() {
        for cellType in dataCellDict.values {
            register(cellType, forCellReuseIdentifier: className(cellType))
        }
    }
    
    // 2.獲取數(shù)量
    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即將展示,刷新數(shù)據(jù)
    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
    }

}

數(shù)據(jù)源
數(shù)據(jù)源用一個列表保存var list: [D] = [],數(shù)據(jù)類型的泛型是AnyObject的子類D: AnyObject>。因為要支持多種類型數(shù)據(jù),所以用AnyObject;為什么不直接用AnyObject,還要加泛型呢?這是考慮到大多數(shù)列表是單類型的,使用泛型可以避免跟AnyObject之間的轉(zhuǎn)換。

cell類型
需要注冊的cell類型保存在一個dict中dataCellDict:[AnyHashable: UITableViewCell.Type],key是數(shù)據(jù)類型,value是cell的類型。因為UI是由數(shù)據(jù)驅(qū)動的,列表中大部分方法提供indexPath位置參數(shù),根據(jù)位置可以獲取對應(yīng)數(shù)據(jù)類型,有了數(shù)據(jù)類型就可以從dict中查到cell的類型了。

單類型列表
單類型列表在OneTableView基礎(chǔ)上簡單封裝一下,數(shù)據(jù)類型和cell類型作為泛型參數(shù)直接寫到類的定義上,重寫一下dataCellDict。

// 只有一種cell的簡單列表 D:數(shù)據(jù)格式,C:Cell格式
class OneSimpleTableView<D: AnyObject, C: OneTableViewCell<D>>: OneTableView<D> {
    
    override var dataCellDict: [AnyHashable : UITableViewCell.Type] {
        return [className(D.self): C.self]
    }
}

注意:數(shù)據(jù)不能直接作為dict的key,因為不是AnyHashable的,需要獲取它的類型。一般來說用swift的type(of:)方法,但這里有一個巨坑,在debug模式下它沒問題,但是release模式下往[AnyObject]中放的不同類型的數(shù)據(jù),有一定概率獲取到的類型還是AnyObject,換成oc的object_getClass()方法就沒問題,所以這有可能是swift的一個bug。

這樣用list和dataCellDict封裝后,UITableView的幾個步驟:1.注冊類型 2.獲取數(shù)量 3.獲取高度 4.獲取cell 都可以在基類中統(tǒng)一實現(xiàn)了,還剩下給cell填充數(shù)據(jù)。

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里面保存一個數(shù)據(jù)model,類型是泛型就可以了。給cell填充數(shù)據(jù)時,需要判斷這個cell是OneCollectionViewCell<D>類型的,但它的具體類型是不確定的,用is或者as操作符就沒法判斷,這是因為swift不支持泛型的不確定類型,也就是Java里面的<?>。只好想了一個有點tricky的方法,抽象一個沒有泛型的BaseOneCollectionViewCell基類出來,調(diào)用它的setAnyObject()方法填充數(shù)據(jù),然后在子類OneCollectionViewCell中進行類型轉(zhuǎn)換。

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")
    }
    
}

多類型列表
復(fù)寫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
    }
}

模擬調(diào)接口數(shù)據(jù),使用如下

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中添加空的實現(xiàn)才行,直接在子類中實現(xiàn)是不會被系統(tǒng)調(diào)用的。這應(yīng)該是swift協(xié)議與繼承的特性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容