前言
可能之前的表述不是特別明確,我的方案不是靜態頁面的通用實現。我的方案針對的是類似一些設定介面之類的簡單的靜態的tableView。看到有很多人認為這樣的方案感覺實現起來會變麻煩,這個可能就是思考問題的側重點不同。我思考的側重點是後期的維護修改、應對頻繁的需求變更 歡迎交流~~
靜態的tableView
類似設定介面、個人主頁等等幾乎是每個APP都會涉及到的一個模組。我相信大家都有一些自己的套路來如何處理這類介面。寫這篇文章的目的是拋磚引玉想要和大家來交流交流。
一些常見的寫法
從具體的寫法來切入,以下是我能想到的一些寫法。
1. 山頂洞人寫法
啥都不封裝
if (indexPath.section == 0) {
if (indexPath.row == 0) {
}else if (indexPath.row == 1) {
}
}else if (indexPath.section == 1) {
if (indexPath.row == 0) {
}else if (indexPath.row == 1) {
}
}
複製程式碼
各種巢狀if
判斷indexPath.section
indexPath.row
拿到對應的cell顯示或者跳轉。這種方式可讀性差,不好擴充套件應該沒人這麼寫了吧,可能你剛學iOS開發的時候這麼寫過。
2. 純程式碼 + 列舉
用一條列舉來對應的一條cell。 資料來源用列舉陣列,或者也可以用帶有列舉屬性的物件陣列。
self.dataArray = @[@[@(settingTypeAccount)],
@[@(settingTypeMessage),@(settingTypePrivacy)],
@[@(settingTypeHelp),@(settingTypeAboutUs)]];
複製程式碼
代理方法裡可以拿到列舉直接用switch判斷
switch (type) {
case settingTypeHelp:
break;
.
.
.
default:
break;
}
複製程式碼
用switch來判斷具體具體的cell,首先可讀性相較於if判斷高了不少,並且在增加刪除cell的情況下,xcode會有提示來幫助不至於漏掉一些地方。這種方式會有比較多的重複程式碼,而且在新增、刪除、調整cell的時候不夠高效。
3. storyboard的靜態cell
首先storyboard
方式相較於純程式碼,不用跑起來就能看見介面,相對比較直觀。而且在開發速度方面也有不小的優勢。但是我從自身開發過程中的情況看來,這種方式在需求頻繁變更的情況下還是比較蛋疼的。
4. 加一層中間層
沒有什麼封裝是加一層中間層解決不了的,如果有那麼再加一層 -- 魯迅
?開個玩笑,來看看加一層怎麼樣操作。首先我認為要有一個概念,封裝在一定程度上是不會減少程式碼量的。該寫的程式碼你還是要寫的,只是合理的結構可以讓程式碼可讀性更好,可擴充套件性也更好。
@interface tableModel : NSObject
- (void)addASection:(tableSectionModel *)section;
@property (nonatomic,strong) NSMutableArray <tableSectionModel *> *sections;
.
.
.
@end
@interface tableSectionModel : NSObject
- (void)addARow:(tableRowModel *)row;
@property (nonatomic,strong) NSMutableArray <tableRowModel *> *rows;
.
.
.
@end
@interface tableRowModel : NSObject
@property (nonatomic,assign) NSInteger rowHeight;
.
.
.
@end
複製程式碼
我們一開始就已近知道了table是如何展示的,包括cell的顯示順序,cell的顯示樣式、行高等等。那麼我們可以把能夠描述一個cell的所有的資料都抽象成一個rowModel的資料,然後把能描述每個section的的所有資料抽象成一個sectionModel的資料。那麼我們只需要生成對應的sectionModel的陣列就可以來描述一個table了,然後我們在資料來源裡解析model裡面的資料完成顯示。這種方式已經相對比較合理了,但是內部還是有比較大的封裝餘地。
show you my code
我看過挺多的類似三行程式碼實現設定介面
的方案,基本都是上面第四種方法的進一步封裝,內部實現了幾種常見的cell樣式,用一個列舉來對應具體的樣式,然後給每個每個rowModel新增一個cell樣式的屬性。這樣一來通過簡單的設定rowModel的cell樣式就能拿到具體的cell。當然這樣的方式已經能夠應對大部分的情況,並且寫起介面來也是很爽了。但是我還是有幾個地方不是太滿意,需要去嘗試解決這些不滿意。
1. 新增section和row的方式
我希望新增section和row的時候這部分的程式碼是一個整體,單純- (void)addASection:(tableSectionModel *)section;
這樣的寫法在我看來還不夠整體,因為你沒辦法保證這部分的程式碼一定是寫在了一個地方。思來想去,我想到了Masonry
的寫法。
[self.view mas_makeConstraints:^(MASConstraintMaker *make) {
// do something
}];
複製程式碼
這種寫法解決了我不滿意的地方,所以最後我希望的寫法是
[self.tableView zhn_addSection:^(ZHNStaticTableSection *section) {
[section zhn_addRow:^(ZHNStaticTableRow *row) {
}];
.
.
.
}];
複製程式碼
2.dataSource
delegate
重複程式碼的問題
我們清楚dataSource
和delegate
是一對一的,所以代理方法和資料來源方法肯定是隻能寫在一個地方。你可能會說那麼我們再加一層Manger來管理sectionModel的陣列,然後把tableView
的資料來源和代理設定為manager,然後在manager內部實現dataSource
和delegate
解析sectionModel的陣列展示介面。這樣一來我們只需要配置sectionModel就可以了。但是這樣做那麼萬一我們在控制器上想要監聽tableView
的滑動呢?思來想去,我最後嘗試用訊息轉發 + 斷言
的方式來嘗試解決這個問題。
訊息轉發實現代理一對多
代理是一對一的,通知是一對多的。
剛開始學iOS的時候,我們肯定都聽過這樣一句話,來描述代理和通知的不同。但是其實通過訊息轉發,我們也是可以來實現代理的一對多的。
如果對訊息轉發沒啥概念的可以看看這篇部落格。簡單理解就是當呼叫方法的時候,系統通過isa指標層層查詢方法列表,找不到方法的時候,在報找不到方法之前,系統還額外提供了幾個方法提供給我們去實現這個方法。
代理一對多的主要實現的邏輯:
-
1.提供一個delegate容器,存放代理。
-
2.
- (BOOL)respondsToSelector:(SEL)aSelector
判斷代理容器裡的代理如果實現了代理方法,這個方法需要返回YES。如果返回NO,系統就判定沒有實現代理方法,那麼就不會呼叫方法,那麼也就不會有後面的一系列的流程了。 -
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
返回方法的簽名。不返回簽名後面的訊息轉發方法也不會呼叫。
-
- (void)forwardInvocation:(NSInvocation *)anInvocation
方法裡遍歷delegate容器,轉發方法。
斷言
斷言 (assertion) 在 Cocoa 開發裡一般用來在檢查輸入引數是否滿足一定條件,並對其進行“論斷”。這是一個編碼世界中的哲學問題,我們程式碼的使用者 (有可能是別的程式設計師,也有可能是未來的自己) 很難做到在不知道實現細節的情況下去對自己的輸入進行限制。大多數時候編譯器可以幫助我們進行輸入型別的檢查,但是如果程式碼需要在特定的輸入條件下才能正確執行的話,這種更細緻的條件就難以控制了。在超過邊界條件的輸入的情況下,我們的程式碼可能無法正確工作,這就需要我們在程式碼實現中進行一些額外工作。
上面這段介紹是從喵神一篇斷言tips裡的摘抄。在很多的第三方庫中你肯定也見過類似比如AFNetworking
中隨便一搜NSAssert(NO, @"State method should never be called in the actual dummy class");
類似的斷言非常常見。簡單理解當我們輸入一個不合法引數的情況的時候,程式就直接崩潰了,並且列印了斷言裡的描述。那麼我們一眼就能知道我們的輸入出問題了,並且問題出在哪裡。
這裡我們為什麼用斷言,由於我內部實現了某些資料來源和代理。那麼我們肯定不希望外部再實現這些實現過的方法。那麼我們肯定需要做一些約束,這裡用斷言顯然是最合適的。如果外部實現了實現過的方法,直接崩潰並且列印提示資訊。
3. 方便切換
類似三行程式碼實現設定介面
的方案內部定義幾種樣式的方案,如果我們想要把我們已經寫好的介面切換到這種方案下,代價相對還是比較大的。我們專案中肯定也實現了一些cell,我希望我之前的cell能無縫的接入進去。針對這種情況我在rowModel裡新增了一個cellClass屬性來指定cell,和一個displayCellHandle
block
來設定cell的一些樣式。
瞄一眼寫法
[self.tableView zhn_initializeEnvironmentWithDefaultRowHeight:44
defaultCellClass:[NormalSettingTableViewCell class]
defaultSectionHeader:nil
defaultHeaderHeight:20
defaultSectionFooter:nil
defaultFooterHeight:0
originalDelegate:self
originalDatasource:self];
[self.tableView zhn_addSection:^(ZHNStaticTableSection *section) {
[section zhn_addRow:^(ZHNStaticTableRow *row) {
row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
cell.textLabel.text = @"賬號與安全";
};
row.selectCellHandle = ^(UITableView *tableView, NSIndexPath *indexPath) {
NSLog(@"賬戶與安全");
};
}];
}];
[self.tableView zhn_addSection:^(ZHNStaticTableSection *section) {
[section zhn_addRow:^(ZHNStaticTableRow *row) {
row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
cell.textLabel.text = @"新訊息通知";
};
}];
[section zhn_addRow:^(ZHNStaticTableRow *row) {
row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
cell.textLabel.text = @"隱私";
};
}];
[section zhn_addRow:^(ZHNStaticTableRow *row) {
row.displayCellHandle = ^(UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) {
cell.textLabel.text = @"通用";
};
}];
}];
複製程式碼
總結
程式碼在這裡 github.com/zhnnnnn/ZHN…
這是我的方案,還沒來得及在實際的專案中使用過。拋磚引玉,希望大家能夠不吝賜教。我也很想知道大型知名專案裡大家都是怎麼寫這部分程式碼的。