IOS - MVVM + ReactiveCocoa 一个简单的登录样例

基础:

RACCommand的详细讲解

MVVM架构已经耳熟能详,网上有非常多的文章,讲的都相当不错,MVVM最主要的关系我这里要提醒一下

最基本的规则:

1.View持有ViewModel  反之不持有

2.ViewModel持有Model 反之不持有

网上copy了一张关系图

  • MVVM 的基本概念

在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件

view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel)

viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方

使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性

  • MVVM 的注意事项

view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)

viewModel 引用model,但反过来不行

  • MVVM 的使用建议

MVVM 可以兼容你当下使用的MVC架构。

MVVM 增加你的应用的可测试性。

MVVM 配合一个绑定机制效果最好(PS:ReactiveCocoa你值得拥有)。

viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。

viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。

viewModel 绝对不能包含视图 view(UIKit.h),不然就跟 view 产生了耦合,不方便复用和测试。

viewModel之间可以有依赖。

viewModel避免过于臃肿,否则重蹈Controller的覆辙,变得难以维护。

  • MVVM 的优势

低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上

可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑

独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计

可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试

  • MVVM 的弊端

数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。

(以上是网上copy来的)

案例代码

pod

target 'demo' do
  pod 'AFNetworking', '~> 3.0'
  pod 'FBSDKCoreKit', '~> 4.9'
  pod 'ReactiveObjC', '~> 3.1.0'
  pod 'SVProgressHUD', '~> 2.2.5'
end

LoginModel

#import <Foundation/Foundation.h>

@interface LoginModel : NSObject

@property (strong, nonatomic) NSString *userId;
@property (strong, nonatomic) NSString *displayName;

@end

LoginViewModel 

持有Model

网络请求,数据验证等等可以放在这里面处理 使用RAC进行数据的双向绑定 RAC是以信号的形式进行数据通信=》解耦 如果没有RAC的绑定机制,那我们需要自己使用delegate KVO等来实现数据的回调相对头疼

V 和 VM之间通过RACCommand命令做事件与数据的绑定 RACCommand不关心业务,它只负责业务间的通信(RACCommand会涉及RACSignal需要先去了解一下) V跟VM通过这个命令去做对应的逻辑处理,处理完毕后VM会通过信号[subscriber sendNext:xxx] 将数据回调给V,V只要订阅这个cmd的信号就可以收到数据了, 或者可以用V配合RACObserve来观察数据结果做展示也是可以的(KVO形式)


#import <Foundation/Foundation.h>
#import <ReactiveObjC.h>
#import "LoginModel.h"
@interface LoginViewModel : NSObject

@property (nonatomic,copy) NSString *account;
@property (nonatomic,copy) NSString *pwd;

@property (nonatomic,strong) NSError *error;
@property (nonatomic,strong) LoginModel *loginModel;

@property (nonatomic,strong) RACCommand *loginBtnEnableCmd;
@property (nonatomic,strong) RACCommand *loginActionCmd;

@end
#import "LoginViewModel.h"
#import <AFNetworking.h>

@implementation LoginViewModel
- (instancetype)init{
    if (self = [super init]) {
        //双向绑定
        [self initCommand];
        [self initSubscribe];
    }
    return self;
}

- (void)initSubscribe {
    [RACObserve(self, account) subscribeNext:^(id  _Nullable x) {
        [self checkSubmitEnable];
    }];
   [RACObserve(self, pwd) subscribeNext:^(id  _Nullable x) {
        [self checkSubmitEnable];
    }];
}

- (void)initCommand {
    self.loginBtnEnableCmd = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        return [self racForSubmitEnable];
    }];
    self.loginActionCmd = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
        return [self racForlogin];
    }];
}

- (void)checkSubmitEnable {
    [self.loginBtnEnableCmd execute:nil];
}

//校验登录按钮的状态
- (RACSignal *)racForSubmitEnable {
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        //模拟:3个字符便可
        BOOL status = self.account.length == 3 && self.pwd.length == 3;
        [subscriber sendNext:@(status)];
        [subscriber sendCompleted];
        return nil;
    }];
}

//登录操作
- (RACSignal *)racForlogin {
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        //模拟网络请求
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            LoginModel *result = [LoginModel new];
            if ([self.account isEqualToString:@"123"] && [self.pwd isEqualToString:@"123"]) {
                //模拟请求结果
                result.userId = [NSString stringWithFormat:@"%@%@",self.account,self.pwd];
                result.displayName = result.userId;
                self.error = nil;
            } else {
                //模拟错误
                self.error = [NSError errorWithDomain:@"-1" code:-1 userInfo:@{@"des":@"账号密码错误"}];
            }
            self.loginModel = result;
            //结果发送给监听方
            [subscriber sendNext:self];
            //设置完成
            [subscriber sendCompleted];
        });
        return nil;
    }];
}

@end

ViewController 

持有ViewModel 所有的逻辑都在ViewModel里面处理而自己只做简单的数据展示到控件上 不直接跟Model打交道

RACCommand进行数据的双向绑定通信


#import "ViewController.h"
#import <ReactiveObjC/ReactiveObjC.h>
#import "RAC.h"
#import <SVProgressHUD.h>
#import "LoginViewModel.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UITextField *txtAccount;
@property (weak, nonatomic) IBOutlet UITextField *txtPwd;
@property (weak, nonatomic) IBOutlet UIButton *btnSubmit;
@property (weak, nonatomic) IBOutlet UILabel *lblResult;

@property (strong,nonatomic) LoginViewModel *viewModel;

- (IBAction)touchAction:(id)sender;

@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
    //双向绑定
    [self initCommand];
    [self initSubscribe];
    //kvo监听数据回调
    //[self bindViewModel];
}

- (void)setupUI {
    self.btnSubmit.enabled = NO;
    [self.btnSubmit setBackgroundImage:[self createImageWithColor:[UIColor blueColor]] forState:UIControlStateNormal];
    [self.btnSubmit setBackgroundImage:[self createImageWithColor:[UIColor grayColor]] forState:UIControlStateDisabled];
}

-(UIImage*)createImageWithColor:(UIColor*) color{
    CGRect rect=CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
    UIGraphicsBeginImageContext(rect.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, [color CGColor]);
    CGContextFillRect(context, rect);
    UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return theImage;
}

- (void)initCommand {
    //关联赋值
    RAC(self.viewModel,account) = self.txtAccount.rac_textSignal;
    RAC(self.viewModel,pwd) = self.txtPwd.rac_textSignal;
}

- (void)initSubscribe {
    //VM内部处理按钮的可用状态 直接将结果告诉V
    [[self.viewModel.loginBtnEnableCmd.executionSignals switchToLatest] subscribeNext:^(id  _Nullable x) {
        self.btnSubmit.enabled = [x boolValue];
        self.lblResult.text = @"";
    }];
    
    //监听当前命令是否正在执行executing
    //监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次信号。
    //x:YES 当前cmd正在触发执行
    //x:NO 当前cmd不处于执行状态/或已处理完成
    [[self.viewModel.loginActionCmd.executing skip:1] subscribeNext:^(id  _Nullable x) {
        if ([x boolValue]) {
            [SVProgressHUD showWithStatus:@"正在请求"];
        }else{
            //[SVProgressHUD showSuccessWithStatus:@"加载完毕"];
        }
    }];
    //监听数据回调
    [[self.viewModel.loginActionCmd.executionSignals switchToLatest] subscribeNext:^(id  _Nullable x) {
        LoginViewModel *loginViewModel = x;
        if (!loginViewModel) {
            return ;
        }
        if (loginViewModel.error) {
            //错误信息自行处理
            self.lblResult.text = [NSString stringWithFormat:@"%@",loginViewModel.error.userInfo[@"des"]];
            [SVProgressHUD showInfoWithStatus:@"登录失败"];
        } else {
            self.lblResult.text = loginViewModel.loginModel.userId;
            [SVProgressHUD showSuccessWithStatus:@"登录成功"];
        }
    }];
}

/*
- (void)bindViewModel {
    //监听登录结果
    [RACObserve(self.viewModel, loginModel) subscribeNext:^(id  _Nullable x) {
        LoginModel *loginModel = x;//监听的是LoginModel对象的改变
        if (!loginModel) {
            return ;
        }
        if (self.viewModel.error) {
            //错误信息自行处理
            self.lblResult.text = [NSString stringWithFormat:@"%@",self.viewModel.error.userInfo[@"des"]];
            [SVProgressHUD showInfoWithStatus:@"登录失败"];
        } else {
            self.lblResult.text = loginModel.userId;
            [SVProgressHUD showSuccessWithStatus:@"登录成功"];
        }
    }];
}
 */

- (IBAction)touchAction:(id)sender {
    //以cmd的形式进行通信 告诉VM发起login请求(结果上面已经有做监听)
    [self.viewModel.loginActionCmd execute:nil];
}

-(LoginViewModel *)viewModel{
    if (_viewModel == nil) {
        _viewModel = [LoginViewModel new];
    }
    return _viewModel;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}



@end

结果 gif

猜你喜欢

转载自blog.csdn.net/aa741649143/article/details/82316857