前言
最近這半年來我開始使用 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
,現在改成用 ReactiveCocoa 的 RACSignal
包起來。當然 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 來處理這件事。所以接下來我要改寫 APIManager
,UserViewController
則不需要變動。改寫之後的程式碼,看起來是不是清爽多了呢!
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 來說只是從原本的 success
跟 failure
block 改成 subscribeNext
跟 subscribeError
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 超能力的過程,如果你有任何意見或建議的話,歡迎在底下留言。