美文网首页MVVM+RAC
iOS开发之RAC+MVVM实战

iOS开发之RAC+MVVM实战

作者: 代码歌 | 来源:发表于2019-07-12 11:10 被阅读0次

简介

  • MVVM:Model–View–Viewmode 是一种软件架构模式。其主作用就是解决Controller代码过于臃肿的问题。因为传统MVC中的Controller要负责View和Model之间调度:网络请求、字典转模型并赋值给view、偶尔还要写一写界面,业务逻辑处理等等,随着APP越来越复杂,导致Controller里的代码越来越臃肿不堪。一不小心Controller里的代码就上到几千行,想象下刚到一家公司就接手这样的项目。。。。。(╯‵□′)╯︵┻━┻
    为了解决这个问题,我们可以在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情。
  • 为什么要使用RAC:为了让View和ViewModel之间能够有比较松散的绑定关系,让代码更加优雅。但是RAC是一个超重量级的框架,学习成本很大,我在之前的文章结合代码示例介绍过一些RAC基本用法,传送🚪:iOS开发之ReactiveCocoa的基本用法干货分享

实战

本文介绍两个开发中常用的场景,第一个是UITableView列表界面通过网络请求数据展示数据,第二个是登录功能。功能比较基础,但都是精髓。分享一下笔者对MVVM的一些见解,在此抛砖引玉,希望能对广大开发者提供一点思路。

一、UITableView列表
订单列表.png

效果如上图,实现此功能用到的类:

  • Controller --- OrderController
  • ViewModel --- RequestViewModel
  • View --- OrderCell
  • Model --- OrderModel

1、OrderController

#import "OrderController.h"
#import "RequestViewModel.h"
#import "OrderCell.h"

@interface OrderController ()<UITableViewDataSource, UITableViewDelegate>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (strong, nonatomic) RequestViewModel *reqVM;
@end

@implementation OrderController

- (void)viewDidLoad {
   [super viewDidLoad];
   [self setUI];
   [self ViewModelEvent];
}
#pragma mark - 界面设置
- (void)setUI {
   self.tableView.dataSource = self;
   self.tableView.delegate = self;
   self.tableView.rowHeight = 100;
   [self.tableView registerNib:[UINib nibWithNibName:@"OrderCell" bundle:nil] forCellReuseIdentifier:@"OrderCell"];
}
#pragma mark - ViewModel事件
- (void)ViewModelEvent {
   [self.reqVM.reqCommand execute:nil];
   @weakify(self);
   [self.reqVM.refreshUISubject subscribeNext:^(id x) {
      @strongify(self);
      [self.tableView reloadData];
   }];
}
#pragma mark - UITableView配置
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
   return self.reqVM.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   OrderCell *cell = [tableView dequeueReusableCellWithIdentifier:@"OrderCell"];
   cell.model = self.reqVM.dataArray[indexPath.row];
   return cell;
}
#pragma mark - 懒加载
- (RequestViewModel *)reqVM {
   if (!_reqVM) {
      _reqVM = [[RequestViewModel alloc] init];
   }
   return _reqVM;
}

@end

OrderController主要讲的是ViewModelEvent中的方法,其他也没什么可说的

  • [self.reqVM.reqCommand execute:nil]; 方法为执行reqCommand事件命令,reqCommand是RequestViewModel中网络请求事件。

  • [self.reqVM.refreshUISubject subscribeNext:^(id x) {
    @strongify(self);
    [self.tableView reloadData];
    }];
    此方法为订阅RequestViewModel中网络请求完成时发送的信号(refreshUISubject),也就是说当网络请求完成之后会执行block中的刷新tableView方法。

2、RequestViewModel:主要向控制器提供数据,通知tableView刷新界面

RequestViewModel.h

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

@interface RequestViewModel : NSObject

@property (nonatomic, strong) RACSubject *refreshUISubject;
@property (strong, nonatomic) RACCommand *reqCommand;
@property (nonatomic, strong) NSArray *dataArray;

@end

RequestViewModel.m

#import "RequestViewModel.h"
#import "OrderModel.h"
#import "AFNetworking.h"
#import "MBProgressHUD+Add.h"
#import "MJExtension.h"

@interface RequestViewModel ()

@end

@implementation RequestViewModel

- (instancetype)init {
    if (self = [super init]) {
        [self or_initialize];
    }
    return self;
}
- (void)or_initialize {
    [self.reqCommand.executionSignals.switchToLatest subscribeNext:^(NSDictionary *dic) {
        NSArray *items = dic[@"items"];
        self.dataArray = [OrderModel mj_objectArrayWithKeyValuesArray:items];
        [self.refreshUISubject sendNext:nil];
    }];
    [[self.reqCommand.executing skip:1] subscribeNext:^(id x) {
        if ([x isEqualToNumber:@(YES)]) {
            [MBProgressHUD showCircleHud:nil];
        }else {
            [MBProgressHUD closeHud:nil];
        }
    }];
}
#pragma mark - 懒加载
- (RACCommand *)reqCommand {
    if (!_reqCommand) {
        _reqCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
            //因为要把请求的数据传出去,所以要把网络请求包装在信号里
            RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
                NSDictionary *dic = @{@"action":@"getProduct",@"page":@"0"};
                NSString *url = @"http://10.49.3.125:8080/";
                
                AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
                manager.responseSerializer = [AFHTTPResponseSerializer serializer];
                manager.requestSerializer = [AFHTTPRequestSerializer serializer];
                [manager GET:url parameters:dic progress:^(NSProgress * _Nonnull downloadProgress) {
                    
                } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                    NSError * error;
                    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:&error];
                    [subscriber sendNext:dic];
                    [subscriber sendCompleted];
                } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                    [MBProgressHUD showMessage:@"网络连接失败" toView:nil];
                    [subscriber sendCompleted];
                }];
                return nil;
            }];
            //返回网络请求信号
            return signal;
        }];
    }
    return _reqCommand;
}
- (RACSubject *)refreshUISubject {
    if (!_refreshUISubject) {
        _refreshUISubject = [RACSubject subject];
    }
    return _refreshUISubject;
}
- (NSArray *)dataArray {
    if (!_dataArray) {
        _dataArray = [[NSArray alloc] init];
    }
    return _dataArray;
}

@end
  • RequestViewModel.h
    refreshUISubject属性是通知控制器刷新UI的信号,其功能类似于代理。reqCommand属性是网络请求事件,暴露在 .h 文件的原因是让控制器来决定什么时候发起事件,也就是说什么时候发起网络请求。
  • RequestViewModel.m
    or_initialize 中第一个方法是订阅reqCommand(网络请求)事件中的信号发出的值,也就是网络请求成功后发送的数据。第二个方法的功能是监听reqCommand事件过程,其block中的值返回YES是,代表事件正在执行,所以在这里面可以加一个正在加载的菊花,当返回值为NO时,代表事件执行完成,把正在加载菊花去掉。
  • 懒加载 - (RACCommand *)reqCommand 方法中就是网络请求事件,block里面的signal信号作用是把网络请求的数据发送给 or_initialize 中第一个方法的订阅者。订阅者拿到数据后执行字典转模型操作,然后发送暴露在.h文件中的 refreshUISubject 信号给订阅此信号的控制器,通知他刷新tableView。

3、OrderCell和OrderModel

跟之前MVC做法完全一致,其实没什么好说的

OrderCell.h

#import <UIKit/UIKit.h>
#import "OrderModel.h"

@interface OrderCell : UITableViewCell

@property (nonatomic, strong) OrderModel *model;

@end

OrderCell.m

#import "OrderCell.h"
#import "SDWebImage.h"

@interface OrderCell ()
@property (weak, nonatomic) IBOutlet UIImageView *imgV;
@property (weak, nonatomic) IBOutlet UILabel *nameLab;
@property (weak, nonatomic) IBOutlet UILabel *typeLab;
@property (weak, nonatomic) IBOutlet UILabel *descLab;

@end

@implementation OrderCell

- (void)awakeFromNib {
    [super awakeFromNib];
    // Initialization code
}
- (void)setModel:(OrderModel *)model {
    [_imgV sd_setImageWithURL:[NSURL URLWithString:model.imageUrl]];
    _nameLab.text = model.name;
    _typeLab.text = model.type;
    _descLab.text = model.desc;
}

@end

OrderModel.h

#import <Foundation/Foundation.h>

@interface OrderModel : NSObject

@property (nonatomic, copy) NSString *desc;
@property (nonatomic, copy) NSString *imageUrl;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *type;

@end
二、登录功能
登录.png

效果如上图,实现此功能用到的类:

  • Controller --- LoginController
  • ViewModel --- LoginViewModel

1、LoginController

#import "LoginController.h"
#import "LoginViewModel.h"

@interface LoginController ()
@property (weak, nonatomic) IBOutlet UITextField *numTextField;
@property (weak, nonatomic) IBOutlet UITextField *pwdTextField;
@property (weak, nonatomic) IBOutlet UIButton *loginBtn;
@property (strong, nonatomic) LoginViewModel *loginVM;
@end

@implementation LoginController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self bindViewModel];
    [self loginEvent];
}
#pragma mark - ViewModel处理
- (void)bindViewModel {
    //给ViewModel账号密码绑定信号
    RAC(self.loginVM,num) = _numTextField.rac_textSignal;
    RAC(self.loginVM,pwd) = _pwdTextField.rac_textSignal;
}
- (void)loginEvent {
    //把_loginBtn的enabled属性与信号绑定
    RAC(_loginBtn,enabled) = self.loginVM.loginEnabledSignal;
    //登录按钮点击事件
    [[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
        [self.loginVM.loginCommand execute:nil ];
    }];
}
#pragma mark - 懒加载
- (LoginViewModel *)loginVM {
    if (!_loginVM) {
        _loginVM = [[LoginViewModel alloc] init];
    }
    return _loginVM;
}

@end
  • bindViewModel方法
    用的RAC()宏,将LoginViewModel对象的num(账号)和pwd(密码)属性分别和_numTextField、_pwdTextField输入的文字绑定。简单的说,就是将控制器界面中的账号和密码输入框的内容传给LoginViewModel,并且输入框里的内容每次改变都要重新传过去。因为我们要在ViewModel中处理业务逻辑,所以要把值传给它。

  • loginEvent方法
    第一个方法同样是用RAC()宏,将登录按钮是否可点击属性绑定LoginViewModel的loginEnabledSignal信号,以达到在 LoginViewModel 中写控制按钮是否能点击的逻辑。

    第二个方法是监听按钮点击事件, 当按钮点击时,执行loginCommand(登录事件)命令。

2、LoginViewModel

  • LoginViewModel.h
#import <Foundation/Foundation.h>
#import <ReactiveObjC/ReactiveObjC.h>

@interface LoginViewModel : NSObject

@property (copy, nonatomic) NSString *num;
@property (copy, nonatomic) NSString *pwd;

//按钮是否被允许点击
@property (strong, nonatomic, readonly) RACSignal *loginEnabledSignal;
//登录按钮命令
@property (strong, nonatomic, readonly) RACCommand *loginCommand;

@end
  • LoginViewModel.m
#import "LoginViewModel.h"
#import "MBProgressHUD+Add.h"

@implementation LoginViewModel

- (instancetype)init {
    if (self = [super init]) {
        [self setRACSignal];
    }
    return self;
}

- (void)setRACSignal {
    _loginEnabledSignal = [RACSignal combineLatest: @[RACObserve(self, num),RACObserve(self, pwd)] reduce:^id (NSString *num, NSString *pwd){
        //账号输入位数大于0,密码大于等于6时登录按钮可点击
        BOOL isEnabled = (num.length > 0 && pwd.length >= 6) ? YES : NO;
        return @(isEnabled);
    }];
    //处理登录点击:创建登录命令。(只要处理事件,就要用到RACCommand)
    _loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
        
        return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
            //模拟请求登录数据
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [subscriber sendNext:@"模拟登录请求"];
                [subscriber sendCompleted]; //一定要写
            });
            return nil;
        }];
    }];
    //订阅命令中的信号
    [_loginCommand.executionSignals.switchToLatest subscribeNext:^(id  _Nullable x) {
        //这里写保存服务器返回的信息
    }];
    //监听命令执行过程
    //skip:1跳过第一次信号,因为刚开始没有执行的时候x也为NO
    [[_loginCommand.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) {
        if ([x boolValue] == YES) {
            [MBProgressHUD showCircleHud:nil];
        }else {
            //执行完成
            [MBProgressHUD closeHud:nil];
            [MBProgressHUD showMessage:@"登陆成功" toView:nil];
        }
    }];
}

@end
  • setRACSignal方法
    1.第一个方法创建按钮是否点击的信号赋值给控制器,其中 combineLatest 方法是将数组中的信合组成为一个新的信号。其中任何一个信号发送数据,组成的信号都能执行订阅后的代码块。RACObserve()用法类似于KVO,只要监听的属性改变就会发送信号。
    2.第二个方法为登录处理事件逻辑,block里的RACSignal信号中可以写登录的网络请求,
    3.第三个方法为订阅登录网络请求产生的数据,在其block中可以写一些处理网络返回数据的逻辑,例如:保存用户信息
    4.第四个方法为监听执行登录命令的过程与例1UITableView网络请求中的用法一致。

相关文章

网友评论

    本文标题:iOS开发之RAC+MVVM实战

    本文链接:https://www.haomeiwen.com/subject/pbiehctx.html