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
相关推荐
Swift社区23 分钟前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
#摩斯先生24 分钟前
Swift从0开始学习 对象和类 day3
ios·xcode·swift
没头脑的ht25 分钟前
Swift内存访问冲突
开发语言·ios·swift
#摩斯先生25 分钟前
Swift从0开始学习 并发性 day4
ios·xcode·swift
没头脑的ht28 分钟前
Swift闭包的本质
开发语言·ios·swift
Jinkey6 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
程序猿看视界13 小时前
如何在 UniApp 中实现 iOS 版本更新检测
ios·uniapp·版本更新
dr李四维16 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
️ 邪神16 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
比格丽巴格丽抱1 天前
flutter项目苹果编译运行打包上线
flutter·ios