ReactiveCocoa代碼實(shí)踐之-RAC網(wǎng)絡(luò)請(qǐng)求重構(gòu)
相關(guān)閱讀:
ReactiveCocoa代碼實(shí)踐之-UI組件的RAC信號(hào)操作
前言
•RAC相比以往的開發(fā)模式主要有以下優(yōu)點(diǎn):提供了統(tǒng)一的消息傳遞機(jī)制;提供了多種奇妙且高效的信號(hào)操作方法;配合MVVM設(shè)計(jì)模式和RAC宏綁定減少多端依賴。
•RAC的理論知識(shí)非常深厚,包含有FRP,高階函數(shù),冷信號(hào)與熱信號(hào),RAC Operation,信號(hào)的生命周期等,這些文檔里都有介紹。 但是由于RAC本身的特性,可能會(huì)聽上去容易上手難。
•本文還是從一個(gè)比較接地氣的角度開始的。因?yàn)楝F(xiàn)在要做一個(gè)完美100%的全項(xiàng)目ReactiveCocoa架構(gòu)基本不太現(xiàn)實(shí),大多數(shù)項(xiàng)目都會(huì)有很多歷史包袱,我們只能漸漸的向RAC靠攏,將一段段惡心的代碼重構(gòu),使邏輯功能更加清晰。
本節(jié)主要我之前對(duì)網(wǎng)絡(luò)請(qǐng)求的重構(gòu)的一個(gè)簡(jiǎn)單記錄。
一.普通請(qǐng)求重構(gòu)
舊代碼結(jié)構(gòu)圖:
之前的代碼控制器中都是一個(gè)個(gè)需要連接網(wǎng)絡(luò)的方法中直接調(diào)用service的請(qǐng)求方法并獲取回調(diào),屬于常規(guī)做法。
// controller.m ************************************ // 控制器中的某一處方法 - (void)requestForTop{ [MDSBezelActivityView activityViewForView:self.view withLabel:@"加載中..."]; // 直接調(diào)用service里的請(qǐng)求方法 [SXFeedbackService requestForFeedbackSummarySuccess:^(NSDictionary *result) { [MDSBezelActivityView removeView]; // 成功后相關(guān)處理 } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [MDSBezelActivityView removeView]; // 失敗后相關(guān)處理 }]; }
重構(gòu)后結(jié)構(gòu)圖:
使用RAC改寫后,controller不會(huì)直接調(diào)用service,controller通過控制一個(gè)個(gè)command的執(zhí)行與否來達(dá)到發(fā)請(qǐng)求的目的。得到數(shù)據(jù)后綁定的值一旦發(fā)生改變,會(huì)來到RACObserve的回調(diào)方法。并且如果請(qǐng)求失敗,也會(huì)以錯(cuò)誤信號(hào)的方式傳遞到execute的subscribeError回調(diào)方法里。 executing可以用來監(jiān)聽命令是否執(zhí)行完。
// controller.m ************************************ @property(nonatomic,strong)SXFeedbackMainViewModel *viewModel; - (void)viewDidLoad{ [self addRACObserve]; } // 在頁(yè)面初次加載時(shí)設(shè)置綁定 - (void)addRACObserve{ @weakify(self); [[RACObserve(self.viewModel, topNumEntity) skip:] subscribeNext:^(id x) { @strongify(self); // 綁定viewModel的值一旦改變來到這里。 }]; } // 原本用來發(fā)請(qǐng)求的地方 - (void)requestForTop { [[self.viewModel.fetchFeedbackSummaryCommand execute:nil] subscribeError:^(NSError *error) { // 對(duì)錯(cuò)誤的處理 }]; [[self.viewModel.fetchFeedbackSummaryCommand.executing skip:] 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; } // 初始化設(shè)定一個(gè)指令用來打開某個(gè)請(qǐng)求 - (void) setupRACCommand { @weakify(self); _fetchFeedbackSummaryCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { // 這里面更徹底的方法是直接將請(qǐng)求寫成一個(gè)operation,但是大多數(shù)項(xiàng)目的網(wǎng)絡(luò)層應(yīng)該都有manager或是簽名等原因想直接改成那種結(jié)構(gòu)可能比較復(fù)雜 ,所以這里面的代碼像是RAC和直接請(qǐng)求的結(jié)合。 [SXMerchantAutorityService requestForFeedbackSummarySuccess:^(NSDictionary *result) { @strongify(self); // 成功回調(diào)后做的相關(guān)操作 [subscriber sendCompleted]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [subscriber sendError:error]; }]; return nil; }]; }]; }
二.需要傳參數(shù)的請(qǐng)求
上面是普通的請(qǐng)求,就是請(qǐng)求地址是寫死或者是從全局變量中拼接參數(shù)的。 如果需要傳入若干參數(shù)的話controller無法直接接觸到service,所以需要以viewModel作為媒介傳值,有兩種傳值方法。
1.通過viewModel的屬性
這種方法可用于參數(shù)少,一個(gè)或兩個(gè)的。直接在viewModel里加上一些屬性,然后controller在適當(dāng)?shù)臅r(shí)候給這個(gè)屬性賦值。 在viewModel中的RACCommand中調(diào)用service方法需要參數(shù)時(shí)直接從自己的屬性取。
// controller.m ************************************ self.viewModel.isAccess = self.isAccess; [self requestForTop]; // viewModel.h ************************************ // input參數(shù) /** * 是美團(tuán)還是點(diǎn)評(píng) */ @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宏設(shè)置viewModel和controller的某些屬性綁定,那也可以省去手動(dòng)給viewModel的set方法賦值這一步。(董鉑然博客園)
2.通過execute方法參數(shù)傳值
這種方法適用于參數(shù)較多的情況無法一一列為viewModel的屬性。 這時(shí)候建議設(shè)置一個(gè)對(duì)象模型,然后在execute方法前將這個(gè)模型建立好并賦值,然后作為參數(shù)傳入。
比如這種常見的列表類的具有多個(gè)參數(shù)的請(qǐng)求方法:
// service.h ************************************ /** * 獲取評(píng)價(jià)列表 */ + (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的發(fā)請(qǐng)求方法中舊方法就是直接調(diào)用service的請(qǐng)求接口,這里不再列出,下面列出RAC的寫法。
// controller.m ************************************ - (void)requestForDataWithType:(int)type { // ------給RACComand傳入一個(gè)input模型。 SXFeedbackListRequestModel *input = [SXFeedbackListRequestModel new]; input.replyStatus = self.replyStatus; // 這里也可以寫成一個(gè)工廠方法 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 = @(); // 上面的input在這里作為參數(shù)傳入 [[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傳入的參數(shù)會(huì)傳到這個(gè)地方 [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; }]; }]; }
可能會(huì)覺得在這個(gè)command中要把之前的模型的每一個(gè)屬性都扒出來傳到參數(shù)里行為有點(diǎn)冗余。 可以將之前service里的那個(gè)參數(shù)很多的方法改寫成只需要傳入一個(gè)模型。然后command這里就可以直接傳入模型了,反正在方法內(nèi)部再取出來也不麻煩。我這邊考慮到了其他非RAC地方的兼容性就沒有改了。
三.所有請(qǐng)求完成才消除toast
這里是一個(gè)類似于請(qǐng)求combo的概念。所有的請(qǐng)求全部結(jié)束后才消除加載中的progressHUD ,如果在普通的架構(gòu)下可用dispatch調(diào)度組來解決,但是RAC實(shí)現(xiàn)這個(gè)功能非常簡(jiǎn)單,主要方法是通過executing信號(hào)來判斷一個(gè)命令的的狀態(tài),然后使用combineLatest操作來監(jiān)聽多個(gè)command的狀態(tài),combineLatest操作的特征是監(jiān)聽的多個(gè)信號(hào)只要有一個(gè)改變了就把所有信號(hào)組成一個(gè)tuple返回。
// 監(jiān)聽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:@"加載中..."]; } }];
這個(gè)建議和之前RACObserve寫在一起。 也可以改成filter的寫法。
// 可以把加載HUD的代碼寫在最前面,然后后面直接控制消除HUD [[hud filter:^BOOL(RACTuple *x) { return ![x.first boolValue]&&![x.second boolValue]; }] subscribeNext:^(id x) { [MDSBezelActivityView removeView]; }];
還有另一種方法也可以實(shí)現(xiàn)這種需求,rac_liftSelector這個(gè)方法是只有所有數(shù)組中的信號(hào)都發(fā)出sendNext信號(hào)時(shí)才會(huì)調(diào)用那個(gè)@selector的方法,并且這個(gè)方法的三個(gè)參數(shù)分別就是那三個(gè)sendNext發(fā)的。 所有的都回來了再統(tǒng)一打包,這主要適用于三個(gè)請(qǐng)求都是異步?jīng)]有依賴關(guān)系。
@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的方法有一定的區(qū)別,具體的使用可以結(jié)合需求。前者是每一個(gè)請(qǐng)求回來了都會(huì)回調(diào)一下,后者是全部回來了再調(diào)用方法。(董鉑然博客園)
四.結(jié)果數(shù)據(jù)的傳遞
如果是希望所有的請(qǐng)求都完成了所有數(shù)據(jù)都獲得了,后再刷新界面,使用上面統(tǒng)一消除toast的方法時(shí)同樣適合的。 把消除toast那行代碼改成[self.tableVIew reloadData]或其他代碼即可。
因?yàn)楝F(xiàn)在的主流是希望能夠瘦身Controller, 所以一般也建議將業(yè)務(wù)邏輯、判斷、計(jì)算、拼接字符串放在viewModel里,最后直接把需要的數(shù)據(jù)返回,控制器只負(fù)責(zé)得到干脆的數(shù)據(jù)后直接展示界面。 下面的例子是一個(gè)文本標(biāo)簽上文字的獲得方法
// 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]; // 這里是直接把拼接好的標(biāo)題返回,現(xiàn)實(shí)中還會(huì)遇到更復(fù)雜的邏輯 if ([self.newsModel.replyCount intValue] > ) { self.replyCountBtnTitle = [NSString stringWithFormat:@"%.f萬跟帖",count/.]; }else{ self.replyCountBtnTitle = [NSString stringWithFormat:@"%ld跟帖",count]; } [subscriber sendCompleted]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [subscriber sendError:error]; }]; return nil; }]; }];
重構(gòu)時(shí)可以將更多控制器的屬性比如模型,或數(shù)組,放到viewModel里。 以前控制器里的self.replyModels 改成self.ViewModel.replyModels。
// ViewModel.h ************************************ /** * 相似新聞 */ @property(nonatomic,strong)NSArray *similarNews; /** * 搜索關(guān)鍵字 */ @property(nonatomic,strong)NSArray *keywordSearch; /** * 獲取搜索結(jié)果數(shù)組命令 */ @property(nonatomic, strong) RACCommand *fetchNewsDetailCommand; // ViewModel.m ************************************ // 某個(gè)command里調(diào)用發(fā)請(qǐng)求方法成功的回調(diào)內(nèi) self.similarNews = [SXSimilarNewsEntity objectArrayWithKeyValuesArray:result[self.newsModel.docid][@"relative_sys"]]; self.keywordSearch = result[self.newsModel.docid][@"keyword_search"]; [subscriber sendCompleted]; // Controller.m ************************************ // 隨便拿了個(gè)方法舉例 - (CGFloat )tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { switch (section) { case : return self.webView.height; break; case : return self.viewModel.replyModels.count > ? : CGFLOAT_MIN; break; case : return self.viewModel.similarNews.count > ? : CGFLOAT_MIN; break; default: return CGFLOAT_MIN; break; } }
合理的分離之后應(yīng)該是Controller只有一些UI控件,ViewModel中存放模型屬性,命令,和一些業(yè)務(wù)邏輯操作或判斷的方法等。
對(duì)其中的一些demo代碼感興趣的可以fork下這里的代碼 https://github.com/dsxNiubility/SXNews 。以前是用土方法寫了個(gè)小項(xiàng)目,現(xiàn)在舊代碼移到了old分支,master分支上持續(xù)在做一些RAC相關(guān)的改動(dòng)。
以上所述是小編給大家介紹的ReactiveCocoa代碼實(shí)踐之-RAC網(wǎng)絡(luò)請(qǐng)求重構(gòu) 的相關(guān)內(nèi)容,希望對(duì)大家有所幫助!
相關(guān)文章
Android AlertDialog對(duì)話框用法示例
這篇文章主要介紹了Android AlertDialog對(duì)話框用法,結(jié)合實(shí)例形式分析了AlertDialog對(duì)話框的功能及常見使用技巧,需要的朋友可以參考下2016-06-06Android自定義滑動(dòng)接聽電話控件組實(shí)例
這篇文章主要介紹了Android自定義滑動(dòng)接聽電話控件組,接聽電話可以左右滑動(dòng),感興趣的小伙伴們可以參考一下。2016-10-10Android ListView構(gòu)建支持單選和多選的投票項(xiàng)目
如何在Android的ListView中構(gòu)建CheckBox和RadioButton列表?這篇文章主要為大家詳細(xì)介紹了Android ListView實(shí)現(xiàn)支持單選和多選的投票項(xiàng)目,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01Android自定義View——扇形統(tǒng)計(jì)圖的實(shí)現(xiàn)代碼
本篇文章主要介紹了Android自定義View——扇形統(tǒng)計(jì)圖的實(shí)現(xiàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02Android 監(jiān)聽Notification 被清除實(shí)例代碼
本文主要介紹Android 監(jiān)聽Notification 事件,這里給大家提供實(shí)例代碼進(jìn)行參考,有需要的小伙伴可以參考下2016-07-07android實(shí)現(xiàn)可拖動(dòng)的浮動(dòng)view
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)可拖動(dòng)的浮動(dòng)view,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04Android?進(jìn)入Activity時(shí)如何禁止彈出軟鍵盤輸入法
這篇文章主要介紹了Android?進(jìn)入Activity時(shí)如何禁止彈出軟鍵盤輸入法,文章圍繞主題展開具體內(nèi)容,需要的小伙伴可以參考一下2022-05-05Android無需權(quán)限調(diào)起系統(tǒng)相機(jī)
在進(jìn)行一些小型APP的開發(fā),或者是對(duì)拍照界面沒有自定義要求時(shí),我們可以用調(diào)起系統(tǒng)相機(jī)的方式快速完成拍照需求2023-03-03