神刀安全网

实战:通过ViewModel规范TableView界面开发

实战:通过ViewModel规范TableView界面开发

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,实现时很容易出现界面逻辑混乱,代码冗余重复的情况。

让我们看一个例子,实现一个店铺管理的界面 :

实战:通过ViewModel规范TableView界面开发

界面包括4个sections(STORE INFO, ADVANCED SETTINGS, INCOME INFO, OTHER)和3种cells(带icon的店铺名称cell,各项设置的入口cell和较高Withdraw cell)。此外,会有2种不同的用户使用这个界面:经理和普通职员。经理可以看到上述所有信息,普通职员只能看到其中一部分,如下:

实战:通过ViewModel规范TableView界面开发

按照传统方式,直接实现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中调用这些方法即可。

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » 实战:通过ViewModel规范TableView界面开发

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址