MVVM+Reactive Cocoa项目完整实例

前言

网上介绍Reactive Cocoa的使用文档很多,但是应用demo要么一带而过,要么过于庞大不适合初学者(大神写的),本人自学一段时间后,略有心得,特意编写了一个完整的demo,该demo完全遵循MVVM架构设计。Github传送门

本文并不适合无任何reactive cocoa基础的童鞋,如需学习reactive cocoa基础请参考。
最快让你上手ReactiveCocoa之基础篇
最快让你上手ReactiveCocoa之进阶篇
iOS ReactiveCocoa 最全常用API整理(可做为手册查询)
ReactiveCocoa 官方GitHub
ReactiveCocoa v2.5 源码解析之架构总览

demo运行

  • 运行前请先进行pod install
  • 运行后,搜索框未输入时,搜索按钮不可用,在搜索框输入电影名导演名等,如张艺谋,搜索按钮可用,点击按钮可得到相关的电影搜索结果;
  • demo网络数据采用的豆瓣Api V2中的电影搜索功能;
电影搜索2.gif

程序说明

主界面

  • 主界面包括两个类HomeViewControllerHomeViewModel,因为model过于简单就直接定义在ViewModel中了
  • HomeViewModel定义了搜索条件searchConditons字符串,并将字符串是否为空与按钮是否可用信号searchBtnEnableSignal绑定;
  • HomeViewController则首先将ViewModel中的searchConditons字符串与输入框内容绑定,再将搜索按钮的enable属性与ViewModel中的searchBtnEnableSignal绑定;
  • 以上两个步骤即可实现通过判断输入框内容是否为空从而确定搜索按钮的enable属性;
  • 点击按钮后页面跳转;

HomeViewModel定义如下:

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

@interface HomeViewModel : NSObject
@property (nonatomic, copy) NSString *searchConditons;

@property (nonatomic, strong, readonly) RACSignal  *searchBtnEnableSignal;
@end

#import "HomeViewModel.h"

@implementation HomeViewModel

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

- (void)setUp{
    [self setupSearchBtnEnableSignal];
}

- (void)setupSearchBtnEnableSignal {
    _searchBtnEnableSignal = [RACSignal combineLatest:@[RACObserve(self, searchConditons)] reduce:^id(NSString *searchConditions){
        return @(searchConditions.length);
    }];
}

@end

HomeViewController定义如下:

#import "HomeViewController.h"
#import "MovieViewController.h"
#import "UIButton+FillColor.h"
#import "HomeViewModel.h"
#import <UIButton+JKBackgroundColor.h>

@interface HomeViewController ()
@property (weak, nonatomic) IBOutlet UITextField *textContent;
@property (weak, nonatomic) IBOutlet UIButton *btnSearch;

@property(nonatomic, strong) HomeViewModel *homeVM;

@end

@implementation HomeViewController

-(HomeViewModel *)homeVM{
    if (!_homeVM) {
        _homeVM = [[HomeViewModel alloc]init];
    }
    
    return  _homeVM;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    _btnSearch.enabled = false;
    
    [_btnSearch setBackgroundColor:[UIColor lightGrayColor] forState:UIControlStateDisabled];
    [_btnSearch setBackgroundColor:[UIColor blueColor] forState:UIControlStateNormal];
    
    RAC(self.homeVM, searchConditons) = self.textContent.rac_textSignal;
    RAC(self.btnSearch, enabled) = self.homeVM.searchBtnEnableSignal;
    
    
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
- (IBAction)onClick:(UIButton *)sender {
    // 进入下一界面
    UIStoryboard * storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    MovieViewController * destViewController = [storyboard instantiateViewControllerWithIdentifier:@"MovieViewController"];
    destViewController.conditions = _textContent.text;
    [self.navigationController pushViewController:destViewController animated:YES];
    
}


@end

搜索结果界面

  • 搜索结果界面包括3个类MovieMovieViewModel以及MovieViewController
  • Movie包括电影名称、时间、导演、主演、图片;实际上,豆瓣Api V2返回的电影数据远不止这么多,这里只选择了一部分;
#import <Foundation/Foundation.h>

@interface Movie : NSObject

@property(nonatomic, strong) NSString * title;
@property(nonatomic, strong) NSString * year;
@property(nonatomic, strong) NSArray *casts;
@property(nonatomic, strong) NSArray *directors;
@property(nonatomic, strong) NSDictionary *images;

+ (instancetype)movieWithDict:(NSDictionary *)dict;

@end
#import "Movie.h"

@implementation Movie

+(instancetype)movieWithDict:(NSDictionary *)dict{
    Movie *movie = [[Movie alloc]init];
    movie.year = dict[@"year"];
    movie.title = dict[@"title"];
    movie.casts = dict[@"casts"];
    movie.directors = dict[@"directors"];
    movie.images = dict[@"images"];
    
    return movie;
}

@end
  • MovieViewModel则包括了业务逻辑代码:定义命令、网络请求、获取数据、发送数据,

注意: 这里使用的是RACCommand,而不是RACSignal,初学者可能很难理解两者之间的差别,个人是这样理解:RACSignal是单向的,就像1个人在做演讲,观众听到就结束了;而RACCommand是双向的,演讲者做演讲,下面的观众听到后还反馈了意见,而演讲者对反馈还做了回复。
该demo中,首先在MovieViewController中做出发出命令,MovieViewModel收到命令后进行网络请求,并将获取的网络数据包发送出去,MovieViewController对收到的数据进行解析和显示;

定义如下:

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

@interface MovieViewModel : NSObject

@property (nonatomic, strong, readonly) RACCommand *requestCommand;
@property (nonatomic, copy, readonly) NSArray *movies;

@end
#import "MovieViewModel.h"
#import "NetworkManager.h"
#import "Movie.h"


@implementation MovieViewModel

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

- (void)setup {
    
    _requestCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
        NSLog(@"%@", input);
        
        
        RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
            NetworkManager *manager = [NetworkManager manager];
            [manager getDataWithUrl:@"https://api.douban.com/v2/movie/search" parameters:input success:^(id json) {
                [subscriber sendNext:json];
                [subscriber sendCompleted];
            } failure:^(NSError *error) {
                
            }];
            
            return nil;
        }];
        return [requestSignal map:^id _Nullable(id  _Nullable value) {
            NSMutableArray *dictArray = value[@"subjects"];
            NSArray *modelArray = [dictArray.rac_sequence map:^id(id value) {
                return [Movie movieWithDict:value];
            }].array;
           NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"year" ascending:NO];
            _movies = [modelArray sortedArrayUsingDescriptors:@[sortDescriptor]];
            NSLog(@"%@",_movies.description);
            
            return nil;
        }];
    }];
    
}

@end
  • MovieViewController则包含:发送命令、数据解析、数据显示;
    定义如下:
#import <UIKit/UIKit.h>

@interface MovieViewController : UITableViewController

@property(nonatomic, copy)NSString *conditions;

@end
#import "MovieViewController.h"
#import "Movie.h"
#import "MovieViewModel.h"
#import "MovieCell.h"
#import <YYWebImage/YYWebImage.h>
#import <ProgressHUD.h>
#import <SVProgressHUD.h>
#import "UITableView+FDTemplateLayoutCell.h"

@interface MovieViewController ()
@property (nonatomic, strong)MovieViewModel *movieVM;
@end

@implementation MovieViewController

-(MovieViewModel *)movieVM{
    if (!_movieVM) {
        _movieVM = [[MovieViewModel alloc]init];
    }
    
    return _movieVM;

}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.movieVM.requestCommand.executionSignals.switchToLatest subscribeNext:^(id x) {
        [self.tableView reloadData];
        [SVProgressHUD dismiss];
    }];
    
    NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
    parameters[@"q"] = _conditions;
    [self.movieVM.requestCommand execute:parameters];
    [SVProgressHUD show];
    
    self.tableView.fd_debugLogEnabled = YES;  
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return self.movieVM.movies.count;
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return [tableView fd_heightForCellWithIdentifier:@"cellID" configuration:^(MovieCell* cell) {
        [self configureCell:cell atIndexPath:indexPath];
    }];
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MovieCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cellID" forIndexPath:indexPath];
    
    [self configureCell:cell atIndexPath:indexPath];
    
    
    return cell;
}

- (void)configureCell:(MovieCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    
    Movie *movie = self.movieVM.movies[indexPath.row];
    
    NSDictionary *dicImage = movie.images;
    NSString *imageStr = dicImage[@"large"];
    NSURL *imageUrl = [NSURL URLWithString:imageStr];
    
    // progressive
    [cell.movieImageView yy_setImageWithURL:imageUrl options:YYWebImageOptionProgressive];
    
    // progressive with blur and fade animation (see the demo at the top of this page)
    [cell.movieImageView yy_setImageWithURL:imageUrl options:YYWebImageOptionProgressiveBlur | YYWebImageOptionSetImageWithFadeAnimation];
    
    cell.title.text = movie.title;
    NSString *year = @"上映时间:";
    cell.year.text = [year stringByAppendingString:movie.year];
    NSString *directors = @"导演:";
    for (NSDictionary *dict in movie.directors) {
        NSString *directname = dict[@"name"];
        directors = [directors stringByAppendingFormat:@"%@,",directname];
    }
    cell.directors.text = directors;
    NSString *casts = @"主演:";
    for (NSDictionary *dict in movie.casts) {
        NSString *castname = dict[@"name"];
        casts = [casts stringByAppendingFormat:@"%@,",castname];
    }
    cell.casts.text = casts;
        
}

参考:
最快让你上手ReactiveCocoa之基础篇
最快让你上手ReactiveCocoa之进阶篇
iOS ReactiveCocoa 最全常用API整理(可做为手册查询)
ReactiveCocoa 官方GitHub
ReactiveCocoa v2.5 源码解析之架构总览

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

推荐阅读更多精彩内容