本節先說網路請求的重構 舊代碼結構圖: 之前的代碼控制器中都是一個個需要連接網路的方法中直接調用service的請求方法並獲取回調,屬於常規做法。 重構後結構圖: 使用RAC改寫後,controller不會直接調用service,controller通過控制一個個command的執行與否來達到發請
前言
- RAC相比以往的開發模式主要有以下優點:提供了統一的消息傳遞機制;提供了多種奇妙且高效的信號操作方法;配合MVVM設計模式和RAC巨集綁定減少多端依賴。
- RAC的理論知識非常深厚,包含有FRP,高階函數,冷信號與熱信號,RAC Operation,信號的生命周期等,這些文檔里都有介紹。 但是由於RAC本身的特性,可能會聽上去容易上手難。
- 本文還是從一個比較接地氣的角度開始的。因為現在要做一個完美100%的全項目ReactiveCocoa架構基本不太現實,大多數項目都會有很多歷史包袱,我們只能漸漸的向RAC靠攏,將一段段噁心的代碼重構,使邏輯功能更加清晰。
本節主要我之前對網路請求的重構的一個簡單記錄。
一.普通請求重構
舊代碼結構圖:
之前的代碼控制器中都是一個個需要連接網路的方法中直接調用service的請求方法並獲取回調,屬於常規做法。
// controller.m ************************************ // 控制器中的某一處方法 - (void)requestForTop{ [MDSBezelActivityView activityViewForView:self.view withLabel:@"載入中..."]; // 直接調用service里的請求方法 [SXFeedbackService requestForFeedbackSummarySuccess:^(NSDictionary *result) { [MDSBezelActivityView removeView]; // 成功後相關處理 } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [MDSBezelActivityView removeView]; // 失敗後相關處理 }]; }
重構後結構圖:
使用RAC改寫後,controller不會直接調用service,controller通過控制一個個command的執行與否來達到發請求的目的。得到數據後綁定的值一旦發生改變,會來到RACObserve的回調方法。並且如果請求失敗,也會以錯誤信號的方式傳遞到execute的subscribeError回調方法里。 executing可以用來監聽命令是否執行完。
// controller.m ************************************ @property(nonatomic,strong)SXFeedbackMainViewModel *viewModel; - (void)viewDidLoad{ [self addRACObserve]; } // 在頁面初次載入時設置綁定 - (void)addRACObserve{ @weakify(self); [[RACObserve(self.viewModel, topNumEntity) skip:1] subscribeNext:^(id x) { @strongify(self); // 綁定viewModel的值一旦改變來到這裡。 }]; } // 原本用來發請求的地方 - (void)requestForTop { [[self.viewModel.fetchFeedbackSummaryCommand execute:nil] subscribeError:^(NSError *error) { // 對錯誤的處理 }]; [[self.viewModel.fetchFeedbackSummaryCommand.executing skip:1] subscribeNext:^(NSNumber *executing) { if ([executing boolValue]) { [MDSBezelActivityView activityViewForView:self.view withLabel:@"載入中..."]; }else{ [MDSBezelActivityView removeView]; } }]; } // viewModel.m ************************************ - (instancetype)init { self = [super init]; [self setupRACCommand]; return self; } // 初始化設定一個指令用來打開某個請求 - (void) setupRACCommand { @weakify(self); _fetchFeedbackSummaryCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { // 這裡面更徹底的方法是直接將請求寫成一個operation,但是大多數項目的網路層應該都有manager或是簽名等原因想直接改成那種結構可能比較複雜 ,所以這裡面的代碼像是RAC和直接請求的結合。 [SXMerchantAutorityService requestForFeedbackSummarySuccess:^(NSDictionary *result) { @strongify(self); // 成功回調後做的相關操作 [subscriber sendCompleted]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [subscriber sendError:error]; }]; return nil; }]; }]; }
二.需要傳參數的請求
上面是普通的請求,就是請求地址是寫死或者是從全局變數中拼接參數的。 如果需要傳入若幹參數的話controller無法直接接觸到service,所以需要以viewModel作為媒介傳值,有兩種傳值方法。
1.通過viewModel的屬性
這種方法可用於參數少,一個或兩個的。直接在viewModel裡加上一些屬性,然後controller在適當的時候給這個屬性賦值。 在viewModel中的RACCommand中調用service方法需要參數時直接從自己的屬性取。
// controller.m ************************************ self.viewModel.isAccess = self.isAccess; [self requestForTop]; // viewModel.h ************************************ // input參數 /** * 是美團還是點評 */ @property(nonatomic, assign) BOOL isAccess; // viewModel.m ************************************ _fetchFeedbackSummaryCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { [SXMerchantAutorityService requestForFeedbackSummaryWithType:self.isAccess success:^(NSDictionary *result) { // 成功 } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // 失敗 }]; return nil; }]; }];
如果是用RAC巨集設置viewModel和controller的某些屬性綁定,那也可以省去手動給viewModel的set方法賦值這一步。(董鉑然博客園)
2.通過execute方法參數傳值
這種方法適用於參數較多的情況無法一一列為viewModel的屬性。 這時候建議設置一個對象模型,然後在execute方法前將這個模型建立好並賦值,然後作為參數傳入。
比如這種常見的列表類的具有多個參數的請求方法:
// service.h ************************************ /** * 獲取評價列表 */ + (void)requestForFeedbacklistWithSource:(BOOL)isFromWeb dealid:(NSInteger)dealid poiid:(NSInteger)poiid labelName:(NSString *)labelName type:(NSString *)type readStatus:(NSString *)readStatus replyStatus:(NSString *)replyStatus limit:(NSNumber *)limit offset:(NSNumber *)offset success:(void(^)(NSDictionary *result))success failure:(void(^)(AFHTTPRequestOperation *operation, NSError *error))failure;
在controller的發請求方法中舊方法就是直接調用service的請求介面,這裡不再列出,下麵列出RAC的寫法。
// controller.m ************************************ - (void)requestForDataWithType:(int)type { // ------給RACComand傳入一個input模型。 SXFeedbackListRequestModel *input = [SXFeedbackListRequestModel new]; input.replyStatus = self.replyStatus; // 這裡也可以寫成一個工廠方法 input.readStatus = self.readStatus; input.isMeituan = self.isMeituan; input.dealid = self.dealid; input.poiid = self.poiid; input.type = self.type; input.labelName = labelName; input.offset = @(self.offset); input.limit = @(10); // 上面的input在這裡作為參數傳入 [[self.viewModel.fetchFeedbackListCommand execute:input] subscribeNext:^(id x) { // ------這裡處理正確的操作。 } error:^(NSError *error) { // ------這裡處理失敗的操作。 }]; } // viewModel.m ************************************ - (void) setupRACCommand { _fetchFeedbackListCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(SXFeedbackListRequestModel *input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { // 用前面execute傳入的參數會傳到這個地方 [SXMerchantAutorityService requestForFeedbacklistWithSource:input.isFormWeb dealid:input.dealid poiid:input.poiid labelName:input.labelName type:input.type readStatus:input.readStatus replyStatus:input.replyStatus limit:input.limit offset:input.offset success:^(NSDictionary *result) { @strongify(self); // 一些操作 [subscriber sendCompleted]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [subscriber sendError:error]; }]; return nil; }]; }]; }
可能會覺得在這個command中要把之前的模型的每一個屬性都扒出來傳到參數里行為有點冗餘。 可以將之前service里的那個參數很多的方法改寫成只需要傳入一個模型。然後command這裡就可以直接傳入模型了,反正在方法內部再取出來也不麻煩。我這邊考慮到了其他非RAC地方的相容性就沒有改了。
三.所有請求完成才消除toast
這裡是一個類似於請求combo的概念。所有的請求全部結束後才消除載入中的progressHUD ,如果在普通的架構下可用dispatch調度組來解決,但是RAC實現這個功能非常簡單,主要方法是通過executing信號來判斷一個命令的的狀態,然後使用combineLatest操作來監聽多個command的狀態,combineLatest操作的特征是監聽的多個信號只要有一個改變了就把所有信號組成一個tuple返回。
// 監聽executing RACSignal *hud = [RACSignal combineLatest:@[self.viewModel.fetchFeedbackListCommand.executing,self.viewModel.fetchFeedbackSummaryCommand.executing]]; [hud subscribeNext:^(RACTuple *x) { if (![x.first boolValue]&&![x.second boolValue]) { [MDSBezelActivityView removeView]; }else{ [MDSBezelActivityView activityViewForView:self.view withLabel:@"載入中..."]; } }];
這個建議和之前RACObserve寫在一起。 也可以改成filter的寫法。
// 可以把載入HUD的代碼寫在最前面,然後後面直接控制消除HUD [[hud filter:^BOOL(RACTuple *x) { return ![x.first boolValue]&&![x.second boolValue]; }] subscribeNext:^(id x) { [MDSBezelActivityView removeView]; }];
還有另一種方法也可以實現這種需求,rac_liftSelector這個方法是只有所有數組中的信號都發出sendNext信號時才會調用那個@selector的方法,並且這個方法的三個參數分別就是那三個sendNext發的。 所有的都回來了再統一打包,這主要適用於三個請求都是非同步沒有依賴關係。
@weakify(self); [[self rac_liftSelector:@selector(doWithA:withB:withC) withSignalsFromArray:@[signalA,signalB,signalC]] subscribeError:^(NSError *error) { @strongify(self); [MDSBezelActivityView removeView]; } completed:^{ [MDSBezelActivityView removeView]; }];
combineLatest和liftselector兩種combo的方法有一定的區別,具體的使用可以結合需求。前者是每一個請求回來了都會回調一下,後者是全部回來了再調用方法。(董鉑然博客園)
四.結果數據的傳遞
如果是希望所有的請求都完成了所有數據都獲得了,後再刷新界面,使用上面統一消除toast的方法時同樣適合的。 把消除toast那行代碼改成[self.tableVIew reloadData]或其他代碼即可。
因為現在的主流是希望能夠瘦身Controller, 所以一般也建議將業務邏輯、判斷、計算、拼接字元串放在viewModel里,最後直接把需要的數據返回,控制器只負責得到乾脆的數據後直接展示界面。 下麵的例子是一個文本標簽上文字的獲得方法
// Controller.m ************************************ // ViewDidLoad RAC(self.replyCountLabel,text) = RACObserve(self.viewModel, replyCountLabelTitle); // ViewModel.m ************************************ _fetchNewsDetailCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { @strongify(self); [self requestForNewsDetailSuccess:^(NSDictionary *result) { // 這邊省去一些判空代碼 self.detailModel = [SXNewsDetailEntity detailWithDict:result[self.newsModel.docid]]; // 中間還有一些其他的操作省略 NSInteger count = [self.newsModel.replyCount intValue]; // 這裡是直接把拼接好的標題返回,現實中還會遇到更複雜的邏輯 if ([self.newsModel.replyCount intValue] > 10000) { self.replyCountBtnTitle = [NSString stringWithFormat:@"%.1f萬跟帖",count/10000.0]; }else{ self.replyCountBtnTitle = [NSString stringWithFormat:@"%ld跟帖",count]; } [subscriber sendCompleted]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [subscriber sendError:error]; }]; return nil; }]; }];
重構時可以將更多控制器的屬性比如模型,或數組,放到viewModel里。 以前控制器里的self.replyModels 改成self.ViewModel.replyModels。
// ViewModel.h ************************************ /** * 相似新聞 */ @property(nonatomic,strong)NSArray *similarNews; /** * 搜索關鍵字 */ @property(nonatomic,strong)NSArray *keywordSearch; /** * 獲取搜索結果數組命令 */ @property(nonatomic, strong) RACCommand *fetchNewsDetailCommand; // ViewModel.m ************************************ // 某個command里調用發請求方法成功的回調內 self.similarNews = [SXSimilarNewsEntity objectArrayWithKeyValuesArray:result[self.newsModel.docid][@"relative_sys"]]; self.keywordSearch = result[self.newsModel.docid][@"keyword_search"]; [subscriber sendCompleted]; // Controller.m ************************************ // 隨便拿了個方法舉例 - (CGFloat )tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { switch (section) { case 0: return self.webView.height; break; case 1: return self.viewModel.replyModels.count > 0 ? 40 : CGFLOAT_MIN; break; case 2: return self.viewModel.similarNews.count > 0 ? 40 : CGFLOAT_MIN; break; default: return CGFLOAT_MIN; break; } }
合理的分離之後應該是Controller只有一些UI控制項,ViewModel中存放模型屬性,命令,和一些業務邏輯操作或判斷的方法等。
對其中的一些demo代碼感興趣的可以fork下這裡的代碼 https://github.com/dsxNiubility/SXNews 。以前是用土方法寫了個小項目,現在舊代碼移到了old分支,master分支上持續在做一些RAC相關的改動。
參照如上所說的方法進行重構,controller的代碼將會大大的減少,業務邏輯也會更加明朗。後續的第二節會整理一些特殊UI組件的RAC代碼實踐,第三節會整理一些更多的思考,再後面還沒想好。 本文是系列文並且也會吸取建議進行修改和更新,所以禁止轉載。本文歡迎提建議和吐槽。(董鉑然博客園)