如何在复杂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,实现时很容易出现界面逻辑混乱,代码冗余重复的情况。在另外的几个protocol方法中,还有更多的这种if else判断,特别是tableView:cellForRowAtIndexPath:方法。

这样的实现当然是非常不规范的。可以想象,如果界面需求发生变化,调整行数或将某个cell的位置移动一下,修改成本是非常大的。问题的原因也很明显,代码中存在如此之多的hard code值和重复的逻辑,分散在了各个protocol方法中。所以解决这个问题,我们需要通过一种方法将所有这些UI逻辑集中起来。

因为接手项目的后续开发者不是看不懂其中的语法或者代码,他有可能看不懂的是其中的逻辑。

那篇文章中的思路是极好的,但是看了Dome并不感觉有多简单,新方式下的代码还是一样负责,我个人觉得可以优化的,但是其中使用 tableViewModel封装cell的布局逻辑,将所有的布局逻辑集中起来,是真的有借鉴意义。

下面的内容是我自己的一个把DataSource和其他 Protocols 抽离出来并封装成类的尝试,内附源码,代码量有点大,但是逻辑很清晰,不想先看源码的朋友可以先看文末的“设计思路”的总结性概述后再看源码会更容易理解源码的设计。

把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; 否则会有警告。

UITableViewController中的实例使用

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android源码框架分析

Android权限检查API checkSelfPermission失效问题为什么targetSdkVersion < 23 Context 的 checkSelfPermission失效target

80130
来自专栏向治洪

多线程下载

楼主三年磨剑(当然不是磨着一把剑),倾血奉献Android多线程下载Demo。有的人就问了“怎么写来写去还是Demo?”,因为老哥我实在太忙了, 每天写一点...

20580
来自专栏海天一树

小朋友学C++(21):命名空间

这里的第一行,#include好理解,iostream是输入输出流,包含了输入流istream和输出流ostream。 第二行using namespace s...

13040
来自专栏小怪聊职场

爬虫课堂(二十六)|使用scrapy-redis框架实现分布式爬虫(1)

40950
来自专栏散尽浮华

CentOS 6下gcc升级的操作记录(由默认的4.4.7升级到6.4.0版本)

机房一台centos6.9机器部署了jenkins发布系统,开发人员在用node编译js,发现依赖的gcc版本低了,故需要将gcc升级到高版本(至少5.0版本以...

30620
来自专栏移动端周边技术扩展

iOS打开系统功能对应的URL

19930
来自专栏蜉蝣禅修之道

iOS开发之CFHttpMessageRef的那些坑

41960
来自专栏cs

爬虫练习--草稿

34140
来自专栏JackieZheng

Gephi可视化(一)——使用Gephi Toolkit创建Gephi应用

  在Prefuse上摸打滚爬了一段时间,发现其和蔼可亲,容易上手。但是每每在打开gephi,导入数据再运行时,总还是在心里暗自赞叹gephi的绚烂之极,无与匹...

43070
来自专栏技术小讲堂

LINQ to SQL(2):生成对象模型

在LINQ to SQL中,可以使用自己的编程语言的对象模型映射到关系数据库,在上一节课,已经有一部分内容,简单的介绍了一下这种对象模型的结构,这一节,我们主要...

29840

扫码关注云+社区

领取腾讯云代金券