iOS开发之RAC+MVVM实战

简介

  • 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网络请求中的用法一致。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351