RAC系列4之 软件架构MVC与MVVM

常见的架构思想

  1. MVC
    1. M:模型 V:视图 C:控制器
  2. MVVM
    1. M:模型 V:视图+控制器 VM:视图模型
  3. MVCS
    1. M:模型 V:视图 C:控制器 C:服务类
  4. VIPER
    1. V:视图 I:交互器 P:展示器 E:实体 R:路由

MVC都是怎么回事?

控制器都干了什么?

  1. 展示UI(添加/赋值/刷新UI)
  2. 逻辑控制(界面跳转/各个子控件之间的逻辑)
  3. 处理数据
    1. 请求数据,获得模型数组
    2. 搜集界面的数据,处理上传到服务器

view都干了什么?

  1. 拿到数据源(model)赋值展示数据(cell)
  2. 封装:实现子view之间的逻辑,自身的功能
  3. 用户交互改变模型的数据

model都干了什么?

  1. 数据都转为属性
  2. 添加辅助属性
    1. 有时候服务器返回的数据并不能直接展示,需要处理后才能直接展示到view上,该属性一般为readonly,.m中自己添加下划线的成员变量,从写get方法,处理过程放在get方法中

MVVM想干啥?

  1. 抽取掉控制器中的不必要代码到viewModel中
  2. 从MVC中可以看出,控制器中哪些不必要呢?
    1. 各个子控件之间的逻辑过程
    2. 网络请求处理过程
    3. 网络上传数据处理的过程
  3. 抽取之后会变成什么效果呢?
    1. 控制器用viewModel来获取网络数据,处理数据源,更新模型数据
    2. 控制器用viewModel来搜集数据,上传服务器数据
    3. 控制器用viemModel来控制各个子控件的逻辑

iOS中MVC的标准架构

这里就不扯什么高大上的语言了,直接上代码

代码举例:

  1. 控制器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
    
  2. 模型(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
    
  3. 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 的特点)

  4. 控制器C:
    1. 控制器有一个数据源模型数组(当然有时也可以是单个模型)属性
    2. 控制器发送网络请求更新模型,然后刷新数据
  5. 视图View(cell):
    1. view有一个模型属性,通过这个模型来获取所要展示的数据(model的set方法重写)
    2. view的.h不暴露任何子控件属性,所有的展示逻辑全部放在.m内部
  6. 模型Model:
    1. 提供数据展示所需的属性
    2. 提供一个字典转模型的方法(当然也可以不提供,通过MJExtension)
    3. 添加辅助属性(当服务器的返回数据不能直接展示时,我们可以添加辅助属性,然后从写该属性的get方法,在get方法中处理成可以直接展示的属性)

我理解的MVVM

MVVM介绍

  1. 模型(M):保存视图数据。
  2. 视图+控制器(V):展示内容 + 如何展示
  3. 视图模型(VM):处理展示的业务逻辑,包括按钮的点击,数据的请求和解析等等。

如何使用MVVM

  1. 之前界面的所有业务逻辑都交给控制器做处理
  2. 在MVVM架构中把控制器的业务全部搬去VM模型,也就是每个控制器对应一个VM模型.
  3. VM特点
    1. 视图模型,处理界面上的所有业务逻辑,每一个控制器对应一个VM模型
    2. 最好不要包括视图V
    3. 模型都是继承自NSObject,命名都是以**ViewModel命名

MVVM编程步骤

  1. 先创建VM模型,把整个界面的一些业务逻辑处理完
  2. 回到控制器去执行

MVVM代码示例

  1. 步骤
    1. 控制器提供一个视图模型(requesViewModel),处理界面的业务逻辑
    2. VM提供一个命令,处理请求业务逻辑
    3. 在创建命令的block中,会把请求包装成一个信号,等请求成功的时候,就会把数据传递出去。
    4. 请求数据成功,应该把字典转换成模型,保存到视图模型(VM)中,控制器想用就直接从视图模型(VM)中获取。
    5. 假设控制器想展示内容到tableView,直接让视图模型(VM)成为tableView的数据源,把所有的业务逻辑交给视图模型(VM)去做,这样控制器的代码就非常少了。
  2. 代码如下:
    1. 控制器与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
      
    2. 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
      
      
    3. 视图(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的特点:

  1. 控制器C:
    1. 控制器有一个视图模型(VM)属性,此时是将视图模型作为数据源
    2. 控制器通过VM执行发送数据请求命令
    3. 控制器中通过VM监听数据请求的过程
    4. 控制器中监听VM中数据源属性的改变,从而驱动(刷新)视图
  2. VM(RequestViewModel):
    1. 提供一个数据请求命令,用于执行数据请求
    2. 拥有一个数据源属性(把MVC控制器中的数据源搬到这里来)
    3. 数据处理,发送网络请求更新模型,搬到了这里
  3. View与MVC 一样仍然不变
  4. Model与MVC 一样仍然不变
  5. 注意:
    1. 不应该在VM中出现View
    2. 用RAC实现数据驱动视图的思想

MVVM用于收集数据上传之登录界面

  1. 情况: sb上分别有账号/密码输入框,两者都有内容时,登录按钮才使能,反之,不使能.
  2. 代码举例:

     //控制器
     #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地址

Table of Contents