在很多以內(nèi)容為主的應(yīng)用中,比如考拉汉买、嚴(yán)選,以及我們美學(xué)佩脊,界面內(nèi)容通常比較復(fù)雜豐富蛙粘,一個頁面通常分為多個模塊垫卤,各個模塊之間獨(dú)立性強(qiáng),這樣勢必一個controller里面會有很多很多代碼與邏輯來處理模塊組合出牧,導(dǎo)致代碼日益膨脹穴肘。
這是一個表現(xiàn)層模塊化組件,按照頁面視覺結(jié)構(gòu)舔痕,將一個頁面劃分為多個模塊评抚,然后通過模塊間的簡單組合,來實(shí)現(xiàn)復(fù)雜頁面伯复。并且將部分邏輯功能放到了對應(yīng)組件慨代,以達(dá)到復(fù)用與使用簡單的效果。
目前啸如,我們大部分展示型頁面controller只有請求相關(guān)代碼侍匙,基本能夠控制在200行左右。
背景:
由于我們的項(xiàng)目處于一個比較早期的階段叮雳,所以我們需要很多的嘗試來改良我們的方案想暗。所以在這期間的頁面結(jié)構(gòu)極不穩(wěn)定,內(nèi)容以及位置順序等帘不,都可能會發(fā)生較大變化说莫。事實(shí)上,在1.0之后的3個版本中寞焙,每個版本的首頁都在大改唬滑。
如下我們的一個首頁版本,模塊非常明顯棺弊,并且在其他頁面也會使用到類似模塊晶密。
用戶內(nèi)容的高自由性,大部分內(nèi)容為用戶選填模她,如果內(nèi)容缺省稻艰,需要刪掉該行,所以需要動態(tài)計(jì)算布局也是非常麻煩侈净。如下除了用戶和產(chǎn)品尊勿,都是可選內(nèi)容,不可控因素太多畜侦。
可以看出元扔,我們的內(nèi)容可能會達(dá)到一個非常大的級別,此時性能也會是一個問題旋膳,必須采用視圖重用才可以避免內(nèi)存問題澎语。
同時,不同模塊的加載可能是異步的,返回結(jié)果也可能不同擅羞,需要部分顯示空態(tài)尸变、錯誤等提示,這樣又進(jìn)一步導(dǎo)致了頁面的復(fù)雜性减俏。
接下來召烂,我們一個個的解決這樣的問題。
方案
布局選型與重用問題:
一種是tableView來實(shí)現(xiàn)這些類似于列表的功能娃承,另外一種是使用CollectionView來實(shí)現(xiàn)同樣的功能奏夫。
雖然分別實(shí)現(xiàn)了這兩種對應(yīng)方案,但是最終使用最多的還是CollectionView历筝,有幾個原因:
- CollectionView的布局是一次性算出來的桶蛔,會有緩存,相當(dāng)于性能優(yōu)化
- 模塊間的間距控制漫谷,CollectionView更加靈活仔雷,不需要調(diào)整cell就可以改變間距
- 可以看到我們的模塊并不一定一行只有一個元素(比如首頁),也不一定一個模塊只有固定行數(shù)(比如上圖的標(biāo)簽?zāi)K)舔示,如果使用tableView碟婆,還是會需要復(fù)雜的計(jì)算,而使用CollectionView惕稻,我們可以控制每個cell為最小的單位竖共。
組件間組合與順序問題
有需求是服務(wù)器控制組合與順序,所以這是我們首先需要解決的問題俺祠。所以這里引入兩個概念:
1公给,視圖組件: 只負(fù)責(zé)視圖展示,比如一個包含小列表的模塊蜘渣,或者僅僅只有一個元素的模塊淌铐。只負(fù)責(zé)職責(zé)內(nèi)的視圖展示。
2蔫缸,容器組件: 只負(fù)責(zé)組件間的組合腿准,比如按照順序或者空態(tài)等組合模式,當(dāng)然最頂層的一個組件也是一個容器類組件拾碌。
這里容器類組件可以包含任意視圖組件及容器類組件吐葱,而視圖組件不能作為組合使用(這里有個特例HeaderFooterSectionComponent
,其實(shí)提供了部分容器的概念校翔,可以配置header和footer弟跑,一個細(xì)化)。
職責(zé)明確之后防症,我們就可以通過這種從屬關(guān)系來任意組合我們的組件孟辑,如果不需要顯示該視圖哎甲,可以從容器組件中移除該組件或者將numberOf
返回0個。
空態(tài)頁扑浸、錯誤頁等
有了上一個的兩個概念,處理這兩個問題就變得簡單了燕偶。抽象的來說喝噪,就是組件依據(jù)不同狀態(tài),而分別展示不同的子組件指么。相當(dāng)于增加一層組件酝惧,該組件的功能是控制展示當(dāng)前子組件。
那么設(shè)計(jì)一個狀態(tài)與組件間對應(yīng)的字典伯诬,在需要的時候切換該狀態(tài)就行了晚唇,這就是后來增加的StatusComponent
。
布局的多樣化
可能有些頁面需要內(nèi)容元素需要居中顯示盗似,或者FlowLayout默認(rèn)的居左顯示(多行的時候哩陕,除最后一行外為兩端對齊模式),又或者需要永遠(yuǎn)居左顯示(比如我們的標(biāo)簽)赫舒。
當(dāng)選擇了CollectionView作為方案時悍及,這個問題就很好解決了,不需要改動component代碼接癌,只需要創(chuàng)建的時候輸入自定義的Layout就可以輕松改變布局了心赶。
實(shí)現(xiàn)
按照以上的分析結(jié)果,最終實(shí)現(xiàn)了一套組件化實(shí)現(xiàn)方案(TableView結(jié)構(gòu)類似缺猛,這里不做說明)缨叫,源碼大家自己看吧,就不介紹了:
上圖藍(lán)色的是視圖組件荔燎,黃色的是容器組件耻姥。Group類型為順序組合,Status組件為狀態(tài)型組合有咨。
請不要問我組件該怎么寫咏闪,和寫一個只有該模塊內(nèi)容的CollectionView一模一樣,不會請參考蘋果官方事例吧~
使用流程
其中紅色部分為日常開發(fā)需要真正關(guān)心的摔吏,可能需要寫代碼的部分鸽嫂,其他均由組件化解決,減少了開發(fā)一個新頁面的成本征讲。
Demo
以我們的首頁推薦為例据某,雖然我們的首頁內(nèi)容多而且復(fù)雜,但是Controller代碼也在200行左右诗箍。下面來看看一個主要流程:
- (void)viewDidLoad {
[super viewDidLoad];
// 外層容器結(jié)構(gòu)
self.sectionGroupComponent = [DDCollectionViewSectionGroupComponent new];
self.statusComponent = [MZCollectionViewStatusComponent defaultComponent];
self.statusComponent.normalComponent = self.sectionGroupComponent;
self.componentArray = @[self.statusComponent];
}
MZHomeRecommendRequest *request = [[MZHomeRecommendRequest alloc] init];
[request startWithBlock:^(MZHomeRecommendRequest *request, NSError *error) {
// 網(wǎng)絡(luò)請求回來后首先判斷狀態(tài)癣籽,來切換空態(tài)頁或者錯誤頁,其實(shí)這里還可以加入loading頁
if (!error) {
if (request.response.banners.count > 0 && request.response.groups.count > 0) {
self.statusComponent.currentState = MZCollectionViewStateNormal;
// 正常數(shù)據(jù)會根據(jù)數(shù)據(jù)來生成對應(yīng)的component
self.sectionGroupComponent.subComponents = [self componentFromData:request.response.groups];
}
else {
self.statusComponent.currentState = MZCollectionViewStateNoData;
}
}
else {
self.statusComponent.currentState = MZCollectionViewStateError;
self.statusComponent.errorComponent.title = error.localizedDescription;
self.statusComponent.errorComponent.delegate = self; // 這里點(diǎn)擊重新加載
}
[self.collectionView reloadData];
}];
再來看看單個component的結(jié)構(gòu),和一個單一元素的collectionView非常相似筷狼。
- (void)prepareCollectionView {
[super prepareCollectionView];
// 由于依賴collectionView瓶籽,所以還是需要注冊
[self.collectionView registerClass:MZRepoNormalStyleCollectionViewCell.class
forCellWithReuseIdentifier:NSStringFromClass(MZRepoNormalStyleCollectionViewCell.class)];
}
#pragma mark - UICollectionView
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.repos.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MZRepoNormalStyleCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass(MZRepoNormalStyleCollectionViewCell.class)
forIndexPath:indexPath];
// config...
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return size;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
// push detail view controller
}
如何控制組件的順序以及顯示特性呢?
[self componentFromData:request.response.groups]
埂材,在我們組裝視圖組件時塑顺,可以隨意調(diào)整組件的順序,控制組件的顯示俏险,而無需關(guān)系兄弟組件的情況严拒。
- (NSArray *)componentFromData:(NSArray *)data {
NSMutibleArray *retArray;
forEach switch (data[index].type) {
case type1:
// add to array ...
case type2:
// add to array ...
}
}
擴(kuò)展性
在一些場景下,我們需要額外的delegate方法來滿足我們的需求竖独,比如我們的居左對齊和左劃刪除裤唠,需要把這些事件傳入最終的視圖component也很簡單,只要擴(kuò)展上面幾個容器類組件的方法即可:
@protocol MZRepoAlignLeftCollectionFlowLayoutDelegate <UICollectionViewDelegateFlowLayout>
@interface DDCollectionViewSectionGroupComponent (MZRepoAlignLeftCollectionFlowLayout) <MZRepoAlignLeftCollectionFlowLayoutDelegate>
- (BOOL)collectionView:(UICollectionView *)collectionView shouldAlignLeftAtSection:(NSInteger)section {
DDCollectionViewBaseComponent *comp = [self componentAtSection:section];
if ([comp respondsToSelector:@selector(collectionView:shouldAlignLeftAtSection:)]) {
return [(id<MZRepoAlignLeftCollectionFlowLayoutDelegate>)comp collectionView:collectionView shouldAlignLeftAtSection:section];
}
return NO;
}
按照這樣的思想莹痢,就具有了高度的可擴(kuò)展性种蘸。
一個對比 Facebook ComponentsKit
Facebook
優(yōu)點(diǎn):
完全實(shí)現(xiàn)了自己的一套布局系統(tǒng),粗略的看了下,反正沒看懂(⊙﹏⊙)
能夠很好的實(shí)現(xiàn)流式布局,類似于iOS的stack绪杏,或者說更像網(wǎng)頁的flex布局(視圖重用性應(yīng)該不好)
缺點(diǎn):
完全顛覆了原生的布局方式和代碼習(xí)慣长踊,學(xué)習(xí)成本高
C++編寫而成,所以需要Objective-C++來編寫,必須承認(rèn)C++還是很難掌握的
美學(xué)
優(yōu)點(diǎn):
和原生CollectionView代碼保持一致,學(xué)習(xí)成本低
從以前代碼的轉(zhuǎn)換成本低,我們也是一步步從原來的代碼轉(zhuǎn)到組件化的
缺點(diǎn):
刷新數(shù)據(jù)需要重新計(jì)算整個Layout滔吠,此時會有性能損耗(這個要看數(shù)據(jù)量和視圖復(fù)雜度,通常發(fā)生在頁面切換挠日,請求回來的時候疮绷,其實(shí)此時用戶感知不到)
需要按照CollectionView的寫法來組建,因此部分接口需要暴露indexPath嚣潜,如果亂用冬骚,可能會導(dǎo)致崩潰
目前
到目前為止,美學(xué)大部分頁面懂算,都是采用組件化組合而成只冻,隨意數(shù)數(shù),已經(jīng)有超過100個組件了计技,接下來可能需要整理下組件喜德,增加單個組件的復(fù)用性了。
歷經(jīng)幾個版本垮媒,組件化目前已經(jīng)是比較完善和穩(wěn)定的一個版本了舍悯,也滿足了目前所有的需求和日常開發(fā)航棱,期間也接受了各種奇怪的需求,目前來看擴(kuò)展性還是可以的萌衬,有疑問可以直接私密我饮醇。