首先RAC和MVVM是两个东西,并不是一定要一起用,只不过RAC可以更优雅的实现MVVM的架构
MVVM
经历过一阵子的开发之后,当项目越写越大的时候,不难发现,传统的MVC架构显得和笨重,尤其处理复杂页面的时候,控制器里面可以分化出网络层,服务层,应用层,等等,根据每个项目的状况和每个人的理解程度不同,还可以有很多不同的方案,总之你会发现控制器越来越复杂。
再大量的码代码的阶段的过程中,解耦是永恒的问题,每个人对于继承,封装,协议,block等等的理解方式导致每个人的项目中或多或少都有一些倾向,无论是什么技术,设计出来的架构都是为了能让项目更明朗,更易用,易扩展。所以在项目架构设计的时候,无论是什么技术貌似都有利有弊,所以平衡就显得尤为重要,技术虽好,不要贪杯哦。没有最好的架构,只有最适合的架构。
定义MVVM
- Model:MVVM定义的model倾向于瘦model,尽量让model只是提供数据——模型之间的转换问题
- View:view层的定义跟MVC中没有什么差别,更加肯定的是,view的作用只是用来提供UI的罗列,并不存在什么有关的逻辑问题,这样也更容易复用。
- ViewModel:通俗点理解他可以是控制器中的网络层,也可以是tableView的代理,无论他是什么,他都是view和model的桥梁。
- Controller:这时候的控制器的作用只是用来调度,与其说是调度,不如说是bind。
实现
需求:
1.MVVM+RAC实现列表数据,扩展上拉下拉到基类中,实现复用
2.彻底解耦,View只化UI,可以复用,model只提供数据类型,可以复用
具体实现
先来看看基类,首先是控制器,初始化方法就传入viewmodel,提供三个初始化后调用的接口,也可以根据具体需求修改。
#import "BaseVCProtocol.h"
@class BaseViewModel;
@interface BaseVC : UIViewController
/** 初始化视图 */
- (instancetype)initWithViewModel:(BaseViewModel *)viewModel;
/** 初始化后调用的方法 */
- (void)bindInitialization;
- (void)bindNotification;
- (void)bindViewModel;
@end
#import "BaseVC.h"
#import "BaseViewModel.h"
@interface BaseVC ()
/** viewModel */
@property(nonatomic,strong) BaseViewModel * viewModel;
@end
@implementation BaseVC
#pragma mark - life cycle
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
BaseVC *viewController = [super allocWithZone:zone];
@weakify(viewController)
[[viewController
rac_signalForSelector:@selector(viewDidLoad)]
subscribeNext:^(id x) {
@strongify(viewController)
[viewController bindInitialization];
[viewController bindNotification];
[viewController bindViewModel];
}];
return viewController;
}
- (instancetype)initWithViewModel:(BaseViewModel *)viewModel {
self = [super init];
if (self) {
self.viewModel = viewModel;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)bindInitialization {}
- (void)bindNotification {}
- (void)bindViewModel {}
@end
控制器基类还是比较简单,扩展性强,主要是做一些规范性的接口,接着是tableViewVC,包装一层tableView代理,提供网络请求状态接口方便在子类中处理异常的逻辑
tableViewVC中要依赖于里面的viewModel进行一些通用的设置,例如,基础的tableView代理配置,是否在在子类中选择是否有刷新,刷新信号的绑定,cell点击信号的绑定,数据更新时的刷新
#import "BaseVC.h"
@interface BaseTableViewVC : BaseVC
/** tableView */
@property(nonatomic,strong) UITableView * tableView;
/** 处理成功回调 */
- (void)dataRequestSuccess:(id)params;
/** 处理错误回调 */
- (void)dataRequestError:(NSError *)error;
/** 处理结束回调 */
- (void)dataRequestFinished;
@end
#import "BaseTableViewVC.h"
#import "BaseTableViewModel.h"
@interface BaseTableViewVC ()
/** viewModel */
@property(nonatomic,strong) BaseTableViewModel * viewModel;
@end
@implementation BaseTableViewVC
#pragma mark - life cycle
- (instancetype)initWithViewModel:(BaseViewModel *)viewModel{
self = [super initWithViewModel:viewModel];
if (self) {
@weakify(self);
[[self rac_signalForSelector:@selector(bindViewModel)]
subscribeNext:^(id x) {
@strongify(self);
//一开始就刷新
if (self.viewModel.isAllowLoadData &&self.viewModel.isLoadDataInitially) {
[self.tableView.mj_header beginRefreshing];
}
}];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.tableView];
if (self.viewModel.isAllowLoadData) {
self.tableView.mj_header = [self setupHeader];
self.tableView.mj_footer = [self setupFooter];
}
}
- (void)bindInitialization
{
[super bindInitialization];
}
- (void)bindNotification
{
[super bindNotification];
}
- (void)bindViewModel
{
[super bindViewModel];
@weakify(self);
//监听数据源,刷新列表 [RACObserve(self.viewModel,dataSource).distinctUntilChanged.deliverOnMainThread
subscribeNext:^(id x) {
@strongify(self);
[self.tableView reloadData];
}];
}
#pragma mark - custom
- (void)dataRequestSuccess:(id)params {}
- (void)dataRequestError:(NSError *)error {}
- (void)dataRequestFinished {}
- (MJRefreshNormalHeader *)setupHeader
{
@weakify(self);
MJRefreshNormalHeader * setupHeader = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
@strongify(self);
self.viewModel.pageIndex = 0;
[self requestRemote];
}];
return setupHeader;
}
- (MJRefreshBackNormalFooter *)setupFooter
{
@weakify(self);
MJRefreshBackNormalFooter * setupFooter = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
@strongify(self);
[self requestRemote];
}];
return setupFooter;
}
- (void)requestRemote
{
@weakify(self);
[[self.viewModel.requestRemoteDataCommand execute:@(self.viewModel.pageIndex + 1)] subscribeNext:^(id _Nullable x) {
@strongify(self);
if ([self respondsToSelector:@selector(dataRequestSuccess:)]) {
[self dataRequestSuccess:x];
}
[self.tableView.mj_header endRefreshing];
[self.tableView.mj_footer endRefreshing];
} error:^(NSError * _Nullable error) {
@strongify(self);
if ([self respondsToSelector:@selector(dataRequestError:)]) {
[self dataRequestError:error];
}
[self.tableView.mj_header endRefreshing];
[self.tableView.mj_footer endRefreshing];
} completed:^{
@strongify(self);
if ([self respondsToSelector:@selector(dataRequestFinished)]) {
[self dataRequestFinished];
}
[self.tableView.mj_header endRefreshing];
[self.tableView.mj_footer endRefreshing];
}];
}
#pragma mark - UITableViewDelegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.viewModel.dataSource.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 44.0f;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *ID = @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
}
cell.textLabel.text = [NSString stringWithFormat:@"%ld-----%ld",indexPath.section,indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[self.viewModel.cellDidSelect execute:indexPath];
}
#pragma mark - lazy
- (UITableView *)tableView
{
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.backgroundColor = [UIColor whiteColor];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self.view addSubview:_tableView];
}
return _tableView;
}
- (BaseViewModel *)viewModel
{
return [[BaseTableViewModel alloc] init];
}
@end
基本的viewModel还是提供最基本的接口供子类重写,并提供一个数据的信号,可以在子类封装出使用的网络请求
#import "BaseViewModelProtocol.h"
@interface BaseViewModel : NSObject
/** 初始化后调用的方法 */
- (void)viewModelDidLoad;
- (void)viewModelLoadNotifications;
/** 获取数据方法 */
- (RACSignal *)requestRemoteDataSignal;
@end
#import "BaseViewModel.h"
@interface BaseViewModel ()
@end
@implementation BaseViewModel
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
BaseViewModel *viewModel = [super allocWithZone:zone];
@weakify(viewModel)
[[viewModel
rac_signalForSelector:@selector(init)]
subscribeNext:^(id x) {
@strongify(viewModel)
[viewModel viewModelDidLoad];
[viewModel viewModelLoadNotifications];
}];
return viewModel;
}
- (void)viewModelDidLoad {}
- (void)viewModelLoadNotifications {}
#pragma mark - custom
- (RACSignal *)requestRemoteDataSignal
{
return [RACSignal empty];
}
#pragma mark - lazy
@end
然后为tableView提供一个专门的viewModel,来配置刷新状态,操作刷新的逻辑处理,网络请求,点击事件和刷新的信号发送
#import "BaseViewModel.h"
//默认每次请求显示10条数据
static const NSInteger pageSize = 10;
@interface BaseTableViewModel : BaseViewModel
/** 是否一开始就需要刷新 */
@property (nonatomic,assign) BOOL isLoadDataInitially;
/** 是否允许刷新 */
@property (nonatomic,assign) BOOL isAllowLoadData;
/** 是否允许加载更多 */
@property (nonatomic,assign) BOOL isAllowLoadAdditionalData;
/** 点击cell指令 */
@property (nonatomic,strong) RACCommand * cellDidSelect;
/** 获取更多数据的指令 */
@property (nonatomic,strong) RACCommand *requestRemoteDataCommand;
/** 数据源 */
@property(nonatomic,strong) NSArray * dataSource;
/** pageIndex */
@property(nonatomic,assign) NSInteger pageIndex;
/** 获取数据方法(重写) */
- (RACSignal *)requestRemoteDataSignalWithPage:(NSUInteger)page;
/** 默认处理数据回调的方法(上拉下拉处理数据) */
- (void)dataSourceHandler:(NSArray *)arr;
@end
#import "BaseTableViewModel.h"
@implementation BaseTableViewModel
#pragma mark - life cycle
- (void)viewModelDidLoad {
[super viewModelDidLoad];
self.pageIndex = 0;
self.dataSource = [NSMutableArray array];
@weakify(self);
self.requestRemoteDataCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(NSNumber *page) {
@strongify(self);
return [self requestRemoteDataSignalWithPage:page.unsignedIntegerValue];
}];
self.cellDidSelect = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(NSIndexPath * _Nullable indexPath) {
return [RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) {
[subscriber sendNext:indexPath];
[subscriber sendCompleted];
return nil;
}];
}];
}
- (void)viewModelLoadNotifications
{
[super viewModelLoadNotifications];
}
#pragma mark - network
- (RACSignal *)requestRemoteDataSignalWithPage:(NSUInteger)page
{
return [RACSignal empty];
}
#pragma mark - custom
- (void)dataSourceHandler:(NSArray *)arr {
if (arr && arr.count > 0) {
if (self.pageIndex == 0) {
self.dataSource = arr;
} else {
self.dataSource = [self.dataSource arrayByAddingObjectsFromArray:arr];
}
self.pageIndex ++;
} else {
if (self.pageIndex == 0) {
self.dataSource = @[];
}
}
}
#pragma mark - lazy
基于这些基础,扩展起来就很容易,代码也很少。view只需要去布局就好了,model只写数据结构,中间通过一个协议进行赋值,高度如果手动算的话就实现协议
@protocol BaseTableViewCellProtocol
@optional
/** 绑定ViewModel以及赋值 */
- (void)bindWithViewModel:(BaseViewModel *)viewModel fetchDataSource:(NSObject *)dataSource forIndexPath:(NSIndexPath *)indexPath;
/** 计算高度 */
+ (CGFloat)cellHeightWithModel:(NSObject *)model;
@end
子类的viewModel只需要重写网络请求信号,并解析
#import "SectionMVVM_ViewModel.h"
#import "SectionMVVM_Model.h"
@implementation SectionMVVM_ViewModel
#pragma mark - life cycle
- (void)viewModelDidLoad
{
[super viewModelDidLoad];
self.isAllowLoadData = YES;
}
- (void)viewModelLoadNotifications
{
[super viewModelLoadNotifications];
}
#pragma mark - network
- (NSArray *)networkHandle
{
NSDictionary * dic = @{@"image":@"http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg",
@"title":@"百度图片",
@"content":@"百度图片使用世界前沿的人工智能技术,为用户甄选海量的高清美图,用更流畅、更快捷、更精准的搜索体验,带你去发现多彩的世界。"};
NSMutableArray *array = [NSMutableArray array];
for (NSInteger i = 0; i < 20; i++) {
[array addObject:dic];
}
return array;
}
/** 获取数据方法(重写) */
- (RACSignal *)requestRemoteDataSignalWithPage:(NSUInteger)page
{
@weakify(self)
return [[[[[ZZRequestManager sectionMVVMRequestWithPage:page] map:^id _Nullable(NSArray * _Nullable listArray) {
return [NSArray yy_modelArrayWithClass:[SectionMVVM_Model class] json:listArray];
}] doNext:^(id _Nullable x) {
@strongify(self)
[self dataSourceHandler:x];
}] doError:^(NSError * _Nonnull error) {
@strongify(self)
NSArray * array = [NSArray yy_modelArrayWithClass:[SectionMVVM_Model class] json:[self networkHandle]];
[self dataSourceHandler:array];
}] doCompleted:^{
}];
}
#pragma mark - custom
#pragma mark - lazy
@end
子类控制器如果想自定义什么就重写什么方法,初始化的时候绑定上viewModel的网络请求和点击事件
#import "SectionMVVM_VC.h"
#import "SectionMVVM_Cell.h"
#import "SectionMVVM_ViewModel.h"
@interface SectionMVVM_VC ()
/** viewModel */
@property(nonatomic,strong) SectionMVVM_ViewModel * viewModel;
@end
@implementation SectionMVVM_VC
#pragma mark - life cycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[SectionMVVM_Cell class] forCellReuseIdentifier:NSStringFromClass([SectionMVVM_Cell class])];
}
- (void)bindViewModel
{
[super bindViewModel];
[self.viewModel.requestRemoteDataCommand execute:@(1)];
[self.viewModel.cellDidSelect.executionSignals.switchToLatest subscribeNext:^(NSIndexPath * _Nullable indexPath) {
NSLog(@"%ld--%ld",indexPath.section,indexPath.row);
}];
}
#pragma mark - customMethod
#pragma mark - reset
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SectionMVVM_Cell * cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([SectionMVVM_Cell class])];
[cell bindWithViewModel:self.viewModel fetchDataSource:self.viewModel.dataSource[indexPath.row] forIndexPath:indexPath];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [tableView fd_heightForCellWithIdentifier:NSStringFromClass([SectionMVVM_Cell class]) cacheByIndexPath:indexPath configuration:^(SectionMVVM_Cell * cell) {
[cell bindWithViewModel:self.viewModel fetchDataSource:self.viewModel.dataSource[indexPath.row] forIndexPath:indexPath];
}];
}
#pragma mark - lazy or setter or getter
总结
由于手中现在的项目做的越来越复杂,控制器的代码逐渐增加,从最开始200行到现在400行,实在是无法忍受了(其实看别人的项目,控制器400行我肯定看不下去),再寻找合适框架的时候,顺便学了一下RAC,写个demo巩固一下所学的知识,最近时间比较紧张,可能还有很多地方可以优化,大神勿喷。
demo地址:https://github.com/754340156/RAC-MVVM