业务爬坑与总结--项目首页重构的思考

841 阅读9分钟

前言

最近公司项目首页(不方便透露,类似天猫,京东首页)要改版,趁着这次机会就把我对首页进行重构的过程给纪录下来。

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里面小的逻辑我就直接写在里面了,可能有点不规范,不过项目里面都是做了处理的

重构完后,我的感觉就是跟着业务和服务器数据的屁股后面走,这种设计纯粹是为了迎合特定的业务和服务器数据结构。所以这次任务完成后再次验证了那句话,没有最好的,只有最适合的。

写完自己读了一遍之后,发现自己的写作能力真是烂的自己都不能看了。。。😂

这里写出来,主要目的是记录自己思考的过程,重在反思,不喜轻喷。。。