前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何在复杂TableView界面开发中变得优雅

如何在复杂TableView界面开发中变得优雅

作者头像
進无尽
发布2018-09-12 16:39:34
1.1K0
发布2018-09-12 16:39:34
举报
文章被收录于专栏:進无尽的文章進无尽的文章

前言

代码语言:javascript
复制
TableView界面可以说是移动App中最常用的界面之一了,物品/消息列表、详情编辑、属性设置……
几乎每个app都可以看到它的身影,如果不做分层处理,眉毛胡子一把抓,最后的扩展和维护简直是个噩梦,尤其是大型负责的模块页面。
所以如何优美地实现一个TableView界面,就成了iOS开发者的必备技能。

没有一套代码模式,就会使代码阅读者心里充满了不可知,无分类,无规律可循,杂乱的感觉,
`同时代码组织模式也是一种规范,有助于项目源码的阅读和管理。`

问题场景

下面的论述引用自这篇文章中举的例子,本文部分语句和代码取自该文,在此感谢作者。

代码语言:javascript
复制
一般地,实现一个UITableView, 需要通过它的两套protocols,UITableViewDataSource和UITableViewDelegate,
来指定页面内容并响应用户操作。常用的方法有:
代码语言:javascript
复制
@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 抽离出来并封装成类

代码语言:javascript
复制
 //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 做赋值操作即可。)。

代码语言:javascript
复制
//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的本质意义所在。

代码语言:javascript
复制
 //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的数据源。

代码语言:javascript
复制
#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中的调用

代码语言:javascript
复制
 #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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.07.25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 问题场景
    • 因为接手项目的后续开发者不是看不懂其中的语法或者代码,他有可能看不懂的是其中的逻辑。
    • 把DataSource和其他 Protocols 抽离出来并封装成类
    • UITableViewController中的实例使用
    • 设计思路
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档