可折疊的分組列表在日常的開發(fā)中并不少見,基本原理不外乎利用tableview的
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
搭配
- (NSInteger)numberOfRowsInSection:(NSInteger)section
返回0即可讓section展示收縮的效果烹困。具體的實現(xiàn)可參考iOS UITableview實現(xiàn)展開折疊效果
最近有一個需求正好是實現(xiàn)一個可折疊的tableview玄妈,但是服務端那邊的接口遲遲不能確定,UI的設計稿卻已經(jīng)出來了,大概長這樣:
點擊這個sectionheader需要收起整個組的cell髓梅。我擦嘞拟蜻,各種小圖標紅的綠的藍的,文字還有各種狀態(tài)枯饿,置灰態(tài)酝锅、正常態(tài)等,是不是還要寫一個跑馬燈吧莘健搔扁?
該怎么辦呢?
model層的前后分離
UI層的業(yè)務邏輯和后端接口產(chǎn)生依賴是一個很不好的現(xiàn)象蟋字。如果能分離前后端的model稿蹲,代碼的可拓展性將大大加強,大概的設計邏輯長下面這樣鹊奖。
后端接口model表示根據(jù)接口字段抽象出的model,完全根據(jù)后端的接口字段確定苛聘,大概會長這樣:
@interface xxxmodel : NSObject
...
@property (nonatomic, copy) str1;
@property (nonatomic, assign) num1;
...
@end
@implementation
@end
UI層的model表示決定UI展示樣式的model。中間的adapter就是一個轉換器忠聚,將接口Model轉換成UI層的model焰盗,同常這里是充滿膠水代碼的地方,不知道讀者有沒有聽過這么一句話:什么是設計模式咒林,設計模式就是讓代碼里面所有的屎都集中在一個茅坑里熬拒,讓其他地方都干干凈凈。沒錯垫竞,如果你也沒聽過澎粟,那么這句話就是我說的- -,
在后端接口沒確定的情況下我們可以先根據(jù)設計稿長的樣式開始先著手UI層model的開發(fā)蛀序,注意這里的UI層model并不是嚴格意義上的MVVM中的viewModel。
這里可以將每一個section抽象成一個stage活烙,將section下面的每個cell抽象成一個item徐裸。
比如這里每個cell左上角的有一個小圖標,從設計稿來看它有可能是已結業(yè)啸盏、未結業(yè)重贺、已完成或者不顯示,item的model就可以這么寫:
typedef NS_ENUM(NSUInteger, CompositeSubItemStatus) {
/**已結業(yè)*/
CompositeSubItemCompleted = 0,
/**未結業(yè)*/
CompositeSubItemNotSatisfied,
/**已完成*/
CompositeSubItemSubmit,
/**無狀態(tài)*/
CompositeSubItemStatusNone
};
@interface CompositeSubItemModel : NSObject
...
/**是否結業(yè)回懦、完成狀態(tài), default statusNone*/
@property (nonatomic, assign) CompositeSubItemStatus status;
...
@end
每個cell持有一個itemModel根據(jù)itemModel中的這個字段就能判斷該怎么正確顯示气笙。以此類推cell中其他的UI對應不同的itemModel里面的字段,這里我想說明的是設計稿中一眼能看出有兩個大類的cell怯晕,一個帶body文字的潜圃,一個不帶。
這里當然可以枚舉一個字段去做對應的區(qū)分就像下面這樣:
typedef NS_ENUM(NSUInteger, CompositeSubItemCellType) {
CompositeSubItemCellContainBody = 0,
CompositeSubItemCellNoBody = 0,
};
但是最佳實踐應該是定義一個body的字符串舟茶,cell根據(jù)body是否有值去展示不同的cell(可以認為徹底貫徹數(shù)據(jù)驅動UI的理念)
@property (nonatomic, copy) NSString *body;
好谭期,現(xiàn)在你已經(jīng)根據(jù)UI的樣式抽象出itemModel和stageModel了。這樣子設計除了能讓你在不知道后端接口的情況下提前開始寫代碼之外有什么好處呢吧凉?若以后有其他的場景需要這個頁面但是接口又不完全一樣的話隧出,只需要新寫一個adapter就OK了,這塊的UI完全實現(xiàn)了和后端業(yè)務接口的解耦阀捅。
數(shù)據(jù)驅動UI
仔細看設計稿胀瞪,發(fā)現(xiàn)每一個section的最后一個cell的最底下的分割線都會隱藏,right也搓?一般的實現(xiàn)姿勢大概是這樣:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
if (indexPath.row == [tableView numberOfRowsInSection:indexPath.section]-1) {
cell.separateLine.hidden = YES;
}
return cell;
}
but,好像哪里不對勁赏廓,不是說好的數(shù)據(jù)驅動UI嗎?所以直接在vc里面去改變cell的顯示狀態(tài)是違背這個理念的傍妒。那么正確的姿勢應該是讓cell根據(jù)model的狀態(tài)自行去判斷是否應該隱藏分割線:
//CompositeSubItemModel.h
@interface CompositeSubItemModel : NSObject
...
@property (nonatomic, weak) CompositeCourseStageModel *parentStage;
...
@end
//CompositeSubItemBaseCell.m
@implementation CompositeSubItemBaseCell
- (void)setItemModel:(JYVCompositeSubItemModel *)itemModel {
...
if (_itemModel == _itemModel.parentStage.items.lastObject) {
self.separateLine.hidden = YES;
} else {
self.separateLine.hidden = NO;
}
...
- }
@end
這樣做只需要在adapter里面生成UI層model的時候賦值好parentStage這個字段就好了,但是真的需要組件的使用者在adapter里面去賦值這個字段嗎幔摸?我們回想一下cocoa里面UIView的API:
UIView *parent = [UIView new];
UIView *son = [UIView new];
[parent addSubview son];
這時候son.superview已經(jīng)能指向parent了。嗯哼颤练,所以我們要盡量保持使用的簡潔性既忆,下面才是正確的姿勢:
@implementation CompositeCourseStageModel
- (void)setStageHeader:(CompositeStageHeaderModel *)stageHeader {
_stageHeader = stageHeader;
_stageHeader.parentStage = self;
}
- (void)setItems:(NSArray<CompositeSubItemModel *> *)items {
_items = items.copy;
for (CompositeSubItemModel *item in items) {
item.parentStage = self;
}
}
@end
所以在設計組件或者框架的時候,一定要參考cocoa的API設計理念嗦玖。下面一起來實現(xiàn)這個展開收起的功能患雇,section展開收起的狀態(tài)我們用一個字段fold來保存。因為section的header在展開收起時候的UI是有不同的宇挫,所以stageModel和headerModel都得持有這個狀態(tài):
@interface CompositeCourseStageModel: NSObject
@property (nonatomic, assign) BOOL fold;
@end
@interface CompositeStageHeaderModel: NSObject
@property (nonatomic, assign) BOOL fold;
@end
oh no,明明是表示的是一個同一個stage的展開或者收起狀態(tài)苛吱,為什么需要用兩個字段?這個fold的狀態(tài)站在數(shù)據(jù)層的角度來看就是最細粒度的一個標志位器瘪,若我們在實現(xiàn)的model里面用了兩個或者多個字段來表示翠储,則我們必須維護這兩個字段的一致性绘雁,這在以后的維護上是很麻煩一件事情。所以干脆不要headerModel里面的這個字段吧援所,畢竟headerModel有一個parentStage的指針
header.parentStage.fold
這樣就解決了多個標志位帶來的麻煩庐舟。不錯,這是一種解決方案住拭,但這仍舊不是最優(yōu)雅的實現(xiàn)挪略。我還是希望用header.fold怎么辦,這樣子代碼的可讀性是最高的滔岳。
@interface CompositeStageHeaderModel : NSObject
@property (nonatomic, assign) BOOL fold;
@end
@implementation CompositeStageHeaderModel
- (BOOL)fold {
return self.parentStage.fold;
}
- (void)setFold:(BOOL)fold {
self.parentStage.fold = fold;
}
@end
header并沒有持有一個_fold的實例變量杠娱,而是將fold briding到parentStage上去,這樣底層仍舊只有stageModel持有一個_fold這個實例變量澈蟆,但是對于使用者來說墨辛,既可以訪問stage.fold也可以訪問header.fold同時不同解決多個標志位帶來的一致性問題卓研,so elegant!
說了這么多趴俘,是時候抽象出一張數(shù)據(jù)驅動UI的腦圖了:
核心是數(shù)據(jù),數(shù)據(jù)只接受網(wǎng)絡請求和用戶操作的改變奏赘,然后去改變UI寥闪,UI的改動只依賴于數(shù)據(jù)。遵照這種設計磨淌,還有一個額外的好處就是可測試性大大加強疲憋,輸入不同的數(shù)據(jù)測試UI是否正確顯示。這一點和函數(shù)式編程中"純函數(shù)式"的函數(shù)的概念不謀而合梁只,一個函數(shù)只接收一個確定的輸入產(chǎn)生一個輸出缚柳,輸入確定的話輸出就一定是可預測的。
可拓展性
突然有一天搪锣,策劃跑過來找你秋忙,開發(fā)大大我這里希望有一種cell能不展開收縮誒,比如最底下能有一個播放視頻的cell构舟。
好好思考下我們抽象出的stage和item的模型灰追,不能展開收縮的cell其實就是一個fold==NO的stage里面只有一個item,而且這個stage的header不能改變fold的狀態(tài)狗超,剩下的工作只要根據(jù)視覺稿自定義下這種類型的stage的sectionheader就好了弹澎。
突然有一天,策劃跑過來找你努咐,開發(fā)大大我這里希望有一個三級聯(lián)動的section收縮展開誒苦蒿。
對于界面UI來說,不管幾級的展開收起都可以最終轉化成section和下面的cell的個數(shù)的變化渗稍,二級的聯(lián)動的展開收起表現(xiàn)為一次只有一個section的有變化佩迟,多級聯(lián)動的展開收起是一次有多個section的變化溃肪,仔細思考下這句話,right音五?下面圖示以三級的聯(lián)動為例:
我們上面的數(shù)據(jù)結構仍舊可以保留惫撰,只需要在最外面新建一個segment的model持有一個數(shù)組的stage。在tableview生成sectionheader的時候需要判斷生成的是哪種model的header躺涝,是segment的還是stage的厨钻。每當用戶點擊發(fā)生的時候去改變對應的fold標志位的狀態(tài),然后reload坚嗜。在tableview的代理:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
//通過section計算出對應的segment和stage
...
if(isSegment) return 0;//恰好是segment最上層夯膀,只顯示一個section header
//正常的stage
if (_dataSource[segment].fold || _dataSource[segment][stage].fold) { return 0; }
return _dataSource[segment][stage].items.count;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
//通過section計算出對應的segment和stage
...
if(isSegment) return [SegmentHeaderView new];//segment必然需要展示header
if(!isSegment && !_datasource[segment].fold) return [Stageheader new];//stage的header在segment展開的狀態(tài)才展示
return nil;
}
上面貼出了偽代碼,有興趣的讀者可以自行去實現(xiàn)苍蔬。
結語
- 日常的工作不可避免的要遇到各種寫界面的需求诱建,數(shù)據(jù)驅動UI的思想能使寫出的代碼更具可拓展性和可讀性,易于單元測試碟绑。同時數(shù)據(jù)驅動UI的思路能將主要工作集中在對model層的抽象上俺猿,對我們以后寫其他的框架也是很有好處的。
- model層的抽象要注意控制model的粒度格仲,盡量減少不同字段的重復表意押袍,重讀表意的字段將為以后的維護帶來很大的麻煩。
- 接口的設計要盡量遵循cocoa的設計風格凯肋。這里面接口的設計又是有大文章可以研究谊惭,即要控制好接口的功能粒度,粒度太大的接口一次性完成了很多工作但是是去了靈活性侮东,粒度太小的接口會讓使用者失去便利性(如實現(xiàn)一個功能需要組合調(diào)用幾個接口)圈盔。同時接口設計要為以后的拓展預留出空間又要避免過度設計。設計是永遠需要鉆研的一門藝術悄雅,建議讀者多研究一些出名開源庫的源碼驱敲。
- 最后,數(shù)據(jù)驅動UI換成另外一種表達就是"UI只是數(shù)據(jù)的一種表現(xiàn)形式" (這個逼就要裝不下去了煤伟,逃癌佩。。)
- 數(shù)據(jù)驅動UI再往深處探究下就是以ReactiveCocoa為代表的函數(shù)式編程了便锨,有興趣的讀者可以參考王巍大神的這篇單向數(shù)據(jù)流動的函數(shù)式 View Controller