ReactiveCocoa代碼實踐之-RAC網路請求重構

来源:http://www.cnblogs.com/dsxniubility/archive/2016/03/14/5265132.html
-Advertisement-
Play Games

本節先說網路請求的重構 舊代碼結構圖: 之前的代碼控制器中都是一個個需要連接網路的方法中直接調用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代碼實踐,第三節會整理一些更多的思考,再後面還沒想好。 本文是系列文並且也會吸取建議進行修改和更新,所以禁止轉載。本文歡迎提建議和吐槽。(董鉑然博客園)

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • There are only two hard things in Computer Science: cache invalidation and naming things.在電腦科學中只有兩件難事:緩存失效和命名。 – Phil Karlton 電腦語言是人和電腦之間通訊的媒介。好的代碼
  • Android Material Design
  • 游戲項目尾聲,做下總結: 1.sharesdk微信微博分享(1) 如果接入眾多渠道,選用服務端獲取代碼配置參數的方式(微信:app_id 微博: app_key, app_secret)代碼配置2.x版本需註意setPlatformConfig設置參數時Android和ios設置key不同(Andr
  • 今天發現之前自己一直有個誤區,new Runnable(run()方法){}原來它不是一定創建一個線程 如果用主線程的handler去post(Runnable),他就不會創建子線程,而是在主線程上執行的Runnable方法 如果用new Thread(Runnable).start();那他就是在
  • 在網上找到了一篇總結的非常好的文章,我這裡就貼出他的博文地址。自己就不再寫這個方面的總結了。 "Activity與Fragment通信(99%)完美解決方案"
  • Android Material Design DrawerLayout和NavigationView及Palette
  • 本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的動態特性,使這門古老的語言煥發生機。主要內容如下: 曾經覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文檔和調用。還記得初學 Objective-C 時把[
  • iOS9系統下 為了我司APP的相容性問題 特意把手上的iOS Mac XCode都升級到了最新的beta版 然後發現iOS9的多任務管理器風格大變 變成了下麵這種樣子 我忽然想起來之前的文章提到我最愛的UI控制項iCarousel要實現類似這種效果其實是很簡單的 一時興起就花時間試驗了一下 效果還不
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...