iOS ReactiveCocoa MVVM

学习了在MVVM中如何使用RactiveCocoa,简单的写上一个demo。重点在于如何在MVVM各层之间使用RAC的信号来更方便的在各个层之间进行响应式数据交互。

demo需求:一个登录界面(登录界面只有账号和密码都有输入,登录按钮才可以点击操作),一个登录成功后跳转展示界面。

ViewModel中是负责登录的业务逻辑层,该层中负责数据验证,网络请求,数据存储等一些和ui无关的业务逻辑。

因为ViewModel层是独立于UI层而存在的,所以可以在没有UI的情况下我们就可以去实现相应模块的ViewModel层。这正好减少了个个层次间的耦合性,同时也提高了可测试性,总体上改善了可维护性。好废话少说,接下来要实现登录的Model层。

c 复制代码
//
//  User.h
//  RACDemo
//
//  Created by johnny on 2024/6/5.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface User : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, copy) NSString *pwd;

@end

NS_ASSUME_NONNULL_END

接下来要实现登录的ViewModel层。

c 复制代码
//
//  UserViewModel.h
//  RACDemo
//
//  Created by johnny on 2024/6/5.
//

#import <Foundation/Foundation.h>
#import "ReactiveObjC.h"
#import "User.h"

NS_ASSUME_NONNULL_BEGIN

@interface UserViewModel : NSObject

@property (nonatomic, strong) User *user;

// 登录成功
@property (nonatomic, strong) RACSubject *successObject;

// 认证失败
@property (nonatomic, strong) RACSubject *failObject;

// 登录失败
@property (nonatomic, strong) RACSubject *errorObject;

// 登录按钮是否可用
- (id)loginBtnIsValid;

// 登录
-(void)login;

@end

NS_ASSUME_NONNULL_END

登录ViewModel层对应的类的头文件中内容如上所示。其实上面一些常用的信号可以抽象出来放到ViewModel的父类中。属性user是用来绑定用户输入的用户名和密码。上面三个定义信号successObject,failObject,errorObject用来发送网络请求的数据。

successObject 负责处理网络请求成功且符合正常业务逻辑事件。

failObject 负责处理网络请求成功不符合正常业务逻辑的处理

errorObject 负责处理网络异常处理。

结合项目中的实例来解释一下什么时候发送successObject信号,如何发送failureObject信号,何时使用errorObject信号。

以某些理财App中购买理财产品的业务流程为例。在用户下单之前先去判断用户是否实名认证以及绑定银行卡,如果用户已经实名和绑定银行卡就走正常支付流程(用户就是想去下单购买),VM就往VC发送successObject信号,当前VC就会根据信号的指示跳转到下单支付页面。 但是如果用户没有实名或者绑卡,那么VM就给VC发送failureObject信号,根据信号中的参数来判断是走实名认证流程还是走绑定银行卡流程。 errorObject就比较简单了,网络异常,后台服务器抛出的异常等不需要iOS这边做业务逻辑处理的,就放在errorObject中负责错误信息的展示。

文字说完了,如果有些小伙伴还不太明白,那看下面这张原理图吧。把三种信号我们可以类比成十字路口的红绿灯。successObject就是绿灯,可以走正常流程。failureObject是黄灯,先等一下,完成该做的就可以走绿灯了。而errorObject就是一红灯,报错异常,终止业务流程并提升错误信息。有图有真相,到这儿如果还不理解我就没招了。

备注:借用的图片

在Public方法中- (id) buttonIsValid; 负责返回登录按钮是否可用的信号。- (void)login;发起网络请求,调用登录网络接口。

c 复制代码
//
//  UserViewModel.m
//  RACDemo
//
//  Created by johnny on 2024/6/5.
//

#import "UserViewModel.h"


@interface UserViewModel ()

@property (nonatomic, strong)RACSignal *userNameSignal;
@property (nonatomic, strong)RACSignal *pswSignal;

@property (nonatomic, strong) NSArray *requestData;

@end

@implementation UserViewModel

-(User *)user {
    if (_user == nil) {
        _user = [[User alloc]init];
    }
    return _user;
}
// VCViewModel的初始化方法如下,负责初始化属性
-(instancetype)init {
    if (self= [super init]) {
        [self initialize];
    }
    return self;
}


- (void)initialize {
    _userNameSignal = RACObserve(self, self.user.name);
    _pswSignal = RACObserve(self, self.user.pwd);
    
    // 初始化 success
    _successObject = [RACSubject subject];
    _failObject = [RACSubject subject];
    _errorObject = [RACSubject subject];
}

// 合并两个输入框信号,并返回按钮bool类型的值
-(id)loginBtnIsValid {
   RACSignal *isValid = [RACSignal combineLatest:@[_userNameSignal, _pswSignal] reduce:^id (NSString *user, NSString *psw){
        return @(user.length > 3 && psw.length > 3);
    }];
    return isValid;
}

// 模拟网络请求的发送,并发出网络请求成功的信号
-(void)login {
    // 网络请求
    _requestData = @[_user.name, _user.pwd];
    
    NSLog(@"网络请求");
    
    // 成功发送信号
    [_successObject sendNext:_requestData];
    
    [_failObject sendNext:@"认证失败"];
    
    [_errorObject sendNext:@"登录失败"];
}


@end

上面是VM的实现,如果要进行单元测试的话,就对相应的VM类进行初始化,调用相应的函数进行单元测试即可。接着就是看如何在相应的VC模块中使用VM。

剩下的就是在VC中实例化响应的VM类。

c 复制代码
//
//  LoginViewController.m
//  RACDemo
//
//  Created by johnny on 2024/6/5.
//

#import "LoginViewController.h"
#import "ReactiveObjC.h"
#import "UserViewModel.h"


@interface LoginViewController ()

@property (nonatomic, strong) UITextField *accountField;
@property (nonatomic, strong) UITextField *pwdField;
@property (nonatomic, strong) UIButton *loginBtn;

@property (nonatomic, strong) UserViewModel *viewModel;

@end

@implementation LoginViewController

-(UserViewModel *)viewModel {
    if (_viewModel == nil) {
        _viewModel = [[UserViewModel alloc]init];
    }
    return _viewModel;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor yellowColor];
    
    [self setUP];
    
    [self bindViewModel];
    
    //按钮点击事件
    @weakify(self);
    [[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
        NSLog(@"点击登录按钮");
        @strongify(self);
        [self.viewModel login];
    }];
    
    //登录成功要处理的方法
    [_viewModel.successObject subscribeNext:^(id  _Nullable x) {
        NSLog(@"successObject %@", x);
    }];
    
    //fail
    [_viewModel.failObject subscribeNext:^(id  _Nullable x) {
        NSLog(@"failObject %@", x);
    }];
    //error
    [_viewModel.errorObject subscribeNext:^(id  _Nullable x) {
        NSLog(@"errorObject %@", x);
    }];
}

//关联ViewModel
- (void)bindViewModel {
    RAC(self.viewModel.user, name) = _accountField.rac_textSignal;
    RAC(self.viewModel.user, pwd) = _pwdField.rac_textSignal;
    RAC(self.loginBtn, enabled) = _viewModel.loginBtnIsValid;
    
    [_viewModel.loginBtnIsValid subscribeNext:^(id  _Nullable x) {
        NSLog(@"loginBtnIsValid %@", x);
        self.loginBtn.backgroundColor = [UIColor systemPinkColor];
    }];
}

- (void)setUP {
    self.accountField = [[UITextField alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
    self.accountField.backgroundColor = [UIColor redColor];
    [self.view addSubview:self.accountField];
    
    self.pwdField = [[UITextField alloc]initWithFrame:CGRectMake(100, 160, 200, 40)];
    self.pwdField.backgroundColor = [UIColor greenColor];
    [self.view addSubview:self.pwdField];
    
    self.loginBtn = [[UIButton alloc]initWithFrame:CGRectMake(100, 400, 50, 50)];
    self.loginBtn.backgroundColor = [UIColor blackColor];
    [self.view addSubview:self.loginBtn];
}


@end

到此为止,一个完整模拟登录模块的RAC下的MVVM就实现完毕。

bash 复制代码
 platform :ios, '12.0'
 

target 'RACDemo' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

 pod 'ReactiveObjC'
 pod 'ReactiveCocoa'


end

post_install do |installer|
  
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['ENABLE_BITCODE'] = 'NO'
      # Xcode 14 以后 bundle 需要需要签名
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0'
#      config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
      config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
      config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"

    end
  end
end
相关推荐
问道飞鱼43 分钟前
【移动端知识】移动端多 WebView 互访方案:Android、iOS 与鸿蒙实现
android·ios·harmonyos·多webview互访
mascon2 小时前
U3D打包IOS的自我总结
ios
名字不要太长 像我这样就好2 小时前
【iOS】继承链
macos·ios·cocoa
karshey3 小时前
【IOS webview】IOS13不支持svelte 样式嵌套
ios
潜龙95273 小时前
第4.3节 iOS App生成追溯关系
macos·ios·cocoa
游戏开发爱好者812 小时前
iOS App 电池消耗管理与优化 提升用户体验的完整指南
android·ios·小程序·https·uni-app·iphone·webview
神策技术社区19 小时前
iOS 全埋点点击事件采集白皮书
大数据·ios·app
wuyoula20 小时前
iOS V2签名网站系统源码/IPA在线签名/全开源版本/亲测
ios
2501_9159184120 小时前
iOS 性能监控工具全解析 选择合适的调试方案提升 App 性能
android·ios·小程序·https·uni-app·iphone·webview
fishycx20 小时前
iOS 构建配置与 AdHoc 打包说明
ios