iOS-UITableView和UICollectionView实现多类型可刷新列表
列表是最常用的UI组件,iOS中列表分为UITableView和UICollectionView。UITableView是普通的纵向滑动列表,UICollectionView相当于前者的升级版,可以实现横向滑动等复杂的布局,定义列表item的样式等。
列表的使用相对麻烦一点,除了要操作控件,还要操作数据源,尤其当列表需要展示多种类型item时,需要在很多地方判断类型,加很多if-else代码。大部分的类型判断是固定代码,只有类型是变化的,因此可以想办法利用泛型等特性,把这些固定代码封装起来,方便使用。
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基本相同,不再赘述,我都放到项目里了。
6. Github
注意:如果列表要添加更多方法,如点击事件
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
、左滑删除等,需要在基类OneTableView中添加空的实现才行,直接在子类中实现是不会被系统调用的。这应该是swift协议与继承的特性。