最近公司項目首頁(不方便透露,相似天貓,京東首頁)要改版,趁着此次機會就把我對首頁進行重構的過程給紀錄下來。html
demo只是大概的模型,比較簡陋,請見諒前端
題外話ios
之前有一段的時間瘋狂的想要重寫項目中的tableView,想寫出一個萬能的tableview ,不用幾行代碼,就能實現delegate和dataSource 與項目低耦合,高內聚。中間也拜讀了行業中各位大神關於tableview 本身的想法 ,還有bestswifter(目前只找的到bestswifter大神的如何寫好一個tableview,簡書大神前兩天剛離開),固然還閱讀了好多同行的代碼。最後發現,這根本就不可能,只有最適合本身的業務的tableview,沒有適合全部業務,全部業務場景的tableview。不過蘋果給咱們的tableview 確實已經夠完美了,大部分狀況下都不須要咱們進行二次包裝了,因此就想辦法儘可能在tableview的delegate和dataSource裏面寫儘量少的代碼git
這裏只是吐槽 如今大部分公司的項目結構我相信大都仍是MVC設計模式,至於像MVVM,MVP,MVCS之類的歸根結底仍是MVC(這裏的對設計模式也不作其餘的探討),咱們公司用的就是所謂的MVP(MVC+NetManager)。github
先看個相似的效果 數據庫
要實現的視覺效果跟天貓,京東的首頁差很少。大致的就是註冊N個cell,而後從服務器請求回來數據後展現數據,cell的個數根據服務器返回的數據決定(好像都是這個套路。。。)部分cell的樣式根據服務器返回的數據決定。通常寫cell的時候都會暴露出來一個方法去接收數據,十幾個cell就要寫十幾個方法,而後在tableivew的 cellForRow方法裏面去判斷各個cell,而後進行給cell傳遞數據。這麼一套搞下來,tableview 裏面就存放了大量的冗餘代碼,還加大了tableview和cell的耦合性。若是想複用cell或者tableview(咱們項目的4大主頁面就是複用的tableview)的時候就很是噁心)swift
說到降耦合, 下降耦合經常使用的方法就是block,通知,協議代理。設計模式
block的優點有不少,不說了, 這裏說一個缺點,斷點調試不易。api
通知,通常只有跨層級訪問的時候纔會採用通知。緩存
協議,假設咱們採用協議,那麼咱們須要作的操做就是定義一個協議函數,而後讓首頁上的這些個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裏面小的邏輯我就直接寫在裏面了,可能有點不規範,不過項目裏面都是作了處理的
重構完後,個人感受就是跟着業務和服務器數據的屁股後面走,這種設計純粹是爲了迎合特定的業務和服務器數據結構。因此此次任務完成後再次驗證了那句話,沒有最好的,只有最適合的。
寫完本身讀了一遍以後,發現本身的寫做能力真是爛的本身都不能看了。。。😂
這裏寫出來,主要目的是記錄本身思考的過程,重在反思,不喜輕噴。。。