iOS開發(fā)之RAC+MVVM實戰(zhàn)

簡介

  • MVVM:Model–View–Viewmode 是一種軟件架構(gòu)模式。其主作用就是解決Controller代碼過于臃腫的問題。因為傳統(tǒng)MVC中的Controller要負責View和Model之間調(diào)度:網(wǎng)絡(luò)請求、字典轉(zhuǎn)模型并賦值給view、偶爾還要寫一寫界面,業(yè)務(wù)邏輯處理等等,隨著APP越來越復(fù)雜,導(dǎo)致Controller里的代碼越來越臃腫不堪。一不小心Controller里的代碼就上到幾千行,想象下剛到一家公司就接手這樣的項目。。。。。(╯‵□′)╯︵┻━┻
    為了解決這個問題,我們可以在MVC的基礎(chǔ)上,把C拆出一個ViewModel專門負責數(shù)據(jù)處理的事情。
  • 為什么要使用RAC:為了讓View和ViewModel之間能夠有比較松散的綁定關(guān)系,讓代碼更加優(yōu)雅。但是RAC是一個超重量級的框架,學(xué)習成本很大,我在之前的文章結(jié)合代碼示例介紹過一些RAC基本用法,傳送??:iOS開發(fā)之ReactiveCocoa的基本用法干貨分享

實戰(zhàn)

本文介紹兩個開發(fā)中常用的場景,第一個是UITableView列表界面通過網(wǎng)絡(luò)請求數(shù)據(jù)展示數(shù)據(jù),第二個是登錄功能。功能比較基礎(chǔ),但都是精髓。分享一下筆者對MVVM的一些見解,在此拋磚引玉,希望能對廣大開發(fā)者提供一點思路。

一、UITableView列表
訂單列表.png

效果如上圖,實現(xiàn)此功能用到的類:

  • 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 - 界面設(shè)置
- (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]; 方法為執(zhí)行reqCommand事件命令,reqCommand是RequestViewModel中網(wǎng)絡(luò)請求事件。

  • [self.reqVM.refreshUISubject subscribeNext:^(id x) {
    @strongify(self);
    [self.tableView reloadData];
    }];
    此方法為訂閱RequestViewModel中網(wǎng)絡(luò)請求完成時發(fā)送的信號(refreshUISubject),也就是說當網(wǎng)絡(luò)請求完成之后會執(zhí)行block中的刷新tableView方法。

2、RequestViewModel:主要向控制器提供數(shù)據(jù),通知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) {
            //因為要把請求的數(shù)據(jù)傳出去,所以要把網(wǎng)絡(luò)請求包裝在信號里
            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:@"網(wǎng)絡(luò)連接失敗" toView:nil];
                    [subscriber sendCompleted];
                }];
                return nil;
            }];
            //返回網(wǎng)絡(luò)請求信號
            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屬性是網(wǎng)絡(luò)請求事件,暴露在 .h 文件的原因是讓控制器來決定什么時候發(fā)起事件,也就是說什么時候發(fā)起網(wǎng)絡(luò)請求。
  • RequestViewModel.m
    or_initialize 中第一個方法是訂閱reqCommand(網(wǎng)絡(luò)請求)事件中的信號發(fā)出的值,也就是網(wǎng)絡(luò)請求成功后發(fā)送的數(shù)據(jù)。第二個方法的功能是監(jiān)聽reqCommand事件過程,其block中的值返回YES是,代表事件正在執(zhí)行,所以在這里面可以加一個正在加載的菊花,當返回值為NO時,代表事件執(zhí)行完成,把正在加載菊花去掉。
  • 懶加載 - (RACCommand *)reqCommand 方法中就是網(wǎng)絡(luò)請求事件,block里面的signal信號作用是把網(wǎng)絡(luò)請求的數(shù)據(jù)發(fā)送給 or_initialize 中第一個方法的訂閱者。訂閱者拿到數(shù)據(jù)后執(zhí)行字典轉(zhuǎn)模型操作,然后發(fā)送暴露在.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

效果如上圖,實現(xiàn)此功能用到的類:

  • 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輸入的文字綁定。簡單的說,就是將控制器界面中的賬號和密碼輸入框的內(nèi)容傳給LoginViewModel,并且輸入框里的內(nèi)容每次改變都要重新傳過去。因為我們要在ViewModel中處理業(yè)務(wù)邏輯,所以要把值傳給它。

  • loginEvent方法
    第一個方法同樣是用RAC()宏,將登錄按鈕是否可點擊屬性綁定LoginViewModel的loginEnabledSignal信號,以達到在 LoginViewModel 中寫控制按鈕是否能點擊的邏輯。

    第二個方法是監(jiān)聽按鈕點擊事件, 當按鈕點擊時,執(zhí)行l(wèi)oginCommand(登錄事件)命令。

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){
        //賬號輸入位數(shù)大于0,密碼大于等于6時登錄按鈕可點擊
        BOOL isEnabled = (num.length > 0 && pwd.length >= 6) ? YES : NO;
        return @(isEnabled);
    }];
    //處理登錄點擊:創(chuàng)建登錄命令。(只要處理事件,就要用到RACCommand)
    _loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
        
        return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
            //模擬請求登錄數(shù)據(jù)
            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) {
        //這里寫保存服務(wù)器返回的信息
    }];
    //監(jiān)聽命令執(zhí)行過程
    //skip:1跳過第一次信號,因為剛開始沒有執(zhí)行的時候x也為NO
    [[_loginCommand.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) {
        if ([x boolValue] == YES) {
            [MBProgressHUD showCircleHud:nil];
        }else {
            //執(zhí)行完成
            [MBProgressHUD closeHud:nil];
            [MBProgressHUD showMessage:@"登陸成功" toView:nil];
        }
    }];
}

@end
  • setRACSignal方法
    1.第一個方法創(chuàng)建按鈕是否點擊的信號賦值給控制器,其中 combineLatest 方法是將數(shù)組中的信合組成為一個新的信號。其中任何一個信號發(fā)送數(shù)據(jù),組成的信號都能執(zhí)行訂閱后的代碼塊。RACObserve()用法類似于KVO,只要監(jiān)聽的屬性改變就會發(fā)送信號。
    2.第二個方法為登錄處理事件邏輯,block里的RACSignal信號中可以寫登錄的網(wǎng)絡(luò)請求,
    3.第三個方法為訂閱登錄網(wǎng)絡(luò)請求產(chǎn)生的數(shù)據(jù),在其block中可以寫一些處理網(wǎng)絡(luò)返回數(shù)據(jù)的邏輯,例如:保存用戶信息
    4.第四個方法為監(jiān)聽執(zhí)行登錄命令的過程與例1UITableView網(wǎng)絡(luò)請求中的用法一致。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容