学习了在MVVM中如何使用RactiveCocoa,简单的写上一个demo。重点在于如何在MVVM各层之间使用RAC的信号来更方便的在各个层之间进行响应式数据交互。
demo需求:一个登录界面(登录界面只有账号和密码都有输入,登录按钮才可以点击操作),一个登录成功后跳转展示界面。
ViewModel中是负责登录的业务逻辑层,该层中负责数据验证,网络请求,数据存储等一些和ui无关的业务逻辑。
因为ViewModel层是独立于UI层而存在的,所以可以在没有UI的情况下我们就可以去实现相应模块的ViewModel层。这正好减少了个个层次间的耦合性,同时也提高了可测试性,总体上改善了可维护性。好废话少说,接下来要实现登录的Model层。
//
// 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层。
//
// 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;发起网络请求,调用登录网络接口。
//
// 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类。
//
// 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就实现完毕。
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