業務爬坑與總結–專案首頁重構的思考

Swift_Xu發表於2019-02-17

前言

最近公司專案首頁(不方便透露,類似天貓,京東首頁)要改版,趁著這次機會就把我對首頁進行重構的過程給紀錄下來。

demo只是大概的模型,比較簡陋,請見諒

題外話

以前有一段的時間瘋狂的想要重寫專案中的tableView,想寫出一個萬能的tableview ,不用幾行程式碼,就能實現delegate和dataSource 與專案低耦合,高內聚。中間也拜讀了行業中各位大神關於tableview 自己的想法 ,還有bestswifter(目前只找的到bestswifter大神的如何寫好一個tableview,簡書大神前兩天剛離開),當然還閱讀了好多同行的程式碼。最後發現,這根本就不可能,只有最適合自己的業務的tableview,沒有適合所有業務,所有業務場景的tableview。不過蘋果給我們的tableview 確實已經夠完美了,大部分情況下都不需要我們進行二次包裝了,所以就想辦法儘量在tableview的delegate和dataSource裡面寫儘可能少的程式碼

這裡只是吐槽
現在大部分公司的專案結構我相信大都還是MVC設計模式,至於像MVVM,MVP,MVCS之類的歸根結底還是MVC(這裡的對設計模式也不做其他的探討),我們公司用的就是所謂的MVP(MVC+NetManager)。

程式碼部分

先看個類似的效果

業務爬坑與總結–專案首頁重構的思考

要實現的視覺效果跟天貓,京東的首頁差不多。大體的就是註冊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裡面小的邏輯我就直接寫在裡面了,可能有點不規範,不過專案裡面都是做了處理的

重構完後,我的感覺就是跟著業務和伺服器資料的屁股後面走,這種設計純粹是為了迎合特定的業務和伺服器資料結構。所以這次任務完成後再次驗證了那句話,沒有最好的,只有最適合的。

寫完自己讀了一遍之後,發現自己的寫作能力真是爛的自己都不能看了。。。?

這裡寫出來,主要目的是記錄自己思考的過程,重在反思,不喜輕噴。。。

相關文章