RAC系列4之 软件架构MVC与MVVM
常见的架构思想
- MVC
- M:模型 V:视图 C:控制器
- MVVM
- M:模型 V:视图+控制器 VM:视图模型
- MVCS
- M:模型 V:视图 C:控制器 C:服务类
- VIPER
- V:视图 I:交互器 P:展示器 E:实体 R:路由
MVC都是怎么回事?
控制器都干了什么?
- 展示UI(添加/赋值/刷新UI)
- 逻辑控制(界面跳转/各个子控件之间的逻辑)
- 处理数据
- 请求数据,获得模型数组
- 搜集界面的数据,处理上传到服务器
view都干了什么?
- 拿到数据源(model)赋值展示数据(cell)
- 封装:实现子view之间的逻辑,自身的功能
- 用户交互改变模型的数据
model都干了什么?
- 数据都转为属性
- 添加辅助属性
- 有时候服务器返回的数据并不能直接展示,需要处理后才能直接展示到view上,该属性一般为readonly,.m中自己添加下划线的成员变量,从写get方法,处理过程放在get方法中
MVVM想干啥?
- 抽取掉控制器中的不必要代码到viewModel中
- 从MVC中可以看出,控制器中哪些不必要呢?
- 各个子控件之间的逻辑过程
- 网络请求处理过程
- 网络上传数据处理的过程
- 抽取之后会变成什么效果呢?
- 控制器用viewModel来获取网络数据,处理数据源,更新模型数据
- 控制器用viewModel来搜集数据,上传服务器数据
- 控制器用viemModel来控制各个子控件的逻辑
iOS中MVC的标准架构
这里就不扯什么高大上的语言了,直接上代码
代码举例:
-
控制器C
#import "ViewController.h" #import "PersonModel.h" #import "PeopleViewCell.h" @interface ViewController ()<UITableViewDataSource,UITableViewDelegate> @property (nonatomic,weak) UITableView *tableView; //数据源 @property (nonatomic,strong) NSMutableArray *dataSource; @end @implementation ViewController -(NSMutableArray *)dataSource{ if (_dataSource == nil) { _dataSource = [NSMutableArray array]; } return _dataSource ; } - (void)viewDidLoad { [super viewDidLoad]; //1.初始化UI UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds]; tableView.dataSource = self; tableView.delegate = self; [self.view addSubview:tableView]; self.tableView = tableView; [tableView registerNib:[UINib nibWithNibName:@"PeopleViewCell" bundle:nil] forCellReuseIdentifier:@"PeopleViewCell"]; //2. 网络请求 [self requestAction]; } #pragma mark - UITableViewDataSource - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 130; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return self.dataSource.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ PeopleViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PeopleViewCell"]; //只给cell传递模型,不暴露cell的其他属性 cell.model = self.dataSource[indexPath.row]; return cell; } #pragma mark - 网络请求 -(void)requestAction{ //1.相当于网络请求 NSString *filePath = [[NSBundle mainBundle] pathForResource:@"data.plist" ofType:nil]; NSMutableArray *data1 = [[NSMutableArray alloc] initWithContentsOfFile:filePath]; //2.拿到数据: 字典数组转为模型数组 for (NSDictionary *dic in data1) { PersonModel *model = [[PersonModel alloc] initWithDict:dic]; [self.dataSource addObject:model]; } //3. 刷新界面 [self.tableView reloadData]; NSLog(@"%@",data1); } @end
-
模型(Model)
#import <Foundation/Foundation.h> @interface PersonModel : NSObject @property (nonatomic,copy) NSString *userNickName; @property (nonatomic,copy) NSString *province; @property (nonatomic,copy) NSString *city; @property (nonatomic,copy) NSString *village; @property (nonatomic,copy) NSString *town; @property (nonatomic,copy) NSString *userSex; //辅助属性(因此有些逻辑不一定非要放到控制器中) @property (nonatomic,copy) NSString *familyAdress; @property (nonatomic,copy) NSString *sex; -(instancetype)initWithDict:(NSDictionary *)dic; @end #import "PersonModel.h" @implementation PersonModel -(instancetype)initWithDict:(NSDictionary *)dic{ if (self = [super init]) { _userNickName = dic[@"userNickName"]; _province = dic[@"province"]; _city = dic[@"city"]; _village = dic[@"village"]; _town = dic[@"town"]; _userSex = dic[@"userSex"]; } return self; } -(NSString *)sex{ _sex = [_userSex boolValue] ? @"女": @"男"; return _sex; } -(NSString *)familyAdress{ _familyAdress = [NSString stringWithFormat:@"%@%@%@%@",_province,_city,_village,_town]; return _familyAdress; } @end
-
view(xib定义的cell)
#import <UIKit/UIKit.h> @class PersonModel; @interface PeopleViewCell : UITableViewCell @property (nonatomic,strong) PersonModel *model; @end #import "PeopleViewCell.h" #import "PersonModel.h" @interface PeopleViewCell() @property (weak, nonatomic) IBOutlet UILabel *nameLabel; @property (weak, nonatomic) IBOutlet UILabel *sexLabel; @property (weak, nonatomic) IBOutlet UILabel *adressLabel; @end @implementation PeopleViewCell - (void)awakeFromNib { [super awakeFromNib]; _adressLabel.preferredMaxLayoutWidth = [UIScreen mainScreen].bounds.size.width - 15; } //从写模型set方法 -(void)setModel:(PersonModel *)model{ _model = model; _nameLabel.text = model.userNickName; _sexLabel.text = model.sex; _adressLabel.text = model.familyAdress; } @end
MVC总结(标准MVC 的特点)
- 控制器C:
- 控制器有一个数据源模型数组(当然有时也可以是单个模型)属性
- 控制器发送网络请求更新模型,然后刷新数据
- 视图View(cell):
- view有一个模型属性,通过这个模型来获取所要展示的数据(model的set方法重写)
- view的.h不暴露任何子控件属性,所有的展示逻辑全部放在.m内部
- 模型Model:
- 提供数据展示所需的属性
- 提供一个字典转模型的方法(当然也可以不提供,通过MJExtension)
- 添加辅助属性(当服务器的返回数据不能直接展示时,我们可以添加辅助属性,然后从写该属性的get方法,在get方法中处理成可以直接展示的属性)
我理解的MVVM
MVVM介绍
- 模型(M):保存视图数据。
- 视图+控制器(V):展示内容 + 如何展示
- 视图模型(VM):处理展示的业务逻辑,包括按钮的点击,数据的请求和解析等等。
如何使用MVVM
- 之前界面的所有业务逻辑都交给控制器做处理
- 在MVVM架构中把控制器的业务全部搬去VM模型,也就是每个控制器对应一个VM模型.
- VM特点
- 视图模型,处理界面上的所有业务逻辑,每一个控制器对应一个VM模型
- 最好不要包括视图V
- 模型都是继承自
NSObject
,命名都是以**ViewModel
命名
MVVM编程步骤
- 先创建VM模型,把整个界面的一些业务逻辑处理完
- 回到控制器去执行
MVVM代码示例
- 步骤
- 控制器提供一个视图模型(requesViewModel),处理界面的业务逻辑
- VM提供一个命令,处理请求业务逻辑
- 在创建命令的block中,会把请求包装成一个信号,等请求成功的时候,就会把数据传递出去。
- 请求数据成功,应该把字典转换成模型,保存到视图模型(VM)中,控制器想用就直接从视图模型(VM)中获取。
- 假设控制器想展示内容到tableView,直接让视图模型(VM)成为tableView的数据源,把所有的业务逻辑交给视图模型(VM)去做,这样控制器的代码就非常少了。
- 代码如下:
-
控制器与view
//控制器部分 #import "ViewController.h" #import "RequestViewModel.h" #import "Book.h" #import "BookViewCell.h" @interface ViewController ()<UITableViewDataSource,UITableViewDelegate> @property (nonatomic,strong) RequestViewModel *viewModel; @property (nonatomic,weak) UITableView *tableView; @end @implementation ViewController //此时是将视图模型作为数据源 -(RequestViewModel *)viewModel{ if (_viewModel == nil) { _viewModel = [[RequestViewModel alloc] init]; } return _viewModel ; } - (void)viewDidLoad { [super viewDidLoad]; // 1.创建tableView UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds]; tableView.dataSource = self; tableView.delegate = self; [self.view addSubview:tableView]; self.tableView = tableView; [tableView registerNib:[UINib nibWithNibName:@"BookViewCell" bundle:nil] forCellReuseIdentifier:@"BookViewCell"]; //1.执行数据请求命令 [self.viewModel.requestCommand execute:@"CHAAAA"]; //2.监听命令执行过程,弹框提示 //skip1的原因是,程序已启动就回调用一次 [[self.viewModel.requestCommand.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) { if ([x boolValue] == YES) { //正在执行 NSLog(@"请求中"); //弹框提示正在登录 }else{ //执行完成,隐藏弹框 NSLog(@"请求完成"); } } error:^(NSError * _Nullable error) { NSLog(@"请求失败"); }]; //3.监听数据的改变,驱动视图(数据驱动视图的思想) [RACObserve(self.viewModel, models) subscribeNext:^(id _Nullable x) { [self.tableView reloadData]; }]; } #pragma mark - UITableViewDataSource -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 130; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ //此时只是多了一个VM return self.viewModel.models.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ BookViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BookViewCell"]; cell.bookModel = self.viewModel.models[indexPath.row]; return cell; } @end //View #import <UIKit/UIKit.h> @class Book; @interface BookViewCell : UITableViewCell @property (nonatomic,strong) Book *bookModel; @end #import "BookViewCell.h" #import "Book.h" @interface BookViewCell() @property (weak, nonatomic) IBOutlet UILabel *titleLabel; @property (weak, nonatomic) IBOutlet UILabel *subtitleLabel; @property (weak, nonatomic) IBOutlet UILabel *priceAndPubLabel; @end @implementation BookViewCell - (void)awakeFromNib { [super awakeFromNib]; // Initialization code } -(void)setBookModel:(Book *)bookModel{ _bookModel = bookModel; _titleLabel.text = bookModel.title; _subtitleLabel.text = bookModel.subtitle; _priceAndPubLabel.text = bookModel.priceAndPubdate; } @end
-
Model部分
#import <Foundation/Foundation.h> @interface Book : NSObject @property (nonatomic,copy) NSString *title; @property (nonatomic,copy) NSString *subtitle; @property (nonatomic,copy) NSString *price; @property (nonatomic,copy) NSString *pubdate; //辅助属性 @property (nonatomic,copy) NSString *priceAndPubdate; +(instancetype)bookWithDict:(NSDictionary *)dict; @end #import "Book.h" @implementation Book +(instancetype)bookWithDict:(NSDictionary *)dict{ Book *book = [[self alloc] init]; book.title = dict[@"title"]; book.subtitle = dict[@"subtitle"]; book.price = dict[@"price"]; book.pubdate = dict[@"pubdate"]; return book; } -(NSString *)priceAndPubdate{ _priceAndPubdate = [NSString stringWithFormat:@"%@/%@",_price,_pubdate]; return _priceAndPubdate; } @end
-
视图(VM)部分
#import <Foundation/Foundation.h> #import <ReactiveObjC/ReactiveObjC.h> @interface RequestViewModel : NSObject /*网络请求命令*/ @property (nonatomic,strong) RACCommand *requestCommand; //模型数组 @property (nonatomic, strong) NSArray *models; @end #import "RequestViewModel.h" #import <AFNetworking/AFNetworking.h> #import "Book.h" @implementation RequestViewModel -(instancetype)init{ if (self = [super init]) { [self setup]; } return self; } -(void)setup{ //1.执行请求命令 _requestCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) { NSLog(@"======%@",input); //2. 创建请求信号 RACSignal *requestSignal =[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { //3.发送网络请求 //创建请求管理者 AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; [manager GET:@"https://api.douban.com/v2/book/search" parameters:@{@"q":@"基础"} progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { NSLog(@"%@",responseObject); [subscriber sendNext:responseObject]; [subscriber sendCompleted]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { [subscriber sendError:error]; }]; return nil; }]; //4.处理数据 //将原始数据映射出模型数组 RACSignal *dealSignal = [requestSignal map:^id _Nullable(NSDictionary * _Nullable value) { NSArray *dicAr = value[@"books"]; //将字典映射成模型 NSArray *modelArray = [[dicAr.rac_sequence map:^id _Nullable(id _Nullable value) { //一一映射,字典转模型 return [Book bookWithDict:value]; }] array]; return modelArray; }] ; //返回处理后的信号 return dealSignal; }]; //5.执行命令后更新数据源 //使用这个 // @weakify(self); // [_requestCommand.executionSignals.switchToLatest subscribeNext:^(id _Nullable x) { // @strongify(self); // self.models = x; // }]; //或者 RAC(self,models) = _requestCommand.executionSignals.switchToLatest; } @end
-
MVVM的特点:
- 控制器C:
- 控制器有一个视图模型(VM)属性,此时是将视图模型作为数据源
- 控制器通过VM执行发送数据请求命令
- 控制器中通过VM监听数据请求的过程
- 控制器中监听VM中数据源属性的改变,从而驱动(刷新)视图
- VM(RequestViewModel):
- 提供一个数据请求命令,用于执行数据请求
- 拥有一个数据源属性(把MVC控制器中的数据源搬到这里来)
- 数据处理,发送网络请求更新模型,搬到了这里
- View与MVC 一样仍然不变
- Model与MVC 一样仍然不变
- 注意:
- 不应该在VM中出现View
- 用RAC实现数据驱动视图的思想
MVVM用于收集数据上传之登录界面
- 情况: sb上分别有账号/密码输入框,两者都有内容时,登录按钮才使能,反之,不使能.
-
代码举例:
//控制器 #import "ViewController.h" #import <ReactiveObjC/ReactiveObjC.h> #import "LoginViewModel.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UITextField *accountTextfield; @property (weak, nonatomic) IBOutlet UITextField *pwdTextField; @property (weak, nonatomic) IBOutlet UIButton *loginBtn; @property (nonatomic,strong) LoginViewModel *loginVM; @end @implementation ViewController -(LoginViewModel *)loginVM{ if (_loginVM == nil) { _loginVM = [[LoginViewModel alloc] init]; } return _loginVM ; } - (void)viewDidLoad { [super viewDidLoad]; [self bindViewModel]; [self loginEvent]; } -(void)bindViewModel{ //1. 给视图模型的账号和密码绑定信号(只要文本框内容改变,就回给属性赋值) RAC(self.loginVM,account) = _accountTextfield.rac_textSignal; RAC(self.loginVM,pwd) = _pwdTextField.rac_textSignal; } //登录事件 -(void)loginEvent{ //1.处理文本框的业务逻辑 //设置按钮能否点击 RAC(_loginBtn,enabled) = self.loginVM.loginEnableSignal; //2. 监听登录按钮点击 [[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) { NSLog(@"点击了登录按钮"); //处理登录事件(只要是处理事件,就要想到用命令类(Racmmand) //7.执行命令 [self.loginVM.loginCommand execute:nil]; }]; //3. 处理登录的执行过程 //skip1的原因是,程序已启动就回调用一次 [[self.loginVM.loginCommand.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) { if ([x boolValue] == YES) { //正在执行 NSLog(@"正在执行"); //弹框提示正在登录 }else{ //执行完成,隐藏弹框 NSLog(@"执行完成"); } }]; } @end //视图(VM)部分 #import <Foundation/Foundation.h> #import <ReactiveObjC/ReactiveObjC.h> @interface LoginViewModel : NSObject //保存登录界面的账号和密码 //账号 @property (nonatomic,strong) NSString *account; //密码 @property (nonatomic,strong) NSString *pwd; //处理登录按钮是否允许点击 @property (nonatomic,strong,readonly) RACSignal *loginEnableSignal; //登录按钮命令 @property (nonatomic,strong,readonly) RACCommand *loginCommand; @end #import "LoginViewModel.h" @implementation LoginViewModel -(instancetype)init{ if (self = [super init]) { [self setup]; } return self; } -(void)setup{ //1.处理登录点击按钮的信号() //RACObserve(self, account):只要self的account属性已改动,就会产生信号 _loginEnableSignal = [RACSignal combineLatest:@[RACObserve(self, account),RACObserve(self, pwd)] reduce:^id _Nullable(NSString *account ,NSString *pwd){ return @(account.length && pwd.length); }]; //2. 处理登录点击命令 _loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) { //block: 执行命令就回调用 //block作用: 事件处理(发送网络请求) //发送登录的网络请求 NSLog(@"发送网络请求返回的数据"); return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { //将数据发送出去 [subscriber sendNext:@"登录的网络请求数据"]; [subscriber sendCompleted]; return nil; }]; }]; //3. 处理登录请求返回的结果 [_loginCommand.executionSignals.switchToLatest subscribeNext:^(id _Nullable x) { NSLog(@"%@",x); }]; } @end
注意: 在自己项目中—脑卒中这块完全是用的MVVC,没事可以自己参考 Demo地址