TableView界面可以說是移動App中最常用的界面之一了,物品/消息列表、詳情編輯、屬性設置……幾乎每個app都可以看到它的身影。如何優美地實現一個TableView界面,就成了iOS開發者的必備技能。
一般地,實現一個UITableView, 需要通過它的兩套protocols,UITableViewDataSource和UITableViewDelegate,來指定頁面內容並響應用戶操作。常用的方法有:
@protocol UITableViewDataSource- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView; - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath - (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section; - (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section; ... @end @protocol UITableViewDelegate- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section; - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section; - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; ... @end
可見,完整地實現一個UITableView,需要在較多的方法中設定UI邏輯。TabeView結構簡單時還好,但當它相對復雜時,比如存在多種TableViewCell,實現時很容易出現界面邏輯混亂,代碼冗余重復的情況。
讓我們看一個例子,實現一個店鋪管理的界面 :
界面包括4個sections(STORE INFO, ADVANCED SETTINGS, INCOME INFO, OTHER)和3種cells(帶icon的店鋪名稱cell,各項設置的入口cell和較高Withdraw cell)。此外,會有2種不同的用戶使用這個界面:經理和普通職員。經理可以看到上述所有信息,普通職員只能看到其中一部分,如下:
按照傳統方式,直接實現UITableViewDataSource和UITableViewDelegate, 代碼可能會是這樣的:
#pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { switch (self.type) { case MemberTypeEmployee: return 3; break; case MemberTypeManager: return 4; break; default: return 3; break; } } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section == 0) { if (self.type == MemberTypeEmployee) { return 1; // only store info } else { return 2; // store info and goods entry } } else if (section == 1) { if (self.type == MemberTypeEmployee) { return 2; // order list } else { return 3; // advanced settings... } } else if (section == 2) { if (self.type == MemberTypeEmployee) { return 1; // about } else { return 3; // store income and withdraw } } else { return 1; // about } } ...
在另外的幾個protocol方法中,還有更多的這種if else判斷,特別是tableView:cellForRowAtIndexPath:方法。具體代碼可以參看Github項目中的BadTableViewController中的實現。
這樣的實現當然是非常不規范的。可以想象,如果界面需求發生變化,調整行數或將某個cell的位置移動一下,修改成本是非常大的。問題的原因也很明顯,代碼中存在如此之多的hard code值和重復的邏輯,分散在了各個protocol方法中。所以解決這個問題,我們需要通過一種方法將所有這些UI邏輯集中起來。
如果你知道MVVM模式的話,你肯定會想到通過一個ViewModel來持有所有的界面數據及邏輯。比如通過一個Array持有所有section信息, 其中每個section對象持有需要用到的sectionTitle及其cellArray。同樣,cellArray中的每個cell對象持有cell的高度,顯示等信息。ViewModel的接口定義如下:
@interface TableViewModel:NSObject @property (nonatomic, strong) NSMutableArray *sectionModelArray; @end @interface TableViewSectionModel : NSObject @property (nonatomic, strong) NSMutableArray *cellModelArray; @property (nonatomic, strong) NSString *headerTitle; @property (nonatomic, strong) NSString *footerTitle; @end typedef NS_ENUM(NSInteger, CellType) { CellTypeIconText, CellTypeBigText, CellTypeDesc }; @interface TableViewCellModel : NSObject @property (nonatomic, assign) CGFloat height; @property (nonatomic, assign) CGFloat cellType; @property (nonatomic, retain) UIImage *icon; @property (nonatomic, retain) NSString *mainTitle; @property (nonatomic, retain) NSString *subTitle; @end
這時,UITableView的那些protocol方法可以這樣實現:
@implementation TableViewModel - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.sectionModelArray.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { YZSTableViewSectionModel *sectionModel = self.sectionModelArray[section]; return sectionModel.cellModelArray.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { YZSTableViewCellModel *cellModel = [self cellModelAtIndexPath:indexPath]; UITableViewCell *cell = nil; switch (cell.cellType) { case CellTypeIconText: { ... break; } case CellTypeBigText: { ... break; } case CellTypeDesc: { ... break; } } return cell; } ... @end
在TableViewController中,我們只需要構造TableViewModel的sectionModelArray就可以了。這樣的實現無疑進步了很多,所有UI邏輯集中到了一處,基本消除了hard code值及重復代碼。代碼可讀性大大增強,維護和擴展難度大大降低。
但同時我們也發現了一個問題,這個TableViewModel是不可重用的。它的屬性設置決定了它只能用於例子中的店鋪管理界面。如果我們需要另外實現一個詳情編輯頁面,就需要創建另一個TableViewModel. 這就導致使用上的不易和推廣難度的增加。特別是在團隊中,我們需要對每個成員進行規范方式的培訓和代碼實現的review,才能保證沒有不規范的實現方式,成本較高。
如何讓TableViewMode通用起來呢?我們發現上述例子中,造成不通用的原因主要是TableViewCellModel的定義。一些業務邏輯耦合進了cell model,如cellType,icon, mainTitle, subTitle。 並不是所有的界面都有這些元素的。所以我們需要通過一種通用的描述方式來取代上述屬性。
上述屬性主要是用來實現UITableViewCell的,有什麼辦法可以不指定這些內容,同時讓TableViewModel知道如何實現一個cell呢?我們可以用block!
通過block,我們可以把UITableViewCell的實現邏輯封裝起來. 在需要時,執行這個block就可以得到對應的cell對象。
同理,cell的點擊響應,willDisplay等事件,都可以通過block的方式進行封裝。於是一個通用的TableViewModel可以這樣定義:
@interface YZSTableViewModel : NSObject @property (nonatomic, strong) NSMutableArray *sectionModelArray; @end typedef UIView * (^YZSViewRenderBlock)(NSInteger section, UITableView *tableView); @interface YZSTableViewSectionModel : NSObject @property (nonatomic, strong) NSMutableArray *cellModelArray; @property (nonatomic, strong) NSString *headerTitle; @property (nonatomic, strong) NSString *footerTitle; ... @end typedef UITableViewCell * (^YZSCellRenderBlock)(NSIndexPath *indexPath, UITableView *tableView); typedef void (^YZSCellSelectionBlock)(NSIndexPath *indexPath, UITableView *tableView); ... @interface YZSTableViewCellModel : NSObject @property (nonatomic, copy) YZSCellRenderBlock renderBlock; @property (nonatomic, copy) YZSCellSelectionBlock selectionBlock; @property (nonatomic, assign) CGFloat height; ... @end
(篇幅原因,僅列出了部分接口,更多內容可以參看:https://github.com/youzan/SigmaTableViewModel)
UITableView的那些protocol方法也有了通用的實現方式:
@implementation YZSTableViewModel ... #pragma mark - UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { YZSTableViewSectionModel *sectionModel = [self sectionModelAtSection:section]; return sectionModel.cellModelArray.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { YZSTableViewCellModel *cellModel = [self cellModelAtIndexPath:indexPath]; UITableViewCell *cell = nil; YZSCellRenderBlock renderBlock = cellModel.renderBlock; if (renderBlock) { cell = renderBlock(indexPath, tableView); } return cell; } ... #pragma mark - UITableViewDelegate ... - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { YZSTableViewCellModel *cellModel = [self cellModelAtIndexPath:indexPath]; YZSCellSelectionBlock selectionBlock = cellModel.selectionBlock; if (selectionBlock) { selectionBlock(indexPath, tableView); } } ... @end
讓我們回到文章開始的例子,實現這個相對復雜的店鋪管理頁面。通過SigmaTableViewModel,我們只需要:
- (void)viewDidLoad { [super viewDidLoad]; self.viewModel = [[YZSTableViewModel alloc] init]; self.tableView.delegate = self.viewModel; self.tableView.dataSource = self.viewModel; [self initViewModel]; [self.tableView reloadData]; } - (void)initViewModel { [self.viewModel.sectionModelArray removeAllObjects]; [self.viewModel.sectionModelArray addObject:[self storeInfoSection]]; if (self.type == MemberTypeManager) { [self.viewModel.sectionModelArray addObject:[self advancedSettinsSection]]; } [self.viewModel.sectionModelArray addObject:[self incomeInfoSection]]; [self.viewModel.sectionModelArray addObject:[self otherSection]]; } - (YZSTableViewSectionModel*)storeInfoSection { YZSTableViewSectionModel *sectionModel = [[YZSTableViewSectionModel alloc] init]; ... // store info cell YZSTableViewCellModel *cellModel = [[YZSTableViewCellModel alloc] init]; [sectionModel.cellModelArray addObject:cellModel]; cellModel.height = 80; cellModel.renderBlock = ^UITableViewCell *(NSIndexPath *indexPath, UITableView *tableView) { ... }; if (self.type == MemberTypeManager) { // product list cell YZSTableViewCellModel *cellModel = [[YZSTableViewCellModel alloc] init]; [sectionModel.cellModelArray addObject:cellModel]; cellModel.renderBlock = ^UITableViewCell *(NSIndexPath *indexPath, UITableView *tableView) { ... }; cellModel.selectionBlock = ^(NSIndexPath *indexPath, UITableView *tableView) { [tableView deselectRowAtIndexPath:indexPath animated:YES]; ... }; } return sectionModel; } ...
所有的TableView界面實現,都統一成了初始化SigmaTableViewModel的過程。
注:SigmaTableViewModel僅提供了一些常用的TableiVew protocol方法的實現。如果需要其未實現的方法,可以創建它的子類,在子類中提供對應方法的實現。同時因為block的大量使用,需要注意通過weak-strong dance避免循環引用。如果擔心block中持有過多代碼造成內存的增加,可以將代碼實現在另外的方法中,在block中調用這些方法即可。