美文网首页
ReactiveCocoa + MVVM 实战入门

ReactiveCocoa + MVVM 实战入门

作者: 书写不简单 | 来源:发表于2018-11-27 09:49 被阅读0次

    本文涉及的代码可以到这里下载demo

    1. 介绍MVVM架构思想

    1.1 程序为什么要架构:便于程序员开发和维护代码。

    1.2 常见的架构思想:

    • MVC M:模型 V:视图 C:控制器

    • MVVM M:模型 V:视图+控制器 VM:视图模型

    • MVCS M:模型 V:视图 C:控制器 C:服务类

    • VIPER V:视图 I:交互器 P:展示器 E:实体 R:路由
      PS:VIPER架构思想

    1.3 MVVM介绍

    模型(M):保存视图数据。

    视图+控制器(V):展示内容 + 如何展示

    视图模型(VM):处理展示的业务逻辑,包括按钮的点击,数据的请求和解析等等。

    2. ReactiveCocoa + MVVM 实战一:登录界面

    2.1需求+分析+步骤

    需求:1.监听两个文本框的内容,有内容才允许按钮点击
            2.默认登录请求.
     
       用MVVM:实现,之前界面的所有业务逻辑
       分析:1.之前界面的所有业务逻辑都交给控制器做处理
            2.在MVVM架构中把控制器的业务全部搬去VM模型,也就是每个控制器对应一个VM模型.
     
       步骤:1.创建LoginViewModel类,处理登录界面业务逻辑.
            2.这个类里面应该保存着账号的信息,创建一个账号Account模型
            3.LoginViewModel应该保存着账号信息Account模型。
            4.需要时刻监听Account模型中的账号和密码的改变,怎么监听?
            5.在非RAC开发中,都是习惯赋值,在RAC开发中,需要改变开发思维,由赋值转变为绑定,可以在一开始初始化的时候,就给Account模型中的属性绑定,并不需要重写set方法。
            6.每次Account模型的值改变,就需要判断按钮能否点击,在VM模型中做处理,给外界提供一个能否点击按钮的信号.
            7.这个登录信号需要判断Account中账号和密码是否有值,用KVO监听这两个值的改变,把他们聚合成登录信号.
            8.监听按钮的点击,由VM处理,应该给VM声明一个RACCommand,专门处理登录业务逻辑.
            9.执行命令,把数据包装成信号传递出去
            10.监听命令中信号的数据传递
            11.监听命令的执行时刻
    

    2.2 控制器的代码

    控制器BaseViewController.m 代码

    #import "BaseViewController.h"
    #import <ReactiveObjC.h>
    #import "LoginViewModel.h"
    #import <RACEXTScope.h>
    
    @interface BaseViewController ()
    @property (nonatomic, strong) UIButton *btnCommit;
    @property (nonatomic, strong) RACCommand *command;
    @property (nonatomic, strong) UITextField *fieldAccount;
    @property (nonatomic, strong) UITextField *fieldPass;
    @property (nonatomic, strong) LoginViewModel *loginViewModel;
    
    @end
    
    @implementation BaseViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // add subviews
        [self addSubviews];
        // bind model
        [self bindModel];
    }
    
    #pragma mark - addSubviews
    
    -(void)addSubviews{
        self.fieldAccount = [[UITextField alloc]initWithFrame:CGRectMake(100, 150, 200, 40)];
        _fieldAccount.textColor = [UIColor blackColor];
        _fieldAccount.backgroundColor = [UIColor whiteColor];
        _fieldAccount.layer.borderWidth = 0.5f;
        _fieldAccount.layer.borderColor = [UIColor lightGrayColor].CGColor;
        _fieldAccount.placeholder = @"请输入账号";
        [self.view addSubview:_fieldAccount];
        //
        self.fieldPass = [[UITextField alloc]initWithFrame:CGRectMake(100, 220, 200, 40)];
        _fieldPass.textColor = [UIColor blackColor];
        _fieldPass.placeholder = @"请输入密码";
        _fieldPass.backgroundColor = [UIColor whiteColor];
        _fieldPass.layer.borderWidth = 0.5f;
        _fieldPass.layer.borderColor = [UIColor lightGrayColor].CGColor;
        [self.view addSubview:_fieldPass];
        
        self.btnCommit = [UIButton buttonWithType:UIButtonTypeCustom];
        _btnCommit.frame = CGRectMake(100, 300, 100, 40);
        _btnCommit.backgroundColor = [UIColor whiteColor];
        [_btnCommit setTitle:@"登 录" forState:UIControlStateNormal];
        [_btnCommit setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
        _btnCommit.layer.borderWidth = 0.5f;
        _btnCommit.layer.borderColor = [UIColor lightGrayColor].CGColor;
        [self.view addSubview:_btnCommit];
        
    }
    
    #pragma mark -试图与VM的绑定
    -(void)bindModel{
        @weakify(self);
        
        // 将账号输入信号与Account中的账号绑定
        RAC(self.loginViewModel.account, account) = self.fieldAccount.rac_textSignal;
        
        // 将密码输入信号与Account中的密码绑定
        RAC(self.loginViewModel.account, password) = self.fieldPass.rac_textSignal;
        
        // 登录按钮能否点击,由信号决定,信号的返回值是Bool
        RAC(self.btnCommit, enabled) = self.loginViewModel.enableSignal;
        
        // 改变登录按钮的颜色
        [self.loginViewModel.enableSignal subscribeNext:^(id  _Nullable x) {
            @strongify(self);
            if ([x boolValue]) {
                [self.btnCommit setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
            }else{
                [self.btnCommit setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
            }
        }];
        
        // 监听按钮的点击
        [[_btnCommit rac_signalForControlEvents:UIControlEventTouchUpInside]subscribeNext:^(__kindof UIControl * _Nullable x) {
            @strongify(self);
            // 执行点击事件
            [self.loginViewModel.LoginCommand execute:@"登录试试看"];
        }];
        
    }
    
    #pragma mark - getter && setter
    - (LoginViewModel *)loginViewModel{
        if (!_loginViewModel) {
            self.loginViewModel = [[LoginViewModel alloc]init];
        }
        return _loginViewModel;
    }
    
    /*
    #pragma mark - Navigation
    
    // In a storyboard-based application, you will often want to do a little preparation before navigation
    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
        // Get the new view controller using [segue destinationViewController].
        // Pass the selected object to the new view controller.
    }
    */
    
    @end
    

    VM中的代码:

    // .h 中的代码
    #import <Foundation/Foundation.h>
    #import "Account.h"
    #import <ReactiveObjC.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface LoginViewModel : NSObject
    
    @property (nonatomic, strong) Account *account;
    /**
     * 每次Account模型的值改变,就需要判断按钮能否点击,在VM模型中做处理,给外界提供一个能否点击按钮的信号.由于这个信号由其他两个信号决定,所以该信号此处应该聚合而成。
     */
    @property (nonatomic, strong) RACSignal *enableSignal;
    
    /**
     * 处理点击事件
     */
    @property (nonatomic, strong, readonly) RACCommand *LoginCommand;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    // .m 中的代码
    #import "LoginViewModel.h"
    #import <SVProgressHUD.h>
    
    
    @implementation LoginViewModel
    
    - (instancetype)init{
        if (self = [super init]) {
            [self initialBind];
        }
        return self;
    }
    
    -(void)initialBind{
        
        // 监听账号的属性值改变,把他们聚合成一个信号。
        self.enableSignal = [RACSignal combineLatest:@[RACObserve(self.account, account), RACObserve(self.account, password)] reduce:^id _Nonnull{
            return @(self.account.account.length && self.account.password.length);
        }];
        //
        // 处理登录业务逻辑
        _LoginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            
            NSLog(@"点击了登录");
            return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                
                // 模仿网络延迟
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    
                    [subscriber sendNext:@"登录成功"];
                    
                    // 数据传送完毕,必须调用完成,否则命令永远处于执行状态
                    [subscriber sendCompleted];
                    
                });
                
                return nil;
            }];
        }];
        
        // 监听登录产生的数据
        [_LoginCommand.executionSignals.switchToLatest subscribeNext:^(id x) {
            
            if ([x isEqualToString:@"登录成功"]) {
                NSLog(@"登录成功");
            }
        }];
        
        // 监听登录状态
        [[_LoginCommand.executing skip:1] subscribeNext:^(id x) {
            if ([x isEqualToNumber:@(YES)]) {
                
                // 正在登录ing...
                // 用蒙版提示
                [SVProgressHUD showInfoWithStatus:@"正在登录..."];
                
                
            }else
            {
                // 登录成功
                // 隐藏蒙版
                [SVProgressHUD showSuccessWithStatus:@"登录成功"];
            }
        }];
        
        
        
    }
    
    - (Account *)account{
        if (!_account) {
            self.account = [[Account alloc]init];
        }
        return _account;
    }
    
    @end
    
    

    Account中的属性声明

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Account : NSObject
    
    @property (nonatomic, copy) NSString *password;
    @property (nonatomic, copy) NSString *account;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    

    ReactiveCocoa + MVVM 实战二:网络请求数据

    2.1 接口:这里先给朋友介绍一个免费的网络数据接口,豆瓣。可以经常用来练习一些网络请求的小Demo.
    2.2 需求+分析+步骤

    /*
        需求:请求豆瓣图书信息,url:https://api.douban.com/v2/book/search?q=基础
        
        分析:请求一样,交给VM模型管理
     
        步骤:
            1.控制器提供一个视图模型(requesViewModel),处理界面的业务逻辑
            2.VM提供一个命令,处理请求业务逻辑
            3.在创建命令的block中,会把请求包装成一个信号,等请求成功的时候,就会把数据传递出去。
            4.请求数据成功,应该把字典转换成模型,保存到视图模型中,控制器想用就直接从视图模型中获取。
            5.假设控制器想展示内容到tableView,直接让视图模型成为tableView的数据源,把所有的业务逻辑交给视图模型去做,这样控制器的代码就非常少了。
     */
    

    2.3控制器代码

    @interface ViewController ()
    
    @property (nonatomic, weak) UITableView *tableView;
    
    @property (nonatomic, strong) RequestViewModel *requesViewModel;
    
    
    @end
    
    @implementation ViewController
    - (RequestViewModel *)requesViewModel
    {
        if (_requesViewModel == nil) {
            _requesViewModel = [[RequestViewModel alloc] init];
        }
        return _requesViewModel;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        // 创建tableView
        UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
        tableView.dataSource = self.requesViewModel;
        
        [self.view addSubview:tableView];
        
        // 执行请求
     RACSignal *requesSiganl = [self.requesViewModel.reuqesCommand execute:nil];
       
       // 获取请求的数据
        [requesSiganl subscribeNext:^(NSArray *x) {
            
            self.requesViewModel.models = x;
            
            [self.tableView reloadData];
            
        }];
    
    }
    
    
    @end
    

    2.4视图模型(VM)代码

    @interface RequestViewModel : NSObject<UITableViewDataSource>
    
    
        // 请求命令
        @property (nonatomic, strong, readonly) RACCommand *reuqesCommand;
    
        //模型数组
        @property (nonatomic, strong, readonly) NSArray *models;
    
    
    
    @end
    
    @implementation RequestViewModel
    
    - (instancetype)init
    {
        if (self = [super init]) {
            
            [self initialBind];
        }
        return self;
    }
    
    
    - (void)initialBind
    {
        _reuqesCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            
            RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
               
                
                NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
                parameters[@"q"] = @"基础";
                
                // 发送请求
                [[AFHTTPRequestOperationManager manager] GET:@"https://api.douban.com/v2/book/search" parameters:parameters success:^(AFHTTPRequestOperation * _Nonnull operation, id  _Nonnull responseObject) {
                    NSLog(@"%@",responseObject);
                    
                    // 请求成功调用
                    // 把数据用信号传递出去
                    [subscriber sendNext:responseObject];
                    
                    [subscriber sendCompleted];
                    
                    
                } failure:^(AFHTTPRequestOperation * _Nonnull operation, NSError * _Nonnull error) {
                    // 请求失败调用
                    
                }];
                
                return nil;
            }];
            
            
            
         
            // 在返回数据信号时,把数据中的字典映射成模型信号,传递出去
            return [requestSignal map:^id(NSDictionary *value) {
                NSMutableArray *dictArr = value[@"books"];
    
                // 字典转模型,遍历字典中的所有元素,全部映射成模型,并且生成数组
                NSArray *modelArr = [[dictArr.rac_sequence map:^id(id value) {
                 
                    return [Book bookWithDict:value];
                }] array];
                
                return modelArr;
            }];
            
        }];
        
     }
    
    #pragma mark - UITableViewDataSource
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
        return self.models.count;
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        static NSString *ID = @"cell";
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
        if (cell == nil) {
            
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
        }
        
        Book *book = self.models[indexPath.row];
        cell.detailTextLabel.text = book.subtitle;
        cell.textLabel.text = book.title;
        
        return cell;
    }
    
    @end
    
    

    相关文章

      网友评论

          本文标题:ReactiveCocoa + MVVM 实战入门

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