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,实现时很容易出现界面逻辑混乱,代码冗余重复的情况。在另外的几个protocol方法中,还有更多的这种if else判断,特别是tableView:cellForRowAtIndexPath:方法。
这样的实现当然是非常不规范的。可以想象,如果界面需求发生变化,调整行数或将某个cell的位置移动一下,修改成本是非常大的。问题的原因也很明显,代码中存在如此之多的hard code值和重复的逻辑,分散在了各个protocol方法中。所以解决这个问题,我们需要通过一种方法将所有这些UI逻辑集中起来。
那篇文章中的思路是极好的,但是看了Dome并不感觉有多简单,新方式下的代码还是一样负责,我个人觉得可以优化的,但是其中使用 tableViewModel封装cell的布局逻辑,将所有的布局逻辑集中起来,是真的有借鉴意义。
下面的内容是我自己的一个把DataSource和其他 Protocols 抽离出来并封装成类的尝试,内附源码,代码量有点大,但是逻辑很清晰,不想先看源码的朋友可以先看文末的“设计思路”的总结性概述后再看源码会更容易理解源码的设计。
//JWJTableViewDataSourceAndDelegate.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef void (^TableViewCellActionBlock)(NSIndexPath *indexPath,id item);
@interface JWJTableViewDataSourceAndDelegate : NSObject
<UITableViewDataSource,UITableViewDelegate>
- (id)initWithItems:(NSArray *)anItems
cellClass:(Class)acellClass
cellIdentifier:(NSString *)aCellIdentifier
CellActionBlock:(TableViewCellActionBlock)cellActionBlock;
@end
//JWJTableViewDataSourceAndDelegate.m
#import "JWJTableViewDataSourceAndDelegate.h"
#import "JWJBaseTableViewCell.h"
@interface JWJTableViewDataSourceAndDelegate ()
@property (nonatomic, strong) NSArray *items;
@property (nonatomic, copy) NSString *cellIdentifier;
@property (nonatomic, weak) Class acellClass;
@property (nonatomic, copy) TableViewCellActionBlock actionBlock;
@end
@implementation JWJTableViewDataSourceAndDelegate
- (id)init{
return nil;
}
- (id)initWithItems:(NSArray *)anItems
cellClass:(Class)acellClass
cellIdentifier:(NSString *)aCellIdentifier
CellActionBlock:(TableViewCellActionBlock)cellActionBlock
{
self = [super init];
if (self) {
self.items = anItems;
self.cellIdentifier = aCellIdentifier;
self.acellClass = acellClass;
self.actionBlock = [cellActionBlock copy];
}
return self;
}
#pragma mark UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.items.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
id model = self.items[indexPath.row];
// SDAutolayout 中的方法 推荐使用此普通简化版方法(一步设置搞定高度自适应,性能好,易用性好)
return [tableView cellHeightForIndexPath:indexPath model:model keyPath:@"model" cellClass:self.acellClass contentViewWidth:WIDTH];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
JWJBaseTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier
forIndexPath:indexPath];
if (!cell) {
cell = [[JWJBaseTableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:self.cellIdentifier];
}
id item = self.items[(NSUInteger) indexPath.row];
cell.model = item;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
id model = self.items[indexPath.row];
self.actionBlock(indexPath, model);
}
这里为了能够让子类重写,我们提供了JWJBaseTableViewCell(对UITableViewCell的简单封装), 以减少使用 JWJTableViewDataSourceAndDelegate的VC 或者 ViewManager的代码量 (省去了cellForRowAtIndexPath 中的自定义cell的布局设置。 子类cell 只需在 setModel 做赋值操作即可。)。
//JWJBaseTableViewCell.h
#import <UIKit/UIKit.h>
@interface JWJBaseTableViewCell : UITableViewCell
@property (nonatomic, strong) id model;
@end
//实例中的 子类cell
#import <UIKit/UIKit.h>
#import "JWJBaseTableViewCell.h"
#import "IndexCellModel.h"
@interface IndexTableViewCell : JWJBaseTableViewCell
@property(strong,nonatomic)UILabel *leftTextLabel;
@property(strong,nonatomic)UILabel *rightTextLabel;
@property (nonatomic, strong) IndexCellModel *model;// 重写了父类中的model属性。
@end
不过值得一提的是需要在 子cell的 .m中 @synthesize model = _model; 否则会有警告。
cell的ViewModel 以及Model的代码设置。注意区分 cel 的 ViewModel 和 Model的区别,后者只是一个类似 dto的对象,而前者是为 View页面展示提供最终可拿来即用数据的,中间有可能有很多转化逻辑设置的,这也是 ViewModel的本质意义所在。
//IndexCellModel.h
#import <Foundation/Foundation.h>
@interface IndexModel : NSObject
@property(copy,nonatomic)NSString *str1;
@property(copy,nonatomic)NSString *str2;
@property(copy,nonatomic)NSString *method;
@end
@interface IndexCellModel : NSObject //这是一层 ViewModel 是对cell展示的数据的转化层
- (instancetype)initWithModel:(IndexModel *)model;
@property (strong, nonatomic) IndexModel * model;
@property (copy, nonatomic) NSString * titleText;
@property (copy, nonatomic) NSString * subTitleText;
@property (assign, nonatomic) SEL methodName;
@end
//IndexCellModel.m
#import "IndexCellModel.h"
@implementation IndexModel
@end
@implementation IndexCellModel
- (instancetype)initWithModel:(IndexModel *)model;
{
self = [super init];
if (self) {
self.model = model;
self.titleText = model.str1;
self.subTitleText = model.str2;
self.methodName = NSSelectorFromString(model.method);
}
return self;
}
@end
UITableView的ViewModel 的代码设置。这里也是UItableView的数据源。
#import <Foundation/Foundation.h>
#import "IndexCellModel.h"
@interface IndexViewModel : NSObject
- (NSArray *)backTableViewModel;
@end
#import "IndexViewModel.h"
@implementation IndexViewModel
- (NSArray *)backTableViewModel;
{
NSMutableArray *dataArray = [[NSMutableArray alloc]initWithCapacity:0];
IndexModel *model = [IndexModel new];
model.str1 = @"测试1";
model.str2 = @"测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试1测试";
model.method = @"cellOneClick";
IndexCellModel *cellmodel = [[IndexCellModel alloc]initWithModel:model];
[dataArray addObject:cellmodel];
IndexModel *model1 = [IndexModel new];
model1.str1 = @"测试2";
model1.str2 = @"测试2测试2测试2测试2测试2";
model1.method = @"cellTwoClick";
IndexCellModel *cellmodel1 = [[IndexCellModel alloc]initWithModel:model1];
[dataArray addObject:cellmodel1];
return dataArray;
}
UITableViewController中的调用
#import "IndexViewController.h"
#import "JWJTableViewDataSourceAndDelegate.h"
#import "IndexTableViewCell.h"
#include "IndexViewModel.h"
static NSString * const PhotoCellIdentifier = @"PhotoCell";
@interface IndexViewController ()
@property (nonatomic, strong) JWJTableViewDataSourceAndDelegate *tbDataSource;
@property (nonatomic, strong)IndexViewModel *viewModel;
@end
@implementation IndexViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"首页";
self.view.backgroundColor = [UIColor whiteColor];
TableViewCellActionBlock actionCell = ^(NSIndexPath *path, IndexCellModel *item) {
if ([self respondsToSelector:item.methodName]) {
[self performSelector:item.methodName withObject:path afterDelay:0];
}
};
self.viewModel = [IndexViewModel new];
NSArray *cellDataArray = [self.viewModel backTableViewModel];
self.tbDataSource = [[JWJTableViewDataSourceAndDelegate alloc]initWithItems:cellDataArray
cellClass:[IndexTableViewCell class] cellIdentifier:PhotoCellIdentifier
CellActionBlock:actionCell];
self.tableView.dataSource = self.tbDataSource;
self.tableView.delegate = self.tbDataSource;
[self.tableView registerClass:[IndexTableViewCell class] forCellReuseIdentifier:PhotoCellIdentifier];
}
// cell 上的点击事件
- (void)cellOneClick
{
NSLog(@"%s",__FUNCTION__);
}
- (void)cellTwoClick
{
NSLog(@"%s",__FUNCTION__);
}
效果:
cell的点击事件触发打印
首先说明,我封装的这个简单的公共工具类中的例子是一种比较单一的情景,就是一种cell的情况下,提供的是一种思路,这个工具类还并不完善,不过后续可以按照这个思路继续完善下去,主要解决的问题有:
【1】抽象出来的这个类可以作为工具类,一处封装各处 tableViewController皆可使用。 【2】工具类中使用 SDAutolayout这个第三方库,解决了cell 高度自适应的问题。 【3】使用 MVVM的思想对复杂 tableViewController 做逻辑分层处理,避免大量冗余的 if else ,使整个逻辑设置非常的清晰和明朗,有利于后续代码的扩展和维护。
在使用该工具类的时候,开发者只需要:
【1】 创建 IndexViewModel 并在其中组装 IndexCellModel数据作为 UItableView的数据源。 【2】 创建 JWJTableViewDataSourceAndDelegate 并初始化,以及对UItableView进行绑定。 【3】创建好 cell的点击事件。
代码的架构逻辑如下:
【1】UITbaleViewController 通过 类似 MVVM的代码代码架构对功能逻辑进行分层分块管理,并继承自 BaseTableView ,这样就可以使用 父类中一些公用方法(如 刷新和公用设置的逻辑)。
【2】在 ViewManger中可以统计处理各个试图的交互事件,也是替 VC 减负的一种措施,但是本例中并没有设置这样一个 ViewManager对象,如果一旦 VC中处理 View的事件多起来后就可以增加这样一个对象了。
【2】这个分层的架构设计,难免会有对应的组装代码,这也是分层封装调用的必然结果,但是当这个模块是一个非常复杂和多变的模块时,这个代码架构是非常有利和易维护和扩展的。
【3】我们可以看到,如果后续的业务膨胀后,基本上只有两个地方的代码量会跟着增大,一个是 IndexViewModel 中 cell的Model 数据组装,一个是 IndexViewController 中cell的点击事件。我们完全可以使用category 对 IndexViewModel 和 IndexViewController 进行扩展这样依然逻辑依然是十分的清晰。
参考文章: *实战:通过ViewModel规范TableView界面开发 *JC-Hu JHCellConfig *优雅的开发TableView *更轻量的 View Controllers