如何寫好靜態的TableView

小和山吳彥祖發表於2018-03-27

前言

可能之前的表述不是特別明確,我的方案不是靜態頁面的通用實現。我的方案針對的是類似一些設定介面之類的簡單的靜態的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 重複程式碼的問題

我們清楚dataSourcedelegate是一對一的,所以代理方法和資料來源方法肯定是隻能寫在一個地方。你可能會說那麼我們再加一層Manger來管理sectionModel的陣列,然後把tableView的資料來源和代理設定為manager,然後在manager內部實現dataSourcedelegate解析sectionModel的陣列展示介面。這樣一來我們只需要配置sectionModel就可以了。但是這樣做那麼萬一我們在控制器上想要監聽tableView的滑動呢?思來想去,我最後嘗試用訊息轉發 + 斷言的方式來嘗試解決這個問題。

訊息轉發實現代理一對多

代理是一對一的,通知是一對多的。

剛開始學iOS的時候,我們肯定都聽過這樣一句話,來描述代理和通知的不同。但是其實通過訊息轉發,我們也是可以來實現代理的一對多的。

如何寫好靜態的TableView

如果對訊息轉發沒啥概念的可以看看這篇部落格。簡單理解就是當呼叫方法的時候,系統通過isa指標層層查詢方法列表,找不到方法的時候,在報找不到方法之前,系統還額外提供了幾個方法提供給我們去實現這個方法。

代理一對多的主要實現的邏輯:

  • 1.提供一個delegate容器,存放代理。

  • 2.- (BOOL)respondsToSelector:(SEL)aSelector判斷代理容器裡的代理如果實現了代理方法,這個方法需要返回YES。如果返回NO,系統就判定沒有實現代理方法,那麼就不會呼叫方法,那麼也就不會有後面的一系列的流程了。

    1. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector返回方法的簽名。不返回簽名後面的訊息轉發方法也不會呼叫。
    1. - (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…

這是我的方案,還沒來得及在實際的專案中使用過。拋磚引玉,希望大家能夠不吝賜教。我也很想知道大型知名專案裡大家都是怎麼寫這部分程式碼的。

相關文章