ReactiveCocoa + MVVM 实战入门
2018-11-27 本文已影响0人
书写不简单
本文涉及的代码可以到这里下载demo
1. 介绍MVVM架构思想
1.1 程序为什么要架构:便于程序员开发和维护代码。
1.2 常见的架构思想:
-
MVC
M:模型 V:视图 C:控制器 -
MVVM
M:模型 V:视图+控制器 VM:视图模型 -
MVCS
M:模型 V:视图 C:控制器 C:服务类 -
VIPER
V:视图 I:交互器 P:展示器 E:实体 R:路由
PS:VIPER架构思想
1.3 MVVM介绍
模型(M):保存视图数据。
视图+控制器(V):展示内容 + 如何展示
视图模型(VM):处理展示的业务逻辑,包括按钮的点击,数据的请求和解析等等。
2. ReactiveCocoa + MVVM 实战一:登录界面
2.1需求+分析+步骤
需求:1.监听两个文本框的内容,有内容才允许按钮点击
2.默认登录请求.
用MVVM:实现,之前界面的所有业务逻辑
分析:1.之前界面的所有业务逻辑都交给控制器做处理
2.在MVVM架构中把控制器的业务全部搬去VM模型,也就是每个控制器对应一个VM模型.
步骤:1.创建LoginViewModel类,处理登录界面业务逻辑.
2.这个类里面应该保存着账号的信息,创建一个账号Account模型
3.LoginViewModel应该保存着账号信息Account模型。
4.需要时刻监听Account模型中的账号和密码的改变,怎么监听?
5.在非RAC开发中,都是习惯赋值,在RAC开发中,需要改变开发思维,由赋值转变为绑定,可以在一开始初始化的时候,就给Account模型中的属性绑定,并不需要重写set方法。
6.每次Account模型的值改变,就需要判断按钮能否点击,在VM模型中做处理,给外界提供一个能否点击按钮的信号.
7.这个登录信号需要判断Account中账号和密码是否有值,用KVO监听这两个值的改变,把他们聚合成登录信号.
8.监听按钮的点击,由VM处理,应该给VM声明一个RACCommand,专门处理登录业务逻辑.
9.执行命令,把数据包装成信号传递出去
10.监听命令中信号的数据传递
11.监听命令的执行时刻
2.2 控制器的代码
控制器BaseViewController.m 代码
#import "BaseViewController.h"
#import <ReactiveObjC.h>
#import "LoginViewModel.h"
#import <RACEXTScope.h>
@interface BaseViewController ()
@property (nonatomic, strong) UIButton *btnCommit;
@property (nonatomic, strong) RACCommand *command;
@property (nonatomic, strong) UITextField *fieldAccount;
@property (nonatomic, strong) UITextField *fieldPass;
@property (nonatomic, strong) LoginViewModel *loginViewModel;
@end
@implementation BaseViewController
- (void)viewDidLoad {
[super viewDidLoad];
// add subviews
[self addSubviews];
// bind model
[self bindModel];
}
#pragma mark - addSubviews
-(void)addSubviews{
self.fieldAccount = [[UITextField alloc]initWithFrame:CGRectMake(100, 150, 200, 40)];
_fieldAccount.textColor = [UIColor blackColor];
_fieldAccount.backgroundColor = [UIColor whiteColor];
_fieldAccount.layer.borderWidth = 0.5f;
_fieldAccount.layer.borderColor = [UIColor lightGrayColor].CGColor;
_fieldAccount.placeholder = @"请输入账号";
[self.view addSubview:_fieldAccount];
//
self.fieldPass = [[UITextField alloc]initWithFrame:CGRectMake(100, 220, 200, 40)];
_fieldPass.textColor = [UIColor blackColor];
_fieldPass.placeholder = @"请输入密码";
_fieldPass.backgroundColor = [UIColor whiteColor];
_fieldPass.layer.borderWidth = 0.5f;
_fieldPass.layer.borderColor = [UIColor lightGrayColor].CGColor;
[self.view addSubview:_fieldPass];
self.btnCommit = [UIButton buttonWithType:UIButtonTypeCustom];
_btnCommit.frame = CGRectMake(100, 300, 100, 40);
_btnCommit.backgroundColor = [UIColor whiteColor];
[_btnCommit setTitle:@"登 录" forState:UIControlStateNormal];
[_btnCommit setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
_btnCommit.layer.borderWidth = 0.5f;
_btnCommit.layer.borderColor = [UIColor lightGrayColor].CGColor;
[self.view addSubview:_btnCommit];
}
#pragma mark -试图与VM的绑定
-(void)bindModel{
@weakify(self);
// 将账号输入信号与Account中的账号绑定
RAC(self.loginViewModel.account, account) = self.fieldAccount.rac_textSignal;
// 将密码输入信号与Account中的密码绑定
RAC(self.loginViewModel.account, password) = self.fieldPass.rac_textSignal;
// 登录按钮能否点击,由信号决定,信号的返回值是Bool
RAC(self.btnCommit, enabled) = self.loginViewModel.enableSignal;
// 改变登录按钮的颜色
[self.loginViewModel.enableSignal subscribeNext:^(id _Nullable x) {
@strongify(self);
if ([x boolValue]) {
[self.btnCommit setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
}else{
[self.btnCommit setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
}
}];
// 监听按钮的点击
[[_btnCommit rac_signalForControlEvents:UIControlEventTouchUpInside]subscribeNext:^(__kindof UIControl * _Nullable x) {
@strongify(self);
// 执行点击事件
[self.loginViewModel.LoginCommand execute:@"登录试试看"];
}];
}
#pragma mark - getter && setter
- (LoginViewModel *)loginViewModel{
if (!_loginViewModel) {
self.loginViewModel = [[LoginViewModel alloc]init];
}
return _loginViewModel;
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
VM中的代码:
// .h 中的代码
#import <Foundation/Foundation.h>
#import "Account.h"
#import <ReactiveObjC.h>
NS_ASSUME_NONNULL_BEGIN
@interface LoginViewModel : NSObject
@property (nonatomic, strong) Account *account;
/**
* 每次Account模型的值改变,就需要判断按钮能否点击,在VM模型中做处理,给外界提供一个能否点击按钮的信号.由于这个信号由其他两个信号决定,所以该信号此处应该聚合而成。
*/
@property (nonatomic, strong) RACSignal *enableSignal;
/**
* 处理点击事件
*/
@property (nonatomic, strong, readonly) RACCommand *LoginCommand;
@end
NS_ASSUME_NONNULL_END
// .m 中的代码
#import "LoginViewModel.h"
#import <SVProgressHUD.h>
@implementation LoginViewModel
- (instancetype)init{
if (self = [super init]) {
[self initialBind];
}
return self;
}
-(void)initialBind{
// 监听账号的属性值改变,把他们聚合成一个信号。
self.enableSignal = [RACSignal combineLatest:@[RACObserve(self.account, account), RACObserve(self.account, password)] reduce:^id _Nonnull{
return @(self.account.account.length && self.account.password.length);
}];
//
// 处理登录业务逻辑
_LoginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
NSLog(@"点击了登录");
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 模仿网络延迟
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:@"登录成功"];
// 数据传送完毕,必须调用完成,否则命令永远处于执行状态
[subscriber sendCompleted];
});
return nil;
}];
}];
// 监听登录产生的数据
[_LoginCommand.executionSignals.switchToLatest subscribeNext:^(id x) {
if ([x isEqualToString:@"登录成功"]) {
NSLog(@"登录成功");
}
}];
// 监听登录状态
[[_LoginCommand.executing skip:1] subscribeNext:^(id x) {
if ([x isEqualToNumber:@(YES)]) {
// 正在登录ing...
// 用蒙版提示
[SVProgressHUD showInfoWithStatus:@"正在登录..."];
}else
{
// 登录成功
// 隐藏蒙版
[SVProgressHUD showSuccessWithStatus:@"登录成功"];
}
}];
}
- (Account *)account{
if (!_account) {
self.account = [[Account alloc]init];
}
return _account;
}
@end
Account中的属性声明
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Account : NSObject
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *account;
@end
NS_ASSUME_NONNULL_END
ReactiveCocoa + MVVM 实战二:网络请求数据
2.1 接口:这里先给朋友介绍一个免费的网络数据接口,豆瓣。可以经常用来练习一些网络请求的小Demo.
2.2 需求+分析+步骤
/*
需求:请求豆瓣图书信息,url:https://api.douban.com/v2/book/search?q=基础
分析:请求一样,交给VM模型管理
步骤:
1.控制器提供一个视图模型(requesViewModel),处理界面的业务逻辑
2.VM提供一个命令,处理请求业务逻辑
3.在创建命令的block中,会把请求包装成一个信号,等请求成功的时候,就会把数据传递出去。
4.请求数据成功,应该把字典转换成模型,保存到视图模型中,控制器想用就直接从视图模型中获取。
5.假设控制器想展示内容到tableView,直接让视图模型成为tableView的数据源,把所有的业务逻辑交给视图模型去做,这样控制器的代码就非常少了。
*/
2.3控制器代码
@interface ViewController ()
@property (nonatomic, weak) UITableView *tableView;
@property (nonatomic, strong) RequestViewModel *requesViewModel;
@end
@implementation ViewController
- (RequestViewModel *)requesViewModel
{
if (_requesViewModel == nil) {
_requesViewModel = [[RequestViewModel alloc] init];
}
return _requesViewModel;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 创建tableView
UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
tableView.dataSource = self.requesViewModel;
[self.view addSubview:tableView];
// 执行请求
RACSignal *requesSiganl = [self.requesViewModel.reuqesCommand execute:nil];
// 获取请求的数据
[requesSiganl subscribeNext:^(NSArray *x) {
self.requesViewModel.models = x;
[self.tableView reloadData];
}];
}
@end
2.4视图模型(VM)代码
@interface RequestViewModel : NSObject<UITableViewDataSource>
// 请求命令
@property (nonatomic, strong, readonly) RACCommand *reuqesCommand;
//模型数组
@property (nonatomic, strong, readonly) NSArray *models;
@end
@implementation RequestViewModel
- (instancetype)init
{
if (self = [super init]) {
[self initialBind];
}
return self;
}
- (void)initialBind
{
_reuqesCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[@"q"] = @"基础";
// 发送请求
[[AFHTTPRequestOperationManager manager] GET:@"https://api.douban.com/v2/book/search" parameters:parameters success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) {
NSLog(@"%@",responseObject);
// 请求成功调用
// 把数据用信号传递出去
[subscriber sendNext:responseObject];
[subscriber sendCompleted];
} failure:^(AFHTTPRequestOperation * _Nonnull operation, NSError * _Nonnull error) {
// 请求失败调用
}];
return nil;
}];
// 在返回数据信号时,把数据中的字典映射成模型信号,传递出去
return [requestSignal map:^id(NSDictionary *value) {
NSMutableArray *dictArr = value[@"books"];
// 字典转模型,遍历字典中的所有元素,全部映射成模型,并且生成数组
NSArray *modelArr = [[dictArr.rac_sequence map:^id(id value) {
return [Book bookWithDict:value];
}] array];
return modelArr;
}];
}];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.models.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *ID = @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
}
Book *book = self.models[indexPath.row];
cell.detailTextLabel.text = book.subtitle;
cell.textLabel.text = book.title;
return cell;
}
@end