美文网首页
iOS-TableView架构设计思考

iOS-TableView架构设计思考

作者: NSBug | 来源:发表于2018-07-23 21:15 被阅读138次

    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 <ReactiveCocoa.h>
    #import <MJRefresh.h>
    
    @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<SQModelProtocol> model = [self.viewModel modelAtIndexPath:indexPath];
        id<SQViewProtocol> cell = [tableView dequeueReusableCellWithIdentifier:model.identifier forIndexPath:indexPath];
        [cell customViewWithData:model];
    
        return (UITableViewCell *)cell;
    }
    
    #pragma mark -
    - (void)setViewModel:(id<SQViewModelProtocol>)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 <NSObject>
    @optional
    // 用于传递事件(identifier用于标记是哪一个事件, params为需传参数)
    - (void)handleEvent:(void(^)(NSDictionary *params, NSString *identifier))event;
    
    @end
    

    先有一个Event Protocol, 然后cell会遵守这个Event Protocol,cell和controller中这样处理

    - (void)touchesEnded:(NSSet<UITouch *> *)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<SQModelProtocol> model = [self.viewModel modelAtIndexPath:indexPath];
        id<SQViewProtocol> 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<SQModelProtocol> model = self.dataSource[indexPath.row];
        return model.height;
    }
    
    - (NSInteger)numberOfSections {
        return 1;
    }
    
    - (NSInteger)numberOfRowsInSection:(NSInteger)section {
        return self.dataSource.count;
    }
    
    - (id<SQModelProtocol>)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 <Foundation/Foundation.h>
    #import "SQModelProtocol.h"
    
    @interface SQItemModel : NSObject <SQModelProtocol>
    
    @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<SQModelProtocol>)data {
        SQHeaderModel *model = (SQHeaderModel *)data;
        
        self.titleLabel.text = model.title;
        self.subTitleLabel.text = model.subTitle;
    }
    
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self routerEventWithName:@"SQHeaderEvent" userInfo:@{}];
    }
    
    @end
    
    

    由于cell和model是一一对应的,这里可以知道model的具体类型。
    处理事件就是上述的ResponseChain的方式。为了事件聚合在一起,我一般会不用didSelected方法,而是在cell中用touchEnded去传递,这样控制器中就可以用上面代码贴出的方法同意处理事件。我们工程中控制器跳转使用了casatwy大神的中间件,这样配合使用代码结构更加清晰,控制器解耦更加彻底。

    结尾

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

    相关文章

      网友评论

          本文标题:iOS-TableView架构设计思考

          本文链接:https://www.haomeiwen.com/subject/qxtsmftx.html