MVVM之UITableView的一些想法

720 阅读7分钟

关于设计模式,最常用的两个模式是 MVC 和 MVVM,关于这两个模式的具体意思和优缺点,这里就不再赘述了,关于这两个模式的解释网上的资源一大堆,但是关于模式这个东西,都是比较广义的,就像你女朋友要吃饭,你可以去馆子里打包一份给她,也可以自己去菜市场买菜回来再做,你买回来的可能都凉了影响口感,你自己做的味道又不一定比得上人家师傅做的好吃,但是最终结果都是一样的,你得喂你女朋友让她吃的饱饱的.

所以说,具体用哪个模式这个不是死的,两个模式都各有各的好处,一千个读者就有一千个哈姆雷特,自认为自己也算是参与了几个小规模的项目,对于这些项目的设计模式,我觉得没必要死扣哪个模式,怎么实用怎么来,怎么方便扩展怎么来,你用传统 MVC 没问题,你用路由 MVVM 也可以,你在 MVC 里面穿插ViewModel 也可以,毕竟需求就像女朋友的脾气一样,有时候总会让你捉摸不透.

关于 UITableView 的用法,再熟悉不过了,它的功能很简单,就是展示列表的功能,最常见的功能就是上拉刷新,下拉加载下一页,自定义Cell,自定义Cell就不说了,很基础的东西,但是表这个东西,在开发的过程中用的应该是比较多的,而且功能都差不多,如果把表的逻辑实现都放在Controller中,那么Controller中就会出现很多重复的代码,不美观,而且没必要,结合腾讯 IM 中的开源代码,我觉得这个控件可以通过 RAC 和 MVVM 的模式来简化项目中依赖网络请求的表,废话不多说,上内容

首先将dataTableView捆绑起来,新建一个类,继承 UITableView.在 .h 里面先声明数据信号量属性dataSignal,该属性是连接网络请求的;再声明数据源属性data,该属性只是用来展示的,所以设置成只读;并不是所有的表都有下拉刷新,再声明一个是否支持分页属性paging;再声明一个每个页码的请求量pageSize,这个量是接口需要的,可读可写.到此,基本功能需要的属性就声明完成,后续属性需要什么就加什么,这个后面再说

//数据信号
@property(nonatomic, strong) RACSignal *dataSignal;
//数据源
@property(nonatomic, strong, readonly) NSArray *data;
//是否支持分页
@property(nonatomic, assign) BOOL paging;
//默认为 10
@property(nonatomic, assign) NSInteger pageSize;

在 .m 里面写实现, .m 因为部分数据设置成只读,所以 .m 里面需要设置部分私有变量来接受数据,在 .m 里面声明几个属性和方法

//容纳数据
@property(nonatomic, strong) NSMutableArray *muArray;
//如果支持分页,这个是数据的最大量,从接口获取
@property(nonatomic, assign) NSInteger maxNumber;
//当前页码
@property(nonatomic, assign) NSInteger page;
//开始刷新,重新赋值信号
@property(nonatomic, copy) void(^startRefresh)(NSInteger page,NSInteger pageSize);

//刷新
- (void)beginRefresh;

重写初始化方法,部分默认变量进行赋值,在初始化的时候绑定 MJRefreshHeader,当刷新的时候,页码归 1 ,重新请求数据

- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style{
    if (self = [super initWithFrame:frame style:style]) {
        self.muArray = [NSMutableArray array];
        self.page = 1;
        self.pageSize = 10;
        WeakSelf(self);
        
        self.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
            weakself.page = 1;
            if (weakself.startRefresh) {
                //通知信号量,重新请求数据,将页码和数量回调出去,在Controller中完成此操作
                weakself.startRefresh(weakself.page,weakself.pageSize);
            }
            [weakself.muArray removeAllObjects];
            [weakself signalDataSource];
        }];
    }
    
    return self;
}

设置是否支持分页,如果支持,就绑定 MJTableView 同时也将页码和数量回调出去,请求新的数据

- (void)setPaging:(BOOL)paging{
    _paging = paging;
    if (!paging) {
        return;
    }
    WeakSelf(self);
    self.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
        if (weakself.muArray.count == weakself.maxNumber) {
            //如果当前数据源已经到达最大量,就不可再下拉加载数据
            [weakself.mj_footer endRefreshingWithNoMoreData];
        }else{
            weakself.page++;
            if (weakself.startRefresh) {
                weakself.startRefresh(weakself.page,weakself.pageSize);
            }
            [weakself signalDataSource];
        }
    }];
}

然后就是利用RAC来传递从网络请求的数据

- (void)signalDataSource{
    WeakSelf(self);
    //订阅信号,更新数据源
    [self.dataSignal subscribeNext:^(id  _Nullable x) {
        //注意,这里的格式是固定的,x = @[obj1,obj2]
        //obj1 表示的是当前请求的数组
        //obj2 表示改类型数据总共有多少条,用于判断 footer 的刷新状态
        id obj = [x objectOrNilAtIndex:1];
        if (obj) {
            weakself.maxNumber = [obj integerValue];
        }
        [weakself.muArray addObjectsFromArray:[x objectAtIndex:0]];
        [weakself reloadData];
        } error:^(NSError * _Nullable error) {
            [SVProgressHUD showImage:[UIImage imageNamed:@""] status:error.description];
        } completed:^{
            [weakself endRefresh];
        }];
}

- (void)endRefresh{
    [self.mj_header endRefreshing];
    [self.mj_footer endRefreshing];
}

在 .h 中声明了一个只读变量, 写 get 方法来获取它,用于Controller

- (NSArray *)data{
    return self.muArray.copy;
}

- (void)beginRefresh{
    [self.mj_header beginRefreshing];
}

到此基本功能就组建完成,下面来结合接口数据来进行赋值,以我手上现在的项目为例子,写一个接口,接口返回的 是一个信号量

/// @param cityId 城市
/// @param areraId 区域
/// @param priceSort 价格排序
/// @param shelves 上架状态
/// @param status 审核状态
/// @param size size
/// @param page page
+ (RACSignal *)getSecondHouseListWithCityId:(NSString *)cityId
                                    areraId:(NSString *)areraId
                                     sorted:(NSString *)priceSort
                                    shelves:(NSString *)shelves
                                     status:(NSString *)status
                                   pageSize:(NSInteger)size
                                       page:(NSInteger)page
                                   dataType:(NSString *)type;

这是一个带有分页性质的表数据,来看看具体实现部分

+ (RACSignal *)getSecondHouseListWithCityId:(NSString *)cityId areraId:(NSString *)areraId sorted:(NSString *)priceSort shelves:(NSString *)shelves status:(NSString *)status pageSize:(NSInteger)size page:(NSInteger)page dataType:(NSString *)type{
    RequestModel *model = [RequestModel defaultModel];
    model.keys = @[@"projectCityId",@"areaId",@"priceOrder",@"shelves",@"status",@"pageSize",@"pageNumber",@"dataType"];
    model.values = @[cityId,areraId,priceSort,shelves,status,@(size),@(page),type];
    RequestLink *link = [RequestLink initstanceStyle:RequestJsonPostStyle requestModel:model.json];
    link.index = 1;
    link.link =  @"/agent/sh/list/project";
    
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        
        [BaseModel requestByPostAtLink:link CallBack:^(BaseModel *base) {
            if (base.code.integerValue == 2000) {
                NSArray *data = [NSArray yy_modelArrayWithClass:SHSecondHouseModel.class json:base.result[@"records"]];
                NSInteger max = [base.result[@"total"] integerValue];
                //格式固定
                [subscriber sendNext:@[data,@(max)]];
                [subscriber sendCompleted];
            }else{
                [subscriber sendError:base.error];
            }
            
        }];
        
        return [RACDisposable disposableWithBlock:^{
            
        }];
    }];
}

具体网络框架的内容就不讲太多,就是基于AFN和后台的要求封装的框架.

那控制器如何使用呢,也很简单,先设置属性,再赋值信号量,再刷新就可以实现这些简单的功能了,上代码

    [self.view addSubview:self.mTableView];
    [self.mTableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(bottom);
        make.bottom.equalTo(self.view).offset(-HOME_INDICATOR_HEIGHT);
        make.left.right.equalTo(self.view);
    }];
    self.status = @"61";
    self.mTableView.paging = YES;
    self.mTableView.startRefresh = ^(NSInteger page, NSInteger pageSize) {
       weakself.mTableView.dataSignal = [Dao getSecondHouseListWithCityId:@"-1"
                                                                  areraId:@"-1"
                                                                   sorted:@"-1"
                                                                  shelves:@"-1"
                                                                   status:weakself.status
                                                                 pageSize:pageSize
                                                                     page:page
                                                                 dataType:@"2"];
    };
    [self.mTableView beginRefresh];
    

然后在TableView中代理中,我们需要的数据都可以在TableView中拿到,在Controller中也不需要再重复的声明属性了

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return self.mTableView.data.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    SHSecondHouseTableViewCell *cell = [SHSecondHouseTableViewCell cellWithTableView:tableView andIndexPath:indexPath];
    [cell fillCellWithModel:self.mTableView.data[indexPath.row] status:YES personal:YES];
    return cell;;
}

到此UITableView 关于MVVM的一些想法就完成了,如果我们想要的属性没有的话,都可以去基类中声明,比如说想要知道当前页码,结束刷新后需要回调操作等等,都可以封装到基类中去,下面放上常用的功能和属性,将整个代码复制过来

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface LQBaseTableView : UITableView

/**
    将表下拉刷新,上拉加载更多放在基类,格式固定
 */

//数据信号
@property(nonatomic, strong) RACSignal *dataSignal;
//当前数据源
@property(nonatomic, strong, readonly) NSArray *data;
//其他数据,放数组第三条数据
@property(nonatomic, strong, readonly) id extraData;
//默认为 10
@property(nonatomic, assign) NSInteger pageSize;
//是否支持分页
@property(nonatomic, assign) BOOL paging;
//最大数
@property(nonatomic, assign, readonly) NSInteger maxCount;
//当前页码
@property(nonatomic, assign, readonly) NSInteger currentPage;
//开始刷新,重新赋值信号
@property(nonatomic, copy) void(^startRefresh)(NSInteger page,NSInteger pageSize);
//结束刷新回调,可用于是否显示空数据页面等操作
@property(nonatomic, copy) void(^endHederRefresh)(LQBaseTableView *tableView);

//刷新
- (void)beginRefresh;
//删除单行
- (void)deleteRow:(NSIndexPath *)indexPath animation:(UITableViewRowAnimation)animation;
//更新单行
- (void)changeRow:(NSIndexPath *)indexPath obj:(id)obj;

@end

NS_ASSUME_NONNULL_END

.m

@interface LQBaseTableView ()

@property(nonatomic, strong) NSMutableArray *muArray;
@property(nonatomic, assign) NSInteger maxNumber;
@property(nonatomic, assign) NSInteger page;
@property(nonatomic, strong) id extra;

@end

@implementation LQBaseTableView


- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style{
    if (self = [super initWithFrame:frame style:style]) {
        self.muArray = [NSMutableArray array];
        self.page = 1;
        self.pageSize = 10;

        EWWeakSelf;
        self.mj_header = [MJRefreshHeader headerWithRefreshingBlock:^{
            weakSelf.page = 1;
            if (weakSelf.startRefresh) {
                weakSelf.startRefresh(weakSelf.page,weakSelf.pageSize);
            }
            [weakSelf.muArray removeAllObjects];
            [weakSelf signalDataSource];
        }];
    }
    
    return self;
}

- (void)setPaging:(BOOL)paging{
    _paging = paging;
    EWWeakSelf;
    self.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
        if (weakSelf.muArray.count == weakSelf.maxNumber) {
            [weakSelf.mj_footer endRefreshingWithNoMoreData];
        }else{
            weakSelf.page++;
            if (weakSelf.startRefresh) {
                weakSelf.startRefresh(weakSelf.page,weakSelf.pageSize);
            }
            [weakSelf signalDataSource];
        }
    }];
}

- (void)signalDataSource{
    EWWeakSelf;
    [self.dataSignal subscribeNext:^(id  _Nullable x) {
        id obj = [x objectOrNilAtIndex:1];
        if (obj) {
            weakSelf.maxNumber = [obj integerValue];
        }
        id extra = [x objectOrNilAtIndex:2];
        weakSelf.extra = extra;
        [weakSelf.muArray addObjectsFromArray:[x objectAtIndex:0]];
        [weakSelf reloadData];
        if (weakSelf.page == 1) {
            //结束刷新,刷新后操作可在此 block 中执行
            if (weakSelf.endHederRefresh) {
                weakSelf.endHederRefresh(weakSelf);
            }
        }
        } error:^(NSError * _Nullable error) {
            [SVProgressHUD showImage:[UIImage imageNamed:@""] status:error.description];
        } completed:^{
            [weakSelf endRefresh];
        }];
}

- (void)endRefresh{
    [self.mj_header endRefreshing];
    [self.mj_footer endRefreshing];
}

- (NSArray *)data{
    return self.muArray.copy;
}

- (id)extraData{
    return self.extra;
}

- (NSInteger)maxCount{
    return self.maxNumber;
}

- (NSInteger)currentPage{
    return self.page;
}

- (void)deleteRow:(NSIndexPath *)indexPath animation:(UITableViewRowAnimation)animation{
    if ([self.muArray objectOrNilAtIndex:indexPath.row]) {
        [self.muArray removeObjectAtIndex:indexPath.row];
        [self deleteRow:indexPath.row inSection:indexPath.section withRowAnimation:animation];
        [self reloadData];
    }
}

- (void)changeRow:(NSIndexPath *)indexPath obj:(id)obj{
    if ([self.muArray objectOrNilAtIndex:indexPath.row]) {
        self.muArray[indexPath.row] = obj;
        [self reloadRowAtIndexPath:indexPath withRowAnimation:UITableViewRowAnimationNone];
    }
}

- (void)beginRefresh{
    [self.mj_header beginRefreshing];
}

总结

此想法仅仅是本人结合公司实际后台规范和本公司项目规范提出的想法,有优点也有缺点,具体用法和封装还是需要结合公司后台实际情况来用

感谢观看!!!