美學(xué)的表現(xiàn)層組件化之路

在很多以內(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個版本中寞焙,每個版本的首頁都在大改唬滑。

如下我們的一個首頁版本,模塊非常明顯棺弊,并且在其他頁面也會使用到類似模塊晶密。

example1.png

用戶內(nèi)容的高自由性,大部分內(nèi)容為用戶選填模她,如果內(nèi)容缺省稻艰,需要刪掉該行,所以需要動態(tài)計(jì)算布局也是非常麻煩侈净。如下除了用戶和產(chǎn)品尊勿,都是可選內(nèi)容,不可控因素太多畜侦。

example2.png

可以看出元扔,我們的內(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)類似缺猛,這里不做說明)缨叫,源碼大家自己看吧,就不介紹了:

structure.png

上圖藍(lán)色的是視圖組件荔燎,黃色的是容器組件耻姥。Group類型為順序組合,Status組件為狀態(tài)型組合有咨。

請不要問我組件該怎么寫咏闪,和寫一個只有該模塊內(nèi)容的CollectionView一模一樣,不會請參考蘋果官方事例吧~

使用流程

structure2.png

其中紅色部分為日常開發(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ò)展性還是可以的萌衬,有疑問可以直接私密我饮醇。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市秕豫,隨后出現(xiàn)的幾起案子朴艰,更是在濱河造成了極大的恐慌,老刑警劉巖馁蒂,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呵晚,死亡現(xiàn)場離奇詭異蜘腌,居然都是意外死亡沫屡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門撮珠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沮脖,“玉大人,你說我怎么就攤上這事芯急∩捉欤” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵娶耍,是天一觀的道長免姿。 經(jīng)常有香客問我,道長榕酒,這世上最難降的妖魔是什么胚膊? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮想鹰,結(jié)果婚禮上紊婉,老公的妹妹穿的比我還像新娘。我一直安慰自己辑舷,他們只是感情好喻犁,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著何缓,像睡著了一般肢础。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上碌廓,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天乔妈,我揣著相機(jī)與錄音,去河邊找鬼氓皱。 笑死路召,一個胖子當(dāng)著我的面吹牛勃刨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播股淡,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼身隐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了唯灵?” 一聲冷哼從身側(cè)響起贾铝,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎埠帕,沒想到半個月后垢揩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡敛瓷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年叁巨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呐籽。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡锋勺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出狡蝶,到底是詐尸還是另有隱情庶橱,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布贪惹,位于F島的核電站苏章,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏奏瞬。R本人自食惡果不足惜枫绅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丝格。 院中可真熱鬧撑瞧,春花似錦、人聲如沸显蝌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽曼尊。三九已至酬诀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間骆撇,已是汗流浹背瞒御。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留神郊,地道東北人肴裙。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓趾唱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蜻懦。 傳聞我的和親對象是個殘疾皇子甜癞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,510評論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件宛乃、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,024評論 4 62
  • 島上書店 如果這個世界上真的有萬能解藥悠咱,我認(rèn)為應(yīng)該是熱水和愛。 兩天的時間讀完《島上書店》征炼,這兩天我和A.J一起經(jīng)...
    妖君兒閱讀 601評論 0 0
  • 今天我們介紹如何閱讀一本實(shí)用性的書析既。 作為實(shí)用性的書,作者一定想 告訴我們什么方法或給我們什么建議谆奥。所以第一步眼坏,找...
    子姝閱讀 113評論 0 0
  • 我吃過的鹽比你吃過的飯還多,這句話常常是“老鳥”對“菜鳥”說的話雄右。經(jīng)驗(yàn)常常能帶給我們寶貴的財(cái)富空骚,但是經(jīng)驗(yàn)的采用并不...
    楓丹白露蘇眉魚閱讀 183評論 2 1