当一个界面的TableView/CollectionView的数据是写在本地的,比如"个人中心", "设置"等,如图:
通常情况:我们要在tableview的cellforrow和didselect方法里利用indexPath来做操作,有的同学甚至会在cell里对cell上的图片和文字进行判断来进行匹配
弊端:两个方法里利进行了大量的判断,而且在后期维护的时候,比如分组改变,则要修改数据源以及上面两个方法中的判断条件,代码读改都比较繁琐
于是有了新的思路,将数据源与Cell进行绑定,通俗点来说就是数据源model里包含cell上的一切属性,通过这些属性对cell上的view控件进行赋值和逻辑判断来展示cell,包括对应的点击事件, 代码结构为:
观察"模拟个人中心",我们会发现cell上一共有4个控件,分别是: 图片, 标题, 更多, 版本号, 并且cell会有一个自身的点击事件. 于是model就应该有5个对应的属性:
class ZWTableViewExampleModel: NSObject {
var imgStr: String? // cell图片
var title: String? // cell标题
var content: String? // cell内容
var isMore: Bool? // 是否显示更多
/// 点击每行cell点击事件,传索引
var itemClick : ((_ indexPath: IndexPath) -> ())?
/// 将点击事件分离
class func initModel(title: String?, imgStr: String?, content: String?, isMore: Bool?) -> ZWTableViewExampleModel{
let item = ZWTableViewExampleModel()
item.imgStr = imgStr
item.title = title
item.content = content
item.isMore = isMore
return item
}
/// 将点击事件合并
class func initModel(title: String?, imgStr: String?, content: String?, isMore: Bool?, itemClick: ((_ indexPath: IndexPath) -> ())?) -> ZWTableViewExampleModel{
let item = ZWTableViewExampleModel()
item.imgStr = imgStr
item.title = title
item.content = content
item.isMore = isMore
item.itemClick = itemClick
return item
}
}
Cell的布局和赋值代码, 利用model的属性分别进行赋值和展示逻辑判断(显示"更多"还是"版本号")如下:
class ZWTableViewExampleCell: UITableViewCell {
var model: ZWTableViewExampleModel? {
didSet {
titleL.text = model?.title
contentL.text = model?.content
imgV.image = UIImage.init(named: model?.imgStr ?? "")
moreImgV.isHidden = !(model?.isMore ?? true)
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
/// 创建代码写到Cell里,尽可能减少VC的体积
class func cellWithTableView(_ tableView: UITableView) -> ZWTableViewExampleCell{
var cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(self)) as? ZWTableViewExampleCell
if cell == nil {
cell = ZWTableViewExampleCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: NSStringFromClass(self))
}
return cell!
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI(){
addSubview(imgV)
addSubview(titleL)
addSubview(contentL)
addSubview(moreImgV)
}
private lazy var imgV:UIImageView = {
let imgV = UIImageView.init(frame: CGRect.init(x: 15, y: 15, width: 20, height: 20))
return imgV
}()
private lazy var titleL:UILabel = {
let label = UILabel.init(frame: CGRect.init(x: 45, y: 0, width: 200, height: 50))
label.font = UIFont.systemFont(ofSize: 15)
return label
}()
private lazy var contentL:UILabel = {
let label = UILabel.init(frame: CGRect.init(x: UIScreen.main.bounds.width - 150 - 15, y: 0, width: 150, height: 50))
label.textAlignment = .right
label.textColor = .red
label.font = UIFont.systemFont(ofSize: 15)
return label
}()
private lazy var moreImgV:UIImageView = {
let imgV = UIImageView.init(frame: CGRect.init(x: UIScreen.main.bounds.width - 20 - 15, y: 15, width: 20, height: 20))
imgV.image = UIImage.init(named: "more")
return imgV
}()
}
控制器的代码, 如下:
class ZWTableViewExampleVC: UIViewController {
var modelData = [[ZWTableViewExampleModel]]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
title = "优雅的tableview"
view.addSubview(tableView)
setModelDataValue()
}
/// 初始化数据
private func setModelDataValue() {
/// 第0组
/// 点击事件分离写法
let model0_0 = ZWTableViewExampleModel.initModel(title: "分类", imgStr: "fenlei", content: nil, isMore: true)
model0_0.itemClick = { indexPath in
print("点击了分类")
}
/// 点击事件合并写法
let model0_1 = ZWTableViewExampleModel.initModel(title: "我的课表", imgStr: "kebiao", content: nil, isMore: true) { indexPath in
print("点击了我的课表")
}
/// 第1组
let model1_0 = ZWTableViewExampleModel.initModel(title: "我的余额", imgStr: "qianbao", content: nil, isMore: true) { indexPath in
print("点击了我的余额")
}
let model1_1 = ZWTableViewExampleModel.initModel(title: "消息", imgStr: "xiaoxi", content: nil, isMore: true) { indexPath in
print("点击了消息")
}
/// 第2组
let model2_0 = ZWTableViewExampleModel.initModel(title: "当前版本", imgStr: "lishi", content: "1.2.1", isMore: false) { indexPath in
}
/**
无点击事件也可以这样写
let model2_0 = ZWTableViewExampleModel.initModel(title: "当前版本", imgStr: "lishi", content: "lishi", isMore: false, itemClick: nil)
let model2_0 = ZWTableViewExampleModel.initModel(title: "当前版本", imgStr: "lishi", content: "lishi", isMore: false)
*/
modelData = [
[model0_0, model0_1],
[model1_0, model1_1],
[model2_0]
]
}
private lazy var tableView: UITableView = {
let tableV = UITableView.init(frame: UIScreen.main.bounds, style: .grouped)
tableV.backgroundColor = .white
tableV.delegate = self
tableV.dataSource = self
tableV.rowHeight = 50
tableV.tableFooterView = UITableViewHeaderFooterView.init()
/// iOS 15 的 UITableView又新增了一个新属性:sectionHeaderTopPadding 会给每一个section header 增加一个默认高度,当我们 使用 UITableViewStylePlain 初始化 UITableView的时候,就会发现,系统给section header增高了22像素。
/// 去除默认高度
if #available(iOS 15.0, *) {
tableV.sectionHeaderTopPadding = 0
}
return tableV
}()
}
extension ZWTableViewExampleVC: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return modelData.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return modelData[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ZWTableViewExampleCell.cellWithTableView(tableView)
cell.model = modelData[indexPath.section][indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
let model = modelData[indexPath.section][indexPath.row]
guard let click = model.itemClick else {
return
}
click(indexPath)
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = UIView.init(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 15))
view.backgroundColor = UIColor.init(red: 0.95, green: 0.95, blue: 0.95, alpha: 1)
return view
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 15
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let view = UIView.init(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 15))
return view
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0.01
}
}
总结:
我们看到在cellForRowAt方法里只是执行了创建和赋值, 在didSelectRowAt里也只是执行了获取model的点击属性,并进行回调. 以上两个方法均没有出现通常的逻辑判断过程.
思路是: 将model闭包回调,从而通过回调在model创建的方法里进行执行, 从而使每一个model与点击事件进行绑定
优点:
从创建上来说,省去了上述两个代理方法里复杂逻辑判断,
从维护上来说,后期进行重新分组,只需要改变modelData数组里数据的顺序即可, 在更改其他属性时, 也不会涉及到以前的逻辑判断, 对应的点击事件更加清晰
ps: 除了以上写法,为了更轻便,还可以将model写成元组的形式:
/// 在VC类外定义元组
typealias ExampleItem = (title: String?, imgStr: String?, content: String?, isMore: Bool?, itemClick: ((_ indexPath: IndexPath) -> Void)?)
/// 在VC类里使用
let item: ExampleItem = (title: "当前版本", imgStr: "lishi", content: "1.2.1", isMore: false, itemClick: { indexPath in
})
modelData = [[item]]
Demo: ZWModelBindAction