前言
最近公司项目首页(不方便透露,类似天猫,京东首页)要改版,趁着这次机会就把我对首页进行重构的过程给纪录下来。
demo只是大概的模型,比较简陋,请见谅
以前有一段的时间疯狂的想要重写项目中的tableView,想写出一个万能的tableview ,不用几行代码,就能实现delegate和dataSource 与项目低耦合,高内聚。中间也拜读了行业中各位大神关于tableview 自己的想法 ,还有bestswifter(目前只找的到bestswifter大神的如何写好一个tableview,简书大神前两天刚离开),当然还阅读了好多同行的代码。最后发现,这根本就不可能,只有最适合自己的业务的tableview,没有适合所有业务,所有业务场景的tableview。不过苹果给我们的tableview 确实已经够完美了,大部分情况下都不需要我们进行二次包装了,所以就想办法尽量在tableview的delegate和dataSource里面写尽可能少的代码
代码部分
先看个类似的效果
要实现的视觉效果跟天猫,京东的首页差不多。大体的就是注册N个cell,然后从服务器请求回来数据后展示数据,cell的个数根据服务器返回的数据决定(好像都是这个套路。。。)部分cell的样式根据服务器返回的数据决定。一般写cell的时候都会暴露出来一个方法去接收数据,十几个cell就要写十几个方法,然后在tableivew的 cellForRow方法里面去判断各个cell,然后进行给cell传递数据。这么一套搞下来,tableview 里面就存放了大量的冗余代码,还加大了tableview和cell的耦合性。如果想复用cell或者tableview(我们项目的4大主页面就是复用的tableview)的时候就非常恶心)
说到降耦合, 降低耦合常用的方法就是block,通知,协议代理。
block的优势有很多,不说了, 这里说一个缺点,断点调试不易。
通知,一般只有跨层级访问的时候才会采用通知。
协议,假设我们采用协议,那么我们需要做的操作就是定义一个协议函数,然后让首页上的这些个cell 都遵守协议,实现协议方法即可
确定采用协议,这里制定了一个每一个cell需要遵守的协议
protocol XCellSourceProtocol {
/**
@parma coordiantor 协调者
@parma tableView 承载cell的tableview
@parma data tableview的数据源
@parma indexPath
*/
func configCell(coordiantorObj coordiantor:XCoordiantor,
xTableview tableView:XTableview,
dataSource data:[XCoordiantor],
xTableViewIndex indexPath:NSIndexPath) -> Void
}
这里为什么传了这么多的参数,把承载cell的tableview 和cell所在的indexPath,还有整个tableview的数据源都传进去(虽然有的没有用到),这里一部分是为以后做考虑,还有一部分是如果一些规模比较小的业务逻辑都可以交给cell自己去处理
确定了协议,剩下的就是怎样避免在cellForRow里面去一个一个的判断。这里我从后台返回的数据入手,先看下后台返回数据的结构图
能看就行,不要在意这些细节😂
从接口返回的数据结构就可以看出来,数据的结构是充分考虑到前端。我们可以将不同类型的数据块模版看作不同样式的cell,每一个不同的的大数据块模版里面又有一些小的不同的数据块(暂称为大数据块为数据模版)。1 ,2 ,3 为3个不同的数据模版,后面的两个数据模版相同。 每一个数据模版对应UI上的一个cell,然后cell上的每一个展示的Item对应数据模版里面的一个小数据块(比如数据模版1里面的1)。不同的数据模版之间是唯一的,而数据模版下小的数据块之间也是唯一的(我相信大部分公司后台返回数据也应该是这样的,一般后台人员在设计数据库的时候一般都会给定一个id,方便从数据库查找)。这个时候这些个数据模版的唯一特性就成为了我们的突破口,从而把cellForRow里面的if else 给去掉。这里我的想法,就是为每一个cell引入一个协调者(后来做完后发现,这个协调者跟model很像,看来还是离不开MVC设计模式😂),然后为这个协调者也制定一个协议,协议里面都是这些cell共用的属性
@objc protocol XCoordiantorProtocol {
@objc optional var isShow:Bool {set get}
var data:[String:Any]! {set get}
var cellHeight:CGFloat {set get}
var cellIdentifier:String {get}
}
其中data表示每一个cell的数据源,协调者里面包含对应cell的标识符,和cell的高度。
其中最核心的既是cell的表示标识符问题的处理。 从上面的京东的效果图大致可以看出,其实每一个cell的样式都是不一样的,那我们去复用cell的可能性就不存在了(如果完全不同样式风格的UI样式复用同一个cell,那就要删除上面所有的控件重新创建赋值,不过这么费力不讨好的操作,我想谁也不会这么去干)。因为每一个cell不同的样式,那么就可以根据数据模版ID,去制定这个cell的标识符。然后又因为我们的首页个别cell虽然不一样但是只有不多的差别,有些cell的样式是根据后台的数据来动态制定的,而后台返回这些数据的时候又采用相同的数据模版。这个时候就要用到小数据块之间唯一的ID(不同模版之间的小数据块的ID有可能相同,相同模版之间的数据块ID不相同),去标记cell的标识符 至于为什么每一个cell都要给一个标识符,这个大家仔细想想就会明白了
经过一番总结,写出来的代码如下:
class XCoordiantor: XCoordiantorProtocol{
var cellHeight: CGFloat = 0.0
var cellIdentifier: String = ""
var isShow: Bool = false
let itemWidth = UIScreen.main.bounds.size.width/6
var data: [String : Any]! = [String:Any]() {
didSet {
guard let responseData:[String:Any] = data else { return }
let templateCode:String = responseData["templateCode"] as! String
switch templateCode {
case "1":
self.cellHeight = 80
self.cellIdentifier = "XCell1"
break
case "2":
self.cellHeight = 100 + itemWidth + 40
self.cellIdentifier = "XCell2"
break
case "3":
self.cellHeight = 120
self.cellIdentifier = "XCell3"
break
case "4":
self.cellHeight = 120
self.isShow = false
self.cellIdentifier = responseData["dataId"] as! String
break
default:
print("")
}
}
}
}
多个采用数据模版4的都会走到 case4 的分之里面,这个时候cell的标识符就用小数据ID来标记
tableview 里面不需要写很多的代码,只是做一个cell的载体
class XTableview: UITableView,UITableViewDelegate,UITableViewDataSource {
override init(frame: CGRect, style: UITableViewStyle) {
super.init(frame: frame, style: style)
baseSetting()
registerCellClass()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func baseSetting() ->Void{
self.dataSource = self
self.delegate = self
self.rowHeight = 0.0
self.sectionFooterHeight = 0.0
self.sectionHeaderHeight = 0.0
if #available(iOS 11, *) {
self.estimatedRowHeight = 0.0
self.estimatedSectionFooterHeight = 0.0
self.estimatedSectionHeaderHeight = 0.0
}
}
func registerCellClass() ->Void{
self.register(XCell1.classForCoder(), forCellReuseIdentifier: "XCell1")
self.register(XCell2.classForCoder(), forCellReuseIdentifier: "XCell2")
self.register(XCell3.classForCoder(), forCellReuseIdentifier: "XCell3")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let coor:XCoordiantor = self.dataArray[indexPath.row] as! XCoordiantor
var cell:XCellSourceProtocol? = nil
if coor.data["templateCode"] as! String == "4" {
cell = XCell4.init(style: .default, reuseIdentifier: coor.cellIdentifier)
} else {
cell = tableView.dequeueReusableCell(withIdentifier: coor.cellIdentifier, for: indexPath) as? XCellSourceProtocol
}
cell?.configCell(coordiantorObj: coor, xTableview: self, dataSource: self.dataArray as! [XCoordiantor], xTableViewIndex: indexPath as NSIndexPath)
return cell as! UITableViewCell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let coor:XCoordiantor = self.dataArray[indexPath.row] as! XCoordiantor
return coor.cellHeight
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0.01
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0.01
}
var data:[XCoordiantor] = [XCoordiantor](){
didSet {
self.dataArray.removeAllObjects()
self.dataArray.addObjects(from: data)
}
}
lazy var dataArray:NSMutableArray = {
let data = NSMutableArray()
return data
}()
}
回过头来我们看看协调者, 调者的创建可以在服务器数据下来后就异步创建它
这里事后写的demo,所以模拟网络请求。
class XNewWorkManger {
class func requestNetWork(success successHandle:@escaping ([XCoordiantor])->Void,
faile faileHandle:(String)->Void) {
/**
一般app首页的数据都比较大,为了首页的用户访问速度,大部分都不会在一个接口里面把数据返回
像天猫,京东之类的,首页上半部分基本是是不变的,下半部分是一个可刷新加载的列表页
肯定不可能放在一个接口里面,分开了之后反而方便管理 ,多接口的优势有很多,这里只是举个例子。
*/
DispatchQueue.global().async {
let path:String = Bundle.main.path(forResource: "ServerData", ofType: "plist")!
let data:NSDictionary = NSDictionary.init(contentsOfFile: path)!
let dataList:[Any] = data["one"] as! [Any]
var coorArr:[XCoordiantor] = [XCoordiantor]()
for item in 0..<dataList.count {
let coor:XCoordiantor = XCoordiantor()
coor.data = dataList[item] as! [String:Any]
coorArr.append(coor)
}
DispatchQueue.main.async(execute: {
successHandle((coorArr as [XCoordiantor]))
})
}
/**
多接口网络请求管理可以采用采用多线程的组线程来管理
let group:DispatchGroup = DispatchGroup.init()
DispatchGroup.enter()
DispatchGroup.leave()
DispatchGroup.notify()
*/
}
}
tableview 只作为cell的载体,其他的工作都没有做,包括的cell上的点击事件,每一个cell上的点击事件都交给cell自己去处理.
因为这些个cell 创建一次后都会在缓存池里面,所以如果不是用户主动刷新的话就没有必要重新更新数据,也就是cellForRow方法没有必要再执行一遍,所以每一个cell都可以处理一下
func configCell(coordiantorObj coordiantor: XCoordiantor, xTableview tableView: XTableview, dataSource data: [XCoordiantor], xTableViewIndex indexPath: NSIndexPath) {
if (self.coor != nil) && (ObjectIdentifier(self.coor!) == ObjectIdentifier(coordiantor)) { return }
self.coor = coordiantor
self.table = tableView
self.totalData = data
self.currentIndex = indexPath
self.isShow = coordiantor.isShow
let dataList:[Any] = coordiantor.data["dataList"] as! [Any]
self.remoAllViews()
self.dataArray.removeAllObjects()
self.dataArray.addObjects(from: dataList)
setupViews()
}
天猫,京东的首页,大家可以发现,其实也就是UI样式多一点,基本上就是展示图片的UI控件,其他没有复杂的业务逻辑。基本上就是点击跳转到二级页面,所以这里我将这些点击事件交给每一个Cell上的Item自己去处理(这里推荐一篇文章self-manager模式)
还有就是项目里面刷新某一个cell的时候,我没有采用reloadData系列,而是这样写的
if #available(iOS 11.0, *) {
self.table?.performBatchUpdates({
if weakSelf.isShow {
temCoor.cellHeight = 100 + CGFloat(itemWidth) + (otherNum == 0 ? 0 : itemWidth) + 40
} else {
temCoor.cellHeight = 100 + itemWidth + 60
}
}, completion: { (isFinish) in
})
} else {
self.table?.beginUpdates()
if self.isShow {
temCoor.cellHeight = 100 + CGFloat(itemWidth) + (otherNum == 0 ? 0 : itemWidth) + 40
} else {
temCoor.cellHeight = 100 + itemWidth + 60
}
self.table?.endUpdates()
}
weakSelf.showImageHandle(lineNumber: lineNum, otherNumber: otherNum)
这个方法 不会调用cellForRow方法,而是调用的heightForRow方法,避免不必要的性能消耗
demo里面只写了一个cell里面的处理,其他的都省略掉了,cell里面小的逻辑我就直接写在里面了,可能有点不规范,不过项目里面都是做了处理的
重构完后,我的感觉就是跟着业务和服务器数据的屁股后面走,这种设计纯粹是为了迎合特定的业务和服务器数据结构。所以这次任务完成后再次验证了那句话,没有最好的,只有最适合的。
写完自己读了一遍之后,发现自己的写作能力真是烂的自己都不能看了。。。😂
这里写出来,主要目的是记录自己思考的过程,重在反思,不喜轻喷。。。