陈江川

邮箱:jiangchuanc@gmail.com

ReactiveCocoa v2.5学习笔记

这篇文章主要就MVVMDemo进行讲解,在这过程中必定会牵扯到很多RAC的基础概念,我会以目前对RAC的理解去进行说明,如有不解之处可以留言。

在看该文章前,可以先阅读下面两篇文章:
ReactiveCocoa入门教程:第一部分
ReactiveCocoa入门教程:第二部分

项目结构

用红色框住的是重点要讲解的。首先讲解RACNetworkRequest,这个类是对AFNetworking框架的封装。

RACNetworkRequest.h

RACNetworkRequest.h
RACNetworkRequest提供了两个类方法:

+ (RACSignal *)postHeaderField:(NSDictionary *)headerField
                       withURL:(NSString *)urlString
                    parameters:(id)parameters
                     modeClass:(Class)modeClass;
                     
+ (RACSignal *)getHeaderField:(NSDictionary *)headerField
                      withURL:(NSString *)urlString
                   parameters:(id)parameters
                    modeClass:(Class)modeClass;

对比下我们普通的封装

+ (void)get:(NSString *)url
 parameters:(NSDictionary *)parameters
    success:(void(^)(id responseObj))success
    failure:(void(^)(NSError *error))failure;

利用RAC封装的工具类返回的是RACSignal对象,就类似我们普通封装的success参数。

RACNetworkRequest.m

postHeaderField和getHeaderField方法最终调用了下面的方法

+ (RACSignal *)privateGettHeaderField:(NSDictionary *)headerField
                              withURL:(NSString *)urlString
                           parameters:(id)parameters {
    
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        
        AFHTTPSessionManager *manager = [self sharedHTTPSessionManager];
        [self setupHttpHeaderField:headerField withSessionManager:manager];
        
        NSURLSessionDataTask *dataTask =
        [manager GET:urlString
          parameters:parameters
            progress:nil
             success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {  
            [self handleResponseObject:responseObject withSubscriber:subscriber];                 
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            [subscriber sendError:error];    
        }];
        
        // 结束后取消请求
        return [RACDisposable disposableWithBlock:^{
            [dataTask cancel];
        }];
    }];
}
  1. 首先利用createSignal:方法创建了一个RACSignle对象,该方法实现的原理,请参考RACSignal的Subscription深入分析;
  2. 创建一个AFHTTPSessionManager对象,使用它来请求网络服务;
  3. 调用handleResponseObject: withSubscriber:方法,对服务器返回的数据进行处理。
+ (void)handleResponseObject:(id  _Nullable)responseObject
              withSubscriber:(id<RACSubscriber>)subscriber {
    
    // 这个方法要根据服务器返回的实际数据进行处理
    /*
        假设现在服务器成功返回:
     "code": "0",
     "msg": "成功",
     
     失败返回:
     "code": "1",
     "msg": "失败",
     */
    NSString *msg = responseObject[@"msg"];
    if ([msg isEqualToString:@"成功"]) {
        [subscriber sendNext:responseObject];
        [subscriber sendCompleted];
    } else { // 失败
        NSError *error = [NSError errorWithDomain:@"requestDataError"
                                             code:-1
                                         userInfo:responseObject];
        [subscriber sendError:error];
    }
}
  • 这个方法处理服务器返回的数据,需要根据实际的情况进行解析处理!

RACJCViewModel.h

#import <Foundation/Foundation.h>

@interface RACJCViewModel : NSObject

/** username */
@property (nonatomic, copy) NSString *username;
/** password */
@property (nonatomic, copy) NSString *password;

/** 登录按钮是否有效 */
@property (nonatomic, strong, readonly) RACSignal *validLoginSignal;
/** 点击登录按钮后执行的RACCommand */
@property (nonatomic, strong, readonly) RACCommand *loginCommand;

@end
  1. username:保存用户输入的帐号;
  2. password:保存用户输入的密码;
  3. validLoginSignal:决定登录按钮的enable值;
  4. loginCommand:当用户点击了登录按钮后要执行的block块。

RACJCViewModel.m

- (instancetype)init {
    if (self = [super init]) {
        @weakify(self);
        // 登录按钮enable的值取决于username和password,username必须为手机号,password必须大于6
        _validLoginSignal = [[RACSignal
                              combineLatest:@[RACObserve(self, username), RACObserve(self, password)]
                              reduce:^(NSString *username, NSString *password) {
            @strongify(self);
            return @([self isMobileNumber:username] && password.length >= 6);
        }] distinctUntilChanged];
        
        _loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            @strongify(self);
            // 在这里进行网络请求
            NSString *url = [ServerURL stringByAppendingPathComponent:LoginURL];
            
            NSString *uuid = [SYUUID readUUIDFromKeyChain];
            NSDictionary *headerField = @{@"uuid" : uuid};
            
            NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
            parameters[@"tick"] = [self currentMilliSecond];
            parameters[@"username"] = self.username;
            
            return [[RACNetworkRequest postHeaderField:headerField
                                               withURL:url
                                            parameters:parameters
                                             modeClass:nil] doNext:^(id x) {
                NSLog(@"loginCommand\nx= %@", x);
                
                NSString *code = x[@"code"];
                if ([code isEqualToString:@"0"]) {
                    // 跳转到下一个界面
                    [[Routable sharedRouter] open:@"TableViewController"];
                }
            }];
        }];
    }
    return self;
}
  • 给_validLoginSignal信号赋值,其中出现了combineLatest: reduce:方法和RACObserve宏;

    • combineLatest: reduce:是将多个RACSignal合并成一个RACSignal;
    • RACObserve作用与KVO一样,监听某个key的value的变化;
    • distinctUntilChanged当RACObserve(self, username)或者RACObserve(self, password)改变时才做处理。

    这几行代码的意思是:当用户输入的username为有效手机号以及用户输入的password长度大于等于6,返回@(YES),否则返回@(NO);

  • 给_loginCommand赋值,内部就是调用RACNetworkRequest方法进行网络请求,然后跳转界面。

RACLoginViewController.m

@interface RACLoginViewController ()

/** ViewModel */
@property (nonatomic, strong) RACJCViewModel *viewModel;
@property (weak, nonatomic) IBOutlet UITextField *usernameTextField;
@property (weak, nonatomic) IBOutlet UITextField *passwordTextField;
@property (weak, nonatomic) IBOutlet UIButton *loginButton;

@end

- (void)setupViewModel {
    
    // 测试帐号:18170957509, 密码:123456
    RAC(self.viewModel, username) = self.usernameTextField.rac_textSignal;
    RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal;
    RAC(self.loginButton, enabled) = self.viewModel.validLoginSignal;
    
    @weakify(self);
    [[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        @strongify(self);
        [self.viewModel.loginCommand execute:nil];
    }];
}
  1. RAC(self.viewModel, username)意思给RACJCViewModel中的username属性进行赋值;
  2. self.usernameTextField.rac_textSignal把UITextField最新的内容传出来;
  3. rac_signalForControlEvents作用和Target-Action一样,监听登录按钮的点击事件,成功就执行loginCommand,上面有讲过loginCommand本质作用就是进行网络请求。

调用过程

« 使用Routable后的思考 iOS - VoIP推送<一> »