前言
最近公司專案首頁(不方便透露,類似天貓,京東首頁)要改版,趁著這次機會就把我對首頁進行重構的過程給紀錄下來。
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裡面小的邏輯我就直接寫在裡面了,可能有點不規範,不過專案裡面都是做了處理的
重構完後,我的感覺就是跟著業務和伺服器資料的屁股後面走,這種設計純粹是為了迎合特定的業務和伺服器資料結構。所以這次任務完成後再次驗證了那句話,沒有最好的,只有最適合的。
寫完自己讀了一遍之後,發現自己的寫作能力真是爛的自己都不能看了。。。?
這裡寫出來,主要目的是記錄自己思考的過程,重在反思,不喜輕噴。。。