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;
@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方法可以这样实现:

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;
@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方法也有了通用的实现方式:

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界面开发的更多相关文章

  1. iOS界面开发

    [转载] iOS界面开发 发布于:2014-07-29 11:49阅读数:13399 iOS 8 和 OS X 10.10 中一个被强调了多次的主题就是大一统,Apple 希望通过 Hand-off ...

  2. 《Cocos2d-x实战(卷Ⅰ):C++开发》

    <Cocos2d-x实战(卷Ⅰ):C++开发>   基础篇 第1章    准备开始 1.1   本书学习路线图 1.2   使用实例代码   第2章    Cocos2d-x介绍与环境搭建 ...

  3. Android零基础入门第9节:Android应用实战,不懂代码也可以开发

    原文:Android零基础入门第9节:Android应用实战,不懂代码也可以开发 通过上一期的学习,我们成功开发了Android学习的第一个应用程序,不仅可以在Android模拟器上运行,同时还能在我 ...

  4. oc界面开发整理

    oc界面开发整理 ViewController.h from test82 #import <UIKit/UIKit.h> @interface ViewController : UIVi ...

  5. SpringSecurity权限管理系统实战—一、项目简介和开发环境准备

    目录 SpringSecurity权限管理系统实战-一.项目简介和开发环境准备 SpringSecurity权限管理系统实战-二.日志.接口文档等实现 SpringSecurity权限管理系统实战-三 ...

  6. 快速全面了解QT软件界面开发技术

    快速全面了解QT软件界面开发技术     目录 前言 一. 学习QT可能的目的是什么? 只想体验一下QT? 当前的项目选择了用QT. 为将来做QT技术储备. 二. QT的核心技术优势是什么? QT在软 ...

  7. 全球首个全流程跨平台界面开发套件,PowerUI分析

    一.       首个全流程跨平台界面开发套件,PowerUI正式发布 UIPower在DirectUI的基础上,自主研发全球首个全流程跨平台界面开发套件PowerUI(PUI)正式发布,PowerU ...

  8. HTML5界面开发工具jQuery EasyUI更新至v1.3.5

    本文转自:evget.com HTML5界面开发工具 jQuery EasyUI 最新发布v1.3.5,新版修复了多个bug,并改进了menu,tabs和slider等多个控件.jQuery Easy ...

  9. DevExpress .NET界面开发示例大全

    说到做.net界面开发,很多人应该都会想到DevExpress. 它的 .net界面开发系列一共有7个版本:WinForms.ASP.NET.MVC.WPF.Silverlight.Windows 8 ...

随机推荐

  1. collection 中对类排序

    首先 写出 一个person类 让他继承Comparable 构造函数和get/set不用说 我们要覆盖父类中的comparto方法 代码如下 省略get/set package a; public ...

  2. Intellij IDEA 插件开发之自建插件仓库

    Intellij IDEA 有一个自己的官方的插件仓库,但是当我们的开发的 Intellij IDEA 的插件不能够对外公开时,我们就需要搭建自己的 Intellij IDEA 的插件仓库.前不久我们 ...

  3. char能表示(-128~127)

    char 的取值范围是 -128 ~127 注:数0的补码表示是唯一的: +0的补码=+0的反码=+0的原码=00000000 -0的补码=11111111+1=00000000(mod 2的8次方) ...

  4. The type org.apache.http.HttpResponse cannot be resolved. It is indirectly referenced from required

    在Android 6.0(API 23)中,Google已经移除了移除了Apache HttpClient相关的类.HttpResponse类.缺失jar包使用HttpResponse等会报错: Th ...

  5. OpenMP与MPI联合编程

    研究一下如何一起使用mpi和openmp 先上程序: #include <stdio.h> #include <stdlib.h> #ifdef _OPENMP #includ ...

  6. iOS下JS与OC互相调用(一)--UIWebView 拦截URL

    最近准备把之前用UIWebView实现的JS与原生相互调用功能,用WKWebView来替换.顺便搜索整理了一下JS 与OC 交互的方式,非常之多啊.目前我已知的JS 与 OC 交互的处理方式: * 1 ...

  7. kindeditor用法简单介绍

    最近做毕业设计用了一个叫做kindeditor的文本编辑工具,相信很多人都用过,这货和fckeditor差不多,个人感觉这个的皮肤更好看,而且对中文的支持更好,没那么容易出现中文乱码问题.下次记录一下 ...

  8. 06 intent flag三种属性

    flag属性可以看做和写在清单文件中的启动模式一样 但效果有一定差别 1,FLAG_ACTIVITY_SINGLE_TOP:启动模式里的SingleTop一致  如果X启动模式设置为FLAG_ACTI ...

  9. [ExtJS5学习笔记]第四节 欢迎来到extjs5-手把手教你实现你的第一个应用

    本文地址:http://blog.csdn.net/sushengmiyan/article/details/38331347 本文作者:sushengmiyan ------------------ ...

  10. Android的AdapterView及其子类简介-android学习之旅(二十三)

    AdapterView简介 AdapterView组件是一类非常重要的组件,AdapterView本身是一根抽象基类,继承于ViewGroup,用法十分相似,只是显示形式不一样,因此同意讲解. Ada ...