0%

iOS——教你如何使用ReactiveCocoa和MVVM为代码解耦构建清爽APP

1. MVVM简介

不过多赘述MVC,用最通俗的方式解说MVVM。

  1. 拆解:
    1. M: Model ,包括数据模型、访问数据库的操作和网络请求等
    2. V: View ,包括了iOS中的 Viewcontroller 组成,负责 UI 的展示,绑定 viewModel 中的属性
    3. VM: ViewModel ,负责从 Model 中获取 View 所需的数据,转换成 View 可以展示的数据,并暴露公开的属性和命令供 View 进行绑定
    4. Binder:这是我最近发现的,在标准MVVM中没有提到的一部分,但是如果使用MVVM + ReactiveCocoa就会自然地写出这一层。这一层主要为了实现响应式编程的功能,实现 ViewViewModel 的同步
  1. MVC——>MVVM:

    MVC是苹果官方推荐的开发模式,但是伴随这这模式产生的问题非常的多,这是随着项目的逐渐扩大、架构的逐渐复杂显示出来的,这也是为什么MVC也被调侃成Massive View Controller(重量级视图控制器)。大多数情况下,小型项目MVC开发不会带来太大的负担,即使你将大量的逻辑代码(不包括通用的工具类逻辑)放在了ViewController中,但只要该部分由一个人维护,相对来说还是可以保持逻辑清晰的。

    但当项目越来越大时,或者一个模块会有多个人维护时,读代码变成了一件非常困难的事,并且,MVC模式的iOS开发一直存在难以测试的问题。博主在做JAVA开发时JUnit的测试就像每天的必修课一样。开始iOS开发后,加上第二家公司一直没有QA,线上发现的BUG简直就是每天的噩梦。MVVM带来的好处是 VM 层可以方便的做测试,因为 VM 层是独立的逻辑,脱离对 ViewModel 的依赖。

    少写字,多写代码,赶紧进入下一部分介绍,尽快去了解如何编码。

2. ReactiveCocoa

2.1 简介

  1. 简介:ReactiveCocoa简称RAC,集合了函数式编程响应式编程,这也是为什么ReactiveCocoa被描述为函数响应式编程(FRP)框架。

  2. ReactiveCocoa解决的问题:iOS开发中有多种事件处理方式,相信你一定也曾想过这些坑爹的地方,通常有这些事件处理方式:Action、delegate、Notification、KVO。并且通常这些代码总是散落在代码的各个角落,几度分散。ReactiveCocoa为事件提供了很多处理方法,可以把要处理的事情,和监听的事情的代码放在一起,非常方便管理。

  3. 关于ReactiveCocoa的基本用法,希望你能认真的阅读这篇博文

2.2 常用宏

  1. RAC(TARGET, [KEYPATH, [NIL_VALUE]]):用于给某个对象的某个属性绑定。
1
2
// 文本框文字改变时修改label的文字
RAC(self.labelView,text) = _textField.rac_textSignal;
  1. **RACObserve(self, name):**监听某个对象的某个属性,返回的是信号,可以用来代替KVO
1
2
3
[RACObserve(self.view, center) subscribeNext:^(id x) {
NSLog(@"%@",x);
}];

3. 实践

3.1 实现内容

做一个简单的登陆功能,两个输入框,一个登陆按钮。
简单的用户名密码验证,要求都在6位数以上即可,不符合要求时禁用登陆按钮。

3.2 Coding

界面大概是这样的感觉,简单的拉一个即可:

禁用登录
启用登录

M层

抽出简单的User模型,Thin Model,不包含功能型方法:

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface User : NSObject
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *password;
@end

VM层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import <Foundation/Foundation.h>
#import "ReactiveCocoa.h"

@interface LoginViewModel : NSObject

@property (nonatomic, strong) NSString *userName;
@property (nonatomic, strong) NSString *password;
// 成功信号
@property (nonatomic, strong) RACSubject *successSubject;
// 失败信号
@property (nonatomic, strong) RACSubject *failureSubject;
// 错误信号
@property (nonatomic, strong) RACSubject *errorSubject;

/**
* 按钮是否可点信号
*
* @return
*/
- (RACSignal *)validSignal;
/**
* 登陆操作
*/
- (void)login;

@end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#import "LoginViewModel.h"
#import "User.h"

@interface LoginViewModel ()

/** 用户名改变信号 */
@property (nonatomic, strong) RACSignal *userNameSignal;
/** 密码改变信号 */
@property (nonatomic, strong) RACSignal *passwordSignal;
/** 请求数据(模拟) */
@property (nonatomic, strong) NSArray *requestData;

@end


@implementation LoginViewModel

- (instancetype)init {
if (self = [super init]) {
// RACObserve(self, name):监听某个对象的某个属性,返回的是信号。
_userNameSignal = RACObserve(self, userName);
_passwordSignal = RACObserve(self, password);
_successSubject = [RACSubject subject];
_failureSubject = [RACSubject subject];
_errorSubject = [RACSubject subject];
}
return self;
}

/**
* 按钮是否可点信息
*
* @return
*/
- (RACSignal *)validSignal {
RACSignal *validSignal = [RACSignal combineLatest:@[_userNameSignal, _passwordSignal] reduce:^id(NSString *userName, NSString *password){
// 要求用户名和密码大于6位数
return @(userName.length >= 6 && password.length >= 6);
}];
return validSignal;
}

/**
* 登陆操作
*/
- (void)login{
// 网络请求进行登录,当然这里只是模拟一下
User *user = [[User alloc] init];
user.username = self.userName;
user.password = self.password;
_requestData = @[user];
// 成功发送成功的信号
[_successSubject sendNext:_requestData];
// 如果失败发送失败的信息号
}

@end

通过这一层,你是否发现,VM是可以单独测试的,也是可以单独编写的,即使你没有写 View 层,你可以编写针对VM的Unit进行功能测试,确保VM无误后继续编写后续代码。

V层

首先我们要通过RAC实现一部分UI的功能——输入文字的时候同步将文字保存起来&&控制按钮的禁用状态。

想想看我们通常是怎么做的?

  1. 通过实现UITextField的代理
  2. - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string; 方法中获取输入的文字,赋值给username属性和password属性
  3. 再判断username和password是否符合要求
  4. 再设置按钮的enabled属性

是不是看一看就觉得乱糟糟的,按钮的addTarget在一个地方,代理又在一个地方,再加上判断用户名密码合法逻辑单独抽出的方法。OMG。

ReactiveCocoa是怎么做的?

1
2
3
RAC(self.viewModel, userName) = self.tfUserName.rac_textSignal;
RAC(self.viewModel, password) = self.tfPassword.rac_textSignal;
RAC(self.btLogin, enabled) = [self.viewModel validSignal];

三句话,清清爽爽。

再加上 ViewModel 的信号绑定,将上面的代码放到一个方法中,命名为bindModel

最后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#import "ViewController.h"
#import "ReactiveCocoa.h"
#import "LoginViewModel.h"
#import "User.h"

@interface ViewController ()
@property (nonatomic, strong) LoginViewModel *viewModel;
@property (weak, nonatomic ) IBOutlet UITextField *tfUserName;
@property (weak, nonatomic ) IBOutlet UITextField *tfPassword;
@property (weak, nonatomic ) IBOutlet UIButton *btLogin;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
[self bindModel];
}

/**
* 绑定Model中的各种事件
*/
- (void)bindModel {
self.viewModel = [[LoginViewModel alloc] init];
RAC(self.viewModel, userName) = self.tfUserName.rac_textSignal;
RAC(self.viewModel, password) = self.tfPassword.rac_textSignal;
RAC(self.btLogin, enabled) = [self.viewModel validSignal];

// @weakify(self);
// 订阅登录成功信号并作出处理
[self.viewModel.successSubject subscribeNext:^(NSArray * x) {
// @strongify(self);
User *user = x[0];
NSLog(@"username:%@\tpassword:%@", user.username, user.password);
NSLog(@"登陆成功");
}];

// 订阅登录失败信号并作出处理
[self.viewModel.failureSubject subscribeNext:^(id x) {
NSLog(@"登陆失败");
}];

// 订阅登录错误信号并作出处理
[self.viewModel.errorSubject subscribeNext:^(id x) {
NSLog(@"登陆错误");
}];

// 添加按钮点击事件
[[self.btLogin rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
[self.viewModel login];
}];

}

@end

这样,ViewController中事件处理的所有代码被集中在一起,方便管理,你的代码变得如此清爽、低耦合。

代码

以上代码地址请点这里

补充

小鱼最近在换工作哟~各路的朋友有推荐的请务必介绍我哟~
简历在这里


有什么问题都可以在博文后面留言,或者微博上私信我。

博主是 iOS 妹子一枚。

希望大家一起进步。

我的微博:Lotty周小鱼