MVVM+Reactive Cocoa專案完整例項

SeaMoonTime發表於2018-05-15

前言

網上介紹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 原始碼解析之架構總覽

相關文章