列表是最常用的UI組件,iOS中列表分為UITableView和UICollectionView。UITableView是普通的縱向滑動列表,UICollectionView相當于前者的升級版,可以實現(xiàn)橫向滑動等復(fù)雜的布局,定義列表item的樣式等。
列表的使用相對麻煩一點,除了要操作控件,還要操作數(shù)據(jù)源,尤其當列表需要展示多種類型item時,需要在很多地方判斷類型,加很多if-else代碼。大部分的類型判斷是固定代碼,只有類型是變化的,因此可以想辦法利用泛型等特性,把這些固定代碼封裝起來,方便使用。

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基本相同,不再贅述,我都放到項目里了。

6. Github
注意:如果列表要添加更多方法,如點擊事件
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)、左滑刪除等,需要在基類OneTableView中添加空的實現(xiàn)才行,直接在子類中實現(xiàn)是不會被系統(tǒng)調(diào)用的。這應(yīng)該是swift協(xié)議與繼承的特性。