iOS-TableView架构设计思考

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大神的中间件,这样配合使用代码结构更加清晰,控制器解耦更加彻底。

结尾

一篇自认为有点实用性的大水文完成了,文章中有很多表达不清的地方,可以直接看代码。写一篇总结给自己看,希望自己不断进步吧。

你可能感兴趣的:(iOS-TableView架构设计思考)