美學的表現層組件化之路

在很多以內容為主的應用中,比如考拉、嚴選,以及我們美學,界面內容通常比較復雜豐富,一個頁面通常分為多個模塊,各個模塊之間獨立性強,這樣勢必一個controller里面會有很多很多代碼與邏輯來處理模塊組合,導致代碼日益膨脹。

這是一個表現層模塊化組件,按照頁面視覺結構,將一個頁面劃分為多個模塊,然后通過模塊間的簡單組合,來實現復雜頁面。并且將部分邏輯功能放到了對應組件,以達到復用與使用簡單的效果。

目前,我們大部分展示型頁面controller只有請求相關代碼,基本能夠控制在200行左右。

背景:

由于我們的項目處于一個比較早期的階段,所以我們需要很多的嘗試來改良我們的方案。所以在這期間的頁面結構極不穩定,內容以及位置順序等,都可能會發生較大變化。事實上,在1.0之后的3個版本中,每個版本的首頁都在大改。

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

example1.png

用戶內容的高自由性,大部分內容為用戶選填,如果內容缺省,需要刪掉該行,所以需要動態計算布局也是非常麻煩。如下除了用戶和產品,都是可選內容,不可控因素太多。

example2.png

可以看出,我們的內容可能會達到一個非常大的級別,此時性能也會是一個問題,必須采用視圖重用才可以避免內存問題。

同時,不同模塊的加載可能是異步的,返回結果也可能不同,需要部分顯示空態、錯誤等提示,這樣又進一步導致了頁面的復雜性。

接下來,我們一個個的解決這樣的問題。

方案

布局選型與重用問題:

一種是tableView來實現這些類似于列表的功能,另外一種是使用CollectionView來實現同樣的功能。

雖然分別實現了這兩種對應方案,但是最終使用最多的還是CollectionView,有幾個原因:

  • CollectionView的布局是一次性算出來的,會有緩存,相當于性能優化
  • 模塊間的間距控制,CollectionView更加靈活,不需要調整cell就可以改變間距
  • 可以看到我們的模塊并不一定一行只有一個元素(比如首頁),也不一定一個模塊只有固定行數(比如上圖的標簽模塊),如果使用tableView,還是會需要復雜的計算,而使用CollectionView,我們可以控制每個cell為最小的單位。

組件間組合與順序問題

有需求是服務器控制組合與順序,所以這是我們首先需要解決的問題。所以這里引入兩個概念:

1,視圖組件: 只負責視圖展示,比如一個包含小列表的模塊,或者僅僅只有一個元素的模塊。只負責職責內的視圖展示。
2,容器組件: 只負責組件間的組合,比如按照順序或者空態等組合模式,當然最頂層的一個組件也是一個容器類組件。

這里容器類組件可以包含任意視圖組件及容器類組件,而視圖組件不能作為組合使用(這里有個特例HeaderFooterSectionComponent,其實提供了部分容器的概念,可以配置header和footer,一個細化)。

職責明確之后,我們就可以通過這種從屬關系來任意組合我們的組件,如果不需要顯示該視圖,可以從容器組件中移除該組件或者將numberOf返回0個。

空態頁、錯誤頁等

有了上一個的兩個概念,處理這兩個問題就變得簡單了。抽象的來說,就是組件依據不同狀態,而分別展示不同的子組件。相當于增加一層組件,該組件的功能是控制展示當前子組件。

那么設計一個狀態與組件間對應的字典,在需要的時候切換該狀態就行了,這就是后來增加的StatusComponent

布局的多樣化

可能有些頁面需要內容元素需要居中顯示,或者FlowLayout默認的居左顯示(多行的時候,除最后一行外為兩端對齊模式),又或者需要永遠居左顯示(比如我們的標簽)。

當選擇了CollectionView作為方案時,這個問題就很好解決了,不需要改動component代碼,只需要創建的時候輸入自定義的Layout就可以輕松改變布局了。

實現

按照以上的分析結果,最終實現了一套組件化實現方案(TableView結構類似,這里不做說明),源碼大家自己看吧,就不介紹了:

structure.png

上圖藍色的是視圖組件,黃色的是容器組件。Group類型為順序組合,Status組件為狀態型組合。

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

使用流程

structure2.png

其中紅色部分為日常開發需要真正關心的,可能需要寫代碼的部分,其他均由組件化解決,減少了開發一個新頁面的成本。

Demo

以我們的首頁推薦為例,雖然我們的首頁內容多而且復雜,但是Controller代碼也在200行左右。下面來看看一個主要流程:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 外層容器結構
    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) {
    // 網絡請求回來后首先判斷狀態,來切換空態頁或者錯誤頁,其實這里還可以加入loading頁
    if (!error) {
            if (request.response.banners.count > 0 && request.response.groups.count > 0) {
                self.statusComponent.currentState = MZCollectionViewStateNormal;
            // 正常數據會根據數據來生成對應的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; // 這里點擊重新加載
           }
        [self.collectionView reloadData];
    }];

再來看看單個component的結構,和一個單一元素的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],在我們組裝視圖組件時,可以隨意調整組件的順序,控制組件的顯示,而無需關系兄弟組件的情況。

- (NSArray *)componentFromData:(NSArray *)data {
    NSMutibleArray *retArray;
    forEach switch (data[index].type) {
        case type1:
           // add to array ...
        case type2:
        // add to array ...
    }
}

擴展性

在一些場景下,我們需要額外的delegate方法來滿足我們的需求,比如我們的居左對齊和左劃刪除,需要把這些事件傳入最終的視圖component也很簡單,只要擴展上面幾個容器類組件的方法即可:

@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;
}

按照這樣的思想,就具有了高度的可擴展性。

一個對比 Facebook ComponentsKit

Facebook
優點:
完全實現了自己的一套布局系統,粗略的看了下,反正沒看懂(⊙﹏⊙)
能夠很好的實現流式布局,類似于iOS的stack,或者說更像網頁的flex布局(視圖重用性應該不好)
缺點:
完全顛覆了原生的布局方式和代碼習慣,學習成本高
C++編寫而成,所以需要Objective-C++來編寫,必須承認C++還是很難掌握的

美學
優點:
和原生CollectionView代碼保持一致,學習成本低
從以前代碼的轉換成本低,我們也是一步步從原來的代碼轉到組件化的
缺點:
刷新數據需要重新計算整個Layout,此時會有性能損耗(這個要看數據量和視圖復雜度,通常發生在頁面切換,請求回來的時候,其實此時用戶感知不到)
需要按照CollectionView的寫法來組建,因此部分接口需要暴露indexPath,如果亂用,可能會導致崩潰

目前

到目前為止,美學大部分頁面,都是采用組件化組合而成,隨意數數,已經有超過100個組件了,接下來可能需要整理下組件,增加單個組件的復用性了。

歷經幾個版本,組件化目前已經是比較完善和穩定的一個版本了,也滿足了目前所有的需求和日常開發,期間也接受了各種奇怪的需求,目前來看擴展性還是可以的,有疑問可以直接私密我。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,574評論 25 707
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,142評論 4 61
  • 島上書店 如果這個世界上真的有萬能解藥,我認為應該是熱水和愛。 兩天的時間讀完《島上書店》,這兩天我和A.J一起經...
    妖君兒閱讀 626評論 0 0
  • 今天我們介紹如何閱讀一本實用性的書。 作為實用性的書,作者一定想 告訴我們什么方法或給我們什么建議。所以第一步,找...
    子姝閱讀 120評論 0 0
  • 我吃過的鹽比你吃過的飯還多,這句話常常是“老鳥”對“菜鳥”說的話。經驗常常能帶給我們寶貴的財富,但是經驗的采用并不...
    楓丹白露蘇眉魚閱讀 190評論 2 1