Talk is cheap, show me the code!
先上demo
看到标题,又是架构又是设计的,一定觉得这是篇big很高的文章。。。其实你们都被骗了,只是简单的经验总结。文章到处是错别字,代码结构也比较混乱,谁还不能是个标题党了呢?
以前我写tableView,里面包含很多自定义cell时,控制器里就会含有大量的if-else判断,有时候不爽还来一个switch-case。调试修改起来相当的繁琐,而且事件回调会散落在控制器的各个地方,其中包含自定义cell的代理回调,网络请求的刷新回调还有cell被点击的didSelected回调。当然业务逻辑也散落的到处都是。这个demo,可以让TableView的开发效率成倍的提升,而且控制器只需要在相当少的代码量就可以完成。
其实一个简单界面开发的流程:
自定义控件-->网络请求重组数据-->数据交给控件刷新UI-->处理事件修改数据-->重新刷新UI
所以一个TableView在我看来需要自定义的事情也就只有自定义cell,重组数据源,处理点击事件这三个方面。其他事情都是模板化的,或者可以说是提前预处理好的。而且一行代码提成下拉刷新和上拉加载更多功能。
控制器写法
#import "SQBaseTableViewController.h"
#import "SQViewProtocol.h"
#import
#import
@interface SQBaseTableViewController ()
@property (nonatomic) NSInteger currentIndex;
@end
@implementation SQBaseTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self setRefreshEnable:NO];
[self setLoadMoreEnable:NO];
}
#pragma mark -
// 刷新操作,子类可以重写但是必须调用[super refresh]方法
- (void)refresh {
self.currentIndex = 0;
}
// 加载更多操作,子类可以重写但是必须调用[super loadMore]方法
- (void)loadMore {
self.currentIndex++;
}
#pragma mark -
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [self.viewModel numberOfSections];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.viewModel numberOfRowsInSection:section];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self.viewModel heightAtIndexPath:indexPath];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
id model = [self.viewModel modelAtIndexPath:indexPath];
id cell = [tableView dequeueReusableCellWithIdentifier:model.identifier forIndexPath:indexPath];
[cell customViewWithData:model];
return (UITableViewCell *)cell;
}
#pragma mark -
- (void)setViewModel:(id)viewModel {
_viewModel = viewModel;
@weakify(self);
[_viewModel.refreshUISubject subscribeNext:^(id x) {
@strongify(self);
[self.tableView reloadData];
[self.tableView.mj_header endRefreshing];
[self.tableView.mj_footer endRefreshing];
}];
}
- (void)setRefreshEnable:(BOOL)refreshEnable {
if (refreshEnable) {
@weakify(self);
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
@strongify(self);
[self refresh];
}];
}
}
- (void)setLoadMoreEnable:(BOOL)loadMoreEnable {
if (loadMoreEnable) {
@weakify(self);
self.tableView.mj_footer = [MJRefreshAutoFooter footerWithRefreshingBlock:^{
@strongify(self);
[self loadMore];
}];
}
}
- (void)setCurrentIndex:(NSInteger)currentIndex {
_currentIndex = currentIndex;
self.viewModel.currentIndex = currentIndex;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
可以看到基本的创建TableView的方法和上拉刷新、下拉加载更多的代码都在基类控制器里。自定义的子类控制器只要赋值viewmodel、处理事件就可以了。重点提一下cellForRow方法,由于采用了Protocol的方式,每一个cell只要实现协议,cell的类型由model中的identifier去决定。设计模式中,应该依赖于抽象而不是依赖于具体,这里控制器并不用知道具体的cell类型是什么。
#import "ViewController.h"
#import "SQViewModel.h"
#import "SQViewProtocol.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self setRefreshEnable:YES];
[self setLoadMoreEnable:YES];
self.viewModel = SQViewModel.new;
}
//在这里处理事件
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
[self handleEventWithName:eventName param:userInfo];
[super routerEventWithName:eventName userInfo:userInfo];
}
- (void)handleEventWithName:(NSString *)eventName param:(NSDictionary *)param {
if ([eventName isEqualToString:@"SQHeaderEvent"]) {
NSLog(@"~~~~~~~~~SQHeaderEvent");
}
if ([eventName isEqualToString:@"SQItemEvent"]) {
NSLog(@"~~~~~~~~~SQItemEvent");
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
这里只是设置viewModel,和处理事件的代码,其他代码都在基类中。控制中只要import少量的头文件,不需要耦合任何的自定义cell和model。控制器中也不关心具体的cell和model,只要给一个具体的viewModel,事件改变viewModel中数据源然后再刷新UI就可以了。
事件处理方式
处理事件使用的是ResponseChain传递,可以看一种基于ResponderChain的对象交互方式,
@implementation UIResponder (Extension)
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
[[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}
@end
实际上就是利用有事件处理时,比如cell上的一个button被点击了,扔一个事件给响应链,事件会沿着链条往上传递。遇到想处理该事件的某一响应者时,直接处理事件,比如这里的控制器。某一响应者不想处理事件时,直接覆写该方法然后调用super,就会将事件传递到上一响应者。
以前我传递事件是用block传递的,像这样
@protocol SQEventProtocol
@optional
// 用于传递事件(identifier用于标记是哪一个事件, params为需传参数)
- (void)handleEvent:(void(^)(NSDictionary *params, NSString *identifier))event;
@end
先有一个Event Protocol, 然后cell会遵守这个Event Protocol,cell和controller中这样处理
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.event) {
self.event(@{}, @"SQHeaderEvent");
}
}
- (void)handleEvent:(void (^)(NSDictionary *, NSString *))event {
self.event = event;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
id model = [self.viewModel modelAtIndexPath:indexPath];
id cell = [tableView dequeueReusableCellWithIdentifier:model.identifier forIndexPath:indexPath];
[cell customViewWithData:model];
[cell handleEvent:^(NSDictionary *params, NSString *identifier) {
//处理事件
}];
return (UITableViewCell *)cell;
}
直到我看到了 ResponderChain传递事件的方式,发现更加贴合这里的使用场景,代码量更少,功能也更加强大。
viewModel
viewModel是进行数据请求和业务逻辑处理的地方,遵守自ViewModel Protocol。
内部维护了一个Data Source数组,大多数的逻辑都是围绕着这个Data Source进行的。
#import "SQViewModel.h"
#import "SQHeaderModel.h"
#import "SQItemModel.h"
@implementation SQViewModel
@synthesize dataSource = _dataSource;
@synthesize refreshUISubject = _refreshUISubject;
- (instancetype)init {
if (self = [super init]) {
_refreshUISubject = RACSubject.new;
[self loadData];
}
return self;
}
- (void)loadData {
[self loadDataWithCurrentIndex:0];
}
- (void)loadDataWithCurrentIndex:(NSInteger)currentIndex {
NSLog(@"currentIndex:%ld", currentIndex);
NSArray *datas = @[SQHeaderModel.new, SQItemModel.new, SQItemModel.new, SQItemModel.new];
self.dataSource = datas;
}
- (void)setDataSource:(NSArray *)dataSource {
_dataSource = [dataSource copy];
[self.refreshUISubject sendNext:_dataSource];
}
- (void)setCurrentIndex:(NSInteger)currentIndex {
[self loadDataWithCurrentIndex:currentIndex];
}
- (CGFloat)heightAtIndexPath:(NSIndexPath *)indexPath {
id model = self.dataSource[indexPath.row];
return model.height;
}
- (NSInteger)numberOfSections {
return 1;
}
- (NSInteger)numberOfRowsInSection:(NSInteger)section {
return self.dataSource.count;
}
- (id)modelAtIndexPath:(NSIndexPath *)indexPath {
return self.dataSource[indexPath.row];
}
@end
Data Source内部是一组model,这些model用来控制cell的显示的。每一种自定义的cell,都对应一种model。由于控制器中的cell创建过程都是模板化的,具体怎么生成tableView,由viewModel决定。
刷新页面回调使用的是rac中的RACSuject,也可以替换成代理或者是block。这里使用rac,主要是数据请求下来可能会进行处理,使用rac可以使处理数据过程简单,思路清新。
model
model就是遵守了Model Protocol,除了普通的model用来展示数据之外,还添加了行高和cell复用标识功能。
#import
#import "SQModelProtocol.h"
@interface SQItemModel : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subTitle;
@end
#import "SQItemModel.h"
@implementation SQItemModel
- (NSString *)title {
return @"itemTitle";
}
- (NSString *)subTitle {
return @"itemSubTitle";
}
#pragma mark -
- (NSString *)identifier {
return @"SQItemCell";
}
- (CGFloat)height {
return 44.f;
}
@end
由于每一种自定义cell都是和特定的model对应的,model又是组成数据源的基本单位,数据源又控制着cell的显示。
有同学有疑问了,这里把cell行高写死了,万一我的cell是动态行高的怎么办?
其实很简单,可以封装一个layout类,model依赖于这个布局类,所有的布局工作由这个布局类完成,这个布局类可以提前计算好,由model缓存在内存中。这样可以避免tableView滑动的时候每一次都要计算cell行高,消耗了CUP资源。这也算是在布局层面优化了tableView性能。可以参考YYKit里面demo的做法。
cell
cell中要做的很简单了,就是根据具体数据自定义cell以及传递事件两件事情
#import "SQHeaderCell.h"
#import "SQHeaderModel.h"
@interface SQHeaderCell ()
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UILabel *subTitleLabel;
@end
@implementation SQHeaderCell
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
}
- (void)customViewWithData:(id)data {
SQHeaderModel *model = (SQHeaderModel *)data;
self.titleLabel.text = model.title;
self.subTitleLabel.text = model.subTitle;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self routerEventWithName:@"SQHeaderEvent" userInfo:@{}];
}
@end
由于cell和model是一一对应的,这里可以知道model的具体类型。
处理事件就是上述的ResponseChain的方式。为了事件聚合在一起,我一般会不用didSelected方法,而是在cell中用touchEnded去传递,这样控制器中就可以用上面代码贴出的方法同意处理事件。我们工程中控制器跳转使用了casatwy大神的中间件,这样配合使用代码结构更加清晰,控制器解耦更加彻底。
结尾
一篇自认为有点实用性的大水文完成了,文章中有很多表达不清的地方,可以直接看代码。写一篇总结给自己看,希望自己不断进步吧。