Nelson 寫些 iOS 開發的東東

ReactiveCocoa 範例 - 處理網路請求

| Comments

前言

最近這半年來我開始使用 ReactiveCocoa 開發 APP,覺得它真的是很適合一般的 APP 使用情境,在我看來,它可以很漂亮的解決 當 XX 發生的時候,就執行 YY 的需求。今天這篇文章來分享一下,我是如何使用 ReactiveCocoa 強化舊有的網路請求功能。

我是用 AFNetworking 來實現網路請求功能,然後用 Mantle 來建立我的 model。現在我有個 APIManager 繼承自 AFHTTPSessionManager,它專門負責跟 server 之間的 API call,然後有個 User model,它繼承自 MTLModel <MTLJSONSerializing>。我打算完成的功能是「根據 email 取得使用者的資料」。

第一版:單純使用 AFNetworking

最一開始的版本,我在 APIManager 建立一個 getUserByEmail:success:failure: method,然後在 UserViewController 呼叫它並處理成功與失敗的後續動作。

APIManager

- (NSURLSessionDataTask *)getUserByEmail:(NSString *)email success:(void (^)(User *user))success failure:(void (^)(NSError *error))failure {
  NSString *path = @"https://your.server.address/api/user";
  NSDictionary *params = @{ @"email":email };
  return [self GET:path parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
    if (success) {
      NSError *error = nil;
      User *user = [MTLJSONAdapter modelOfClass:[User class] fromJSONDictionary:responseObject error:&error];
      if (error) {
        if (failure) {
          failure(error);
        }
      } else {
        success(user);
      }
    }
  } failure:^(NSURLSessionDataTask *task, NSError *error) {
    if (failure) {
      failure(error);
    }
  }];
}

UserViewController

- (void)getUserInfo {
  [SVProgressHUD show];
  [[APIManager sharedInstance] getUserByEmail:@"test@gmail.com" success:^(User *user) {
    [SVProgressHUD dismiss];
    // Update data
    // Update UI
  } failure:^(NSError *error) {
    [SVProgressHUD dismiss];
    // Data error handling
    // UI error handling
  }];
}

第二版:用 RACSignal 包起來

原本是直接回傳 NSURLSessionDataTask,現在改成用 ReactiveCocoaRACSignal 包起來。當然 UserViewController 也要稍微修改一下來呼應這個改變。

APIManager

- (RACSignal *)getUserByEmail:(NSString *)email {
  return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    NSString *path = @"https://your.server.address/api/user";
    NSDictionary *params = @{ @"email":email };
    
    NSURLSessionDataTask *task = [self dataTaskWithHTTPMethod:@"GET" URLString:path parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
      NSError *error = nil;
      User *user = [MTLJSONAdapter modelOfClass:[User class] fromJSONDictionary:responseObject error:&error];
      if (error) {
        [subscriber sendError:error];
      } else {
        [subscriber sendNext:user];
        [subscriber sendCompleted];
      }
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
      [subscriber sendError:error];
    }];

    [task resume];
    
    return [RACDisposable disposableWithBlock:^{
      [task cancel];
    }];
  }];
}

UserViewController

- (void)getUserInfo {
  [SVProgressHUD show];
  [[[APIManager sharedInstance] getUserByEmail:@"test@gmail.com"]
   subscribeNext:^(User *user) {
    [SVProgressHUD dismiss];
    // Update data
    // Update UI
   } error:(NSError *error) {
    [SVProgressHUD dismiss];
    // Data error handling
    // UI error handling
   }];
}

第三版:加入 AFNetworking-RACExtensions

如果每支 API 都要改寫成這樣的話,程式碼將會變得非常冗長,還好網路上早就有人幫忙開發 AFNetworking-RACExtensions 來處理這件事。所以接下來我要改寫 APIManagerUserViewController 則不需要變動。改寫之後的程式碼,看起來是不是清爽多了呢!

APIManager

- (RACSignal *)getUserByEmail:(NSString *)email {
  NSString *path = @"https://your.server.address/api/user";
  NSDictionary *params = @{ @"email":email };
  return [[self rac_GET:path parameters:params] flattenMap:^RACStream *(RACTuple *tuple) {
    NSError *error = nil;
    User *user = [MTLJSONAdapter modelOfClass:[User class] fromJSONDictionary:tuple.first error:&error];
    return error ? [RACSignal error:error] : [RACSignal return:user];
  }];
}

第四版:改成 MVVM 模式

既然我們都用 ReactiveCocoa 了,那就順便把程式架構從 MVC(Model-View-Controller) 改成 MVVM(Model-View-ViewModel) 吧,更多有關 MVVM 的說明可以參考 objc.io 的這篇文章 以及 ReactiveViewModel 的說明文件.

在這個版本,APIManager 不用修改,然後我們多了一個 UserViewModel。藉由這樣的改動,我們讓 UserViewController 變得更簡潔,它專心處理跟 UI 有關的部分,跟資料邏輯相關的部分則是搬到 UserViewModel 去處理。

UserViewModel

- (RACSignal *)getUserByEmail:(NSString *)email {
  return [[[[APIManager sharedInstance] getUserByEmail:email]
  doNext:^(User *user) {
    // View model updates data
  }]
  doError:^(NSError *error) {
    // View model error handling
  }];
}

UserViewController

- (void)getUserInfo {
  [SVProgressHUD show];
  [[self.viewModel getUserByEmail:@"test@gmail.com"]
   subscribeNext:^(User *user) {
    [SVProgressHUD dismiss];
    // Update UI
   } error:(NSError *error) {
    [SVProgressHUD dismiss];
    // UI error handling
   }];
}

看起來似乎差不多?

或許你會覺得,單純使用 AFNetworking 跟使用 ReactiveCocoa 改寫的差異不大,對 UserViewController 來說只是從原本的 successfailure block 改成 subscribeNextsubscribeError block。

這是因為我們的例子很單純,如果後續還有許多動作要執行的話,使用 ReactiveCocoa 就顯得方便許多。以下的例子取自 ReactiveCocoa ReadMe 的 Chaining dependent operations,你可以自己比較看看兩者的差異。

Dependencies are most often found in network requests, where a previous request to the server needs to complete before the next one can be constructed, and so on:

[client logInWithSuccess:^{
    [client loadCachedMessagesWithSuccess:^(NSArray *messages) {
        [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
            NSLog(@"Fetched all messages.");
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
    } failure:^(NSError *error) {
        [self presentError:error];
    }];
} failure:^(NSError *error) {
    [self presentError:error];
}];

ReactiveCocoa makes this pattern particularly easy:

[[[[client logIn]
    then:^{
        return [client loadCachedMessages];
    }]
    flattenMap:^(NSArray *messages) {
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeError:^(NSError *error) {
        [self presentError:error];
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

以上就是幫單純的網路請求加上 ReactiveCocoa 超能力的過程,如果你有任何意見或建議的話,歡迎在底下留言。

Comments

comments powered by Disqus