实战:通过ViewModel规范TableView界面开发
TableView界面可以说是移动App中最常用的界面之一了,物品/消息列表、详情编辑、属性设置……几乎每个app都可以看到它的身影。如何优美地实现一个TableView界面,就成了iOS开发者的必备技能。
一般地,实现一个UITableView, 需要通过它的两套protocols,UITableViewDataSource和UITableViewDelegate,来指定页面内容并响应用户操作。常用的方法有:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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, 代码可能会是这样的:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
#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的接口定义如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@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;@endtypedef 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方法可以这样实现:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
@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可以这样定义:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@interface YZSTableViewModel : NSObject @property (nonatomic, strong) NSMutableArray *sectionModelArray;@endtypedef UIView * (^YZSViewRenderBlock)(NSInteger section, UITableView *tableView);@interface YZSTableViewSectionModel : NSObject@property (nonatomic, strong) NSMutableArray *cellModelArray;@property (nonatomic, strong) NSString *headerTitle;@property (nonatomic, strong) NSString *footerTitle;...@endtypedef 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方法也有了通用的实现方式:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@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,我们只需要:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
- (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界面开发的更多相关文章
- iOS界面开发
[转载] iOS界面开发 发布于:2014-07-29 11:49阅读数:13399 iOS 8 和 OS X 10.10 中一个被强调了多次的主题就是大一统,Apple 希望通过 Hand-off ...
- 《Cocos2d-x实战(卷Ⅰ):C++开发》
<Cocos2d-x实战(卷Ⅰ):C++开发> 基础篇 第1章 准备开始 1.1 本书学习路线图 1.2 使用实例代码 第2章 Cocos2d-x介绍与环境搭建 ...
- Android零基础入门第9节:Android应用实战,不懂代码也可以开发
原文:Android零基础入门第9节:Android应用实战,不懂代码也可以开发 通过上一期的学习,我们成功开发了Android学习的第一个应用程序,不仅可以在Android模拟器上运行,同时还能在我 ...
- oc界面开发整理
oc界面开发整理 ViewController.h from test82 #import <UIKit/UIKit.h> @interface ViewController : UIVi ...
- SpringSecurity权限管理系统实战—一、项目简介和开发环境准备
目录 SpringSecurity权限管理系统实战-一.项目简介和开发环境准备 SpringSecurity权限管理系统实战-二.日志.接口文档等实现 SpringSecurity权限管理系统实战-三 ...
- 快速全面了解QT软件界面开发技术
快速全面了解QT软件界面开发技术 目录 前言 一. 学习QT可能的目的是什么? 只想体验一下QT? 当前的项目选择了用QT. 为将来做QT技术储备. 二. QT的核心技术优势是什么? QT在软 ...
- 全球首个全流程跨平台界面开发套件,PowerUI分析
一. 首个全流程跨平台界面开发套件,PowerUI正式发布 UIPower在DirectUI的基础上,自主研发全球首个全流程跨平台界面开发套件PowerUI(PUI)正式发布,PowerU ...
- HTML5界面开发工具jQuery EasyUI更新至v1.3.5
本文转自:evget.com HTML5界面开发工具 jQuery EasyUI 最新发布v1.3.5,新版修复了多个bug,并改进了menu,tabs和slider等多个控件.jQuery Easy ...
- DevExpress .NET界面开发示例大全
说到做.net界面开发,很多人应该都会想到DevExpress. 它的 .net界面开发系列一共有7个版本:WinForms.ASP.NET.MVC.WPF.Silverlight.Windows 8 ...
随机推荐
- Github上的Android项目介绍之ListViewAnimation(针对listView item的侧滑菜单)(1)
demo源码,需要可以下载 1.这是一个github开源项目,先去github上面下载,github下载地址. 2.将SwipeMenuListView项目,导入,然后新建项目如果要引用,要设置为相应 ...
- springmvc注解形式的开发参数接收
springmvc基于注解的开发 注解第一个例子 1. 创建web项目 springmvc-2 2. 在springmvc的配置文件中指定注解驱动,配置扫描器 <!-- sprimgmvc 注解 ...
- Dynamics CRM2013 用户进入系统所必需的那些权限
本篇以CRM2013为例,在CRM中新建一个安全角色后该安全角色基本是空的,如果新建的安全角色作为一个账号的唯一安全角色时,那这个安全角色除了需要配置业务场景所需的权限外,是要优先具备进入CRM系统的 ...
- 安卓自定义View实现图片上传进度显示(仿QQ)
首先看下我们想要实现的效果如下图(qq聊天中发送图片时的效果): 再看下图我们实现的效果: 实现原理很简单,首先我们上传图片时需要一个进度值progress,这个不管是自己写的上传的方法还是使用第三方 ...
- 手机微博(weibo.cn)模拟登录及页面解析
package com.laudandjolynn.test; import java.io.IOException; import java.io.OutputStream; import java ...
- JDK 7中的文件操作的新特性
文件系统综述 一个文件系统在某种媒介(通常是一个或多个硬盘)上存储和组织文件.如今的大多数文件系统都是以树状结构来存储文件.在树的顶端是一个或多个根节点,在根节点一下,是文件和目录(在Windows系 ...
- Java之恋
初次见面那是一个河北的夏天风随沙散落天涯蝴蝶依旧恋着花回首走过的日子手指和键盘之间的梦想之光已恍如昨日 那年我还是一个刚踏进这个曾经只在地理课本上狂念南稻北麦,南油北花的土地那年你只是我必须要学的编程 ...
- Linux上程序调试的基石(1)--ptrace
引子: 1.在Linux系统中,进程状态除了我们所熟知的TASK_RUNNING,TASK_INTERRUPTIBLE,TASK_STOPPED等,还有一个TASK_TRACED.这表明这个进程处于什 ...
- 1051. Pop Sequence (25)
题目如下: Given a stack which can keep M numbers at most. Push N numbers in the order of 1, 2, 3, ..., N ...
- Docker教程:docker的概念及安装
http://blog.csdn.net/pipisorry/article/details/50754385 Why docker 对于运维来说,Docker提供了一种可移植的标准化部署过程,使得规 ...