ReactiveCocoa + MVVM 实战入门

2018-11-27  本文已影响0人  书写不简单

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

1. 介绍MVVM架构思想

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

1.2 常见的架构思想:

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

上一篇下一篇

猜你喜欢

热点阅读