你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 實戰:通過ViewModel規范TableView界面開發

實戰:通過ViewModel規范TableView界面開發

編輯:IOS開發基礎

未標題-1.jpg

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,實現時很容易出現界面邏輯混亂,代碼冗余重復的情況。

讓我們看一個例子,實現一個店鋪管理的界面 :

manager.jpg

界面包括4個sections(STORE INFO, ADVANCED SETTINGS, INCOME INFO, OTHER)和3種cells(帶icon的店鋪名稱cell,各項設置的入口cell和較高Withdraw cell)。此外,會有2種不同的用戶使用這個界面:經理和普通職員。經理可以看到上述所有信息,普通職員只能看到其中一部分,如下:

employee.jpg

按照傳統方式,直接實現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中調用這些方法即可。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved