iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信開發(二)

前言

這篇文章是iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信開發(一)的續篇,主要目的就是繼續分析以下幾個核心問題,希望大家能知道其來龍去脈,并有所收獲,文章略長,先馬后看。

  • 項目中的網絡(Network)層解析。(√)
  • 搭建Debug調試工具。(待續...)
  • 項目中如何快速搭建類似發現我的設置、...等界面解析。
  • 如何利用該設計模式搭建游客模式(PS: 微信是登錄模式的架構)的架構(待續...)。
  • 項目中的整體服務(Service)層解析。(待續...)
網絡(Network)層

網絡層在項目中扮演的角色,想必大家是心知肚明的,網絡層通過請求服務器的數據,使得我們的應用變得動態性和有趣性。在微信(WeChat)Demo中,筆者主要賦予網絡層(MHHTTPService)的職責是:網絡數據(NetData)請求用戶數據(UserData)處理。當然這只是筆者的一廂情愿罷了,大家肯定會有更好的職責和使命賦予網絡層的。

網絡數據(NetData)請求:目前絕大多數應用都是使用AFNetworking來做網絡請求,當然常規套路都是為了避免第三方框架的侵略性和耦合性,都會基于AFNetworking封裝成一個網絡工具類,暴露請求數據/上傳數據的API,以便后期使用的做法。例如:(XXHttpTool , XXNetworkTool , XXHttpHelper...)。如果項目比較復雜龐大的,數據請求可以集成YTKNetwork來實現,其底層實現也是基于AFNetworking來封裝實現的,當然蘿卜白菜,各有所愛,筆者主要是為了突出的是:封裝

筆者在WeChat項目中采用的是筆者熟悉的套路,基于AFNetworking 3.1.0封裝的一個網絡請求工具單例(MHHTTPService)。但可能與以往常規的網絡工具類的API,稍有差異,不要走開,請聽筆者慢慢道來。當我們調用AFNetworkingGET/POST的方法請求網絡數據成功/失敗都是以block的形式傳遞出去的,所以平常網絡請求工具類封裝請求數據的API,也是通過block的形式傳遞數據的。類似于+ (void)get:(NSString *)url params:(NSDictionary *)params success:(void (^)(id responseObj))success failure:(void (^)(NSError *error))failure;這樣,但是將其使用在MVVM + RAC + ViewModel-Based Navigation里面,常規做法都是在MHTableViewModel的子類中重寫- (RACSignal *)requestRemoteDataSignalWithPage:(NSUInteger)page的方法來做數據請求,但是如果在該方法里面使用這種block的形式獲取數據,就是有點顯得格格不入,讓人看著覺得別扭。

所以,最后筆者的做法是通過利用AFNetworking來做數據請求,而數據回調則使用ReactiveCocoa來傳遞數據信號(Signal),即:返回的數據是一個信號好RACSignal,這樣就優雅的解決了上述Block返回數據的尷尬。在設計微信(WeChat)網路工具類的API以及內部實現時,筆者主要參照OctoKitAPI來開發設計的,以及數據請求和數據回調信號(RACSignal)的內部實現筆者主要參考的是AFNetworking-RACExtensionsOctoKit的實現方法,可謂是站在巨人的肩膀上開發的。大家有興趣可以看看其源碼,具體的細節還需自行體會。總之,最終的請求數據的方式筆者這里引用OctoKit的一段代碼如下:

// Prepares a request that will load all of the user's repositories, represented
// by `OCTRepository` objects.
//
// Note that the request is not actually _sent_ until you use one of the
// -subscribe… methods below.
RACSignal *request = [client fetchUserRepositories];
// This method actually kicks off the request, handling any results using the
// blocks below.
[request subscribeNext:^(OCTRepository *repository) {
    // This block is invoked for _each_ result received, so you can deal with
    // them one-by-one as they arrive.
} error:^(NSError *error) {
    // Invoked when an error occurs.
    //
    // Your `next` and `completed` blocks won't be invoked after this point.
} completed:^{
    // Invoked when the request completes and we've received/processed all the
    // results.
    //
    // Your `next` and `error` blocks won't be invoked after this point.
}];

當然筆者的請求方式與OctoKit的又略有差異,這里筆者著重講述一下,筆者在設計這套網絡工具的思路和細節處理,當然這肯定不是唯一的封裝方式,畢竟一千個人眼中有一千個潘金蓮(哈姆雷特)嘛,只是為大家提供一個參考而已。Let‘s Do It...

  • 代碼結構
HTTPCodeStructure.png
  • 文件說明
    MHHTTPServiceConstant:常量定義。
    MHKeyedSubscript主要用來配置網絡請求參數字典,大家完全可以將其當做字典(NSDictionary)來看待,當然其本質就是字典。這里只是筆者極不喜歡面向字典開發,所以就將字典封裝在MHKeyedSubscript的內部,提升了一丟丟的逼格罷了。具體應用類似字典,這里不在贅述,代碼如下:

    MHKeyedSubscript *subscript = [MHKeyedSubscript subscript];
    subscript[@"useridx"] = useridx;
    subscript[@"type"] = @(type);
    subscript[@"page"] = @(page);
    

    MHURLParameters主要用來配置請求的基本參數、參數字典、請求路徑、請求方式等。具體內容如下內容如下:

    @interface MHURLParameters : NSObject
    /// 路徑 (v14/order)
    @property (nonatomic, readwrite, strong) NSString *path;
    /// 參數列表
    @property (nonatomic, readwrite, strong) NSDictionary *parameters;
    /// 方法 (POST/GET)
    @property (nonatomic, readwrite, strong) NSString *method;
    /// 拓展的參數屬性 (開發人員不必關心)
    @property (nonatomic, readwrite, strong) SBURLExtendsParameters *extendsParameters;
    
    /**
     參數配置(統一用這個方法配置參數) (SBBaseUrl : https://api.cleancool.tenqing.com/)
     https://api.cleancool.tenqing.com/user/info?user_id=100013
     @param method 方法名 (GET/POST/...)
     @param path 文件路徑 (user/info)
     @param parameters 具體參數 @{user_id:10013}
     @return 返回一個參數實例
     */
    +(instancetype)urlParametersWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters;
    @end
    

    當然筆者著重講一講基本參數(SBURLExtendsParameters)的配置,首先這個基本參數并不是每個服務器都要求配置的,完全根據你們后臺服務器來配置的。給大家看看我們公司的API文檔,截圖如下:

    BaseParameter.png

    所以,基本參數(SBURLExtendsParameters)的屬性字段設計成跟筆者公司的服務器字段一致即可,具體每個參數的作用和如何傳值,跟你們后臺人員協商即可。
    當然項目中對于處理基本參數協議參數的做法無非就是:首先將基本參數協議參數通過拼接(addEntriesFromDictionary)成一個大字典(parameters),然后把parameters按照平常請求參數的拼接樣式key1=value1&key2=value2&key3=value3...拼接成一個參數字符串paramString,接著最重要的是將paramString拼接服務器的privatekeyprivateValue(PS:具體的私鑰)成帶私鑰的字符串(signedString),例如NSString *signedString = [NSString stringWithFormat:@"%@&privateKey=%@",paramString,MHHTTPServiceKeyValue];。其次通過對signedString進行MD5加密得到簽名(sign)的值。最后將簽名(sign)的值添加到大字典(parameters)中parameters[@"sign"] = [sign length]?sign:@"";。最后得到的參數字典(parameters)里面包括基本參數協議參數的鍵值,以及最后增加的signsignValue。參考代碼如下:

    #pragma mark - Parameter 簽名 MD5 生成一個 sign ,這里請根據實際項目來定
    /// 基礎的請求參數
    -(NSMutableDictionary *)_parametersWithRequest:(MHHTTPRequest *)request{
        NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
        /// 模型轉字典
        NSDictionary *extendsUrlParams = [request.urlParameters.extendsParameters mj_keyValues].copy;
        if ([extendsUrlParams count]) {
            [parameters addEntriesFromDictionary:extendsUrlParams];
        }
        if ([request.urlParameters.parameters count]) {
            [parameters addEntriesFromDictionary:request.urlParameters.parameters];
        }
        return parameters;
    }
    
    /// 帶簽名的請求參數
    -(NSString *)_signWithParameters:(NSDictionary *) parameters {
      /// 按照ASCII碼排序
        NSArray *sortedKeys = [[parameters allKeys] sortedArrayUsingSelector:@selector(compare:)];
      
        NSMutableArray *kvs = [NSMutableArray array];
        for (id key in sortedKeys) {
            /// value 為 empty 跳過
            if(MHObjectIsNil(parameters[key])) continue;
            NSString * value = [parameters[key] sb_stringValueExtension];
            if (MHObjectIsNil(value)||!MHStringIsNotEmpty(value)) continue;
            value = [value sb_removeBothEndsWhitespaceAndNewline];
            value = [value sb_URLEncoding];
            [kvs addObject:[NSString stringWithFormat:@"%@=%@",key,value]];
        }
        /// 拼接私鑰
        NSString *paramString = [kvs componentsJoinedByString:@"&"];
        NSString *keyValue = MHHTTPServiceKeyValue;
        NSString *signedString = [NSString stringWithFormat:@"%@&%@=%@",paramString,MHHTTPServiceKey,keyValue];
      
        /// md5
        return [CocoaSecurity md5:signedString].hexLower;
    }
    
    /// 序列化
    - (AFHTTPRequestSerializer *)_requestSerializerWithRequest:(MHHTTPRequest *) request{
        /// 獲取基礎參數(參數+拓展參數)
        NSMutableDictionary *parameters = [self _parametersWithRequest:request];
        /// 獲取帶簽名的參數
        NSString *sign = [self _signWithParameters:parameters];
        /// 賦值
        parameters[MHHTTPServiceSignKey] = [sign length]?sign:@"";
        /// 請求序列化
        AFHTTPRequestSerializer *requestSerializer = [AFHTTPRequestSerializer serializer];
        /// 配置請求頭
        for (NSString *key in parameters) {
            NSString *value = [[parameters[key] sb_stringValueExtension] copy];
            if (value.length==0) continue;
            /// value只能是字符串,否則崩潰
            [requestSerializer setValue:value forHTTPHeaderField:key];
        }
        return requestSerializer;
    }
    

    當然,最終的參數字典(parameters)使用一般有兩種做法(PS:請根據實際項目中服務端的要求來選用方法):
    方式一:將其添加到AFNetworking中的AFHTTPRequestSerializer的請求頭HTTPRequestHeaders中,通過- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(NSString *)field的方法實現對應參數字典(parameters)的keyvalue的添加。具體代碼如下:

      /// 請求序列化
      AFHTTPRequestSerializer *requestSerializer = [AFHTTPRequestSerializer serializer];
      /// 配置請求頭
      for (NSString *key in parameters) {
          NSString *value = [[parameters[key] sb_stringValueExtension] copy];
          if (value.length==0) continue;
          /// value只能是字符串,否則崩潰
          [requestSerializer setValue:value forHTTPHeaderField:key];
      }
    

    最后,注意當我們在使用AFNetworkingGET等方法(API)時,需要將GET方法(API)的參數parameters,則傳遞的是協議參數,而不是我們最終得到的參數字典(parameters)。
    方式二:直接將我們最終得到的參數字典(parameters)傳遞給AFNetworkingGET等方法(API)的參數parameters即可。

    MHHTTPRequest主要是通過MHURLParameters模型來配置請求模型。以及通過為MHHTTPRequest創建了分類,能夠在配置完請求模型完成,就可以直接發起MHHTTPService中的請求。起內容如下:

    @interface MHHTTPRequest : NSObject
    /// 請求參數
    @property (nonatomic, readonly, strong) MHURLParameters *urlParameters;
    /**
     獲取請求類
     @param params  參數模型
     @return 請求類
     */
    +(instancetype)requestWithParameters:(MHURLParameters *)parameters;
    
    @end
    /// MHHTTPService的分類
    @interface MHHTTPRequest (MHHTTPService)
    /// 入隊
    - (RACSignal *) enqueueResultClass:(Class /*subclass of MHObject*/) resultClass;
    @end
    

    MHHTTPService整個網絡服務層(單例),繼承于AFHTTPSessionManager,主要用來做網絡數據請求用戶數據處理,這里筆者主要側重將的是其網絡數據請求。最關鍵的API如下:

    /**
     Enqueues a request to be sent to the server.
     This will automatically fetch a of the given endpoint. Each object
     from each page will be sent independently on the returned signal, so
     subscribers don't have to know or care about this pagination behavior.
    
     @param request config the request
     @param resultClass A subclass of `MHObject` that the response data should be returned as,
     and will be accessible from the `parsedResult`
     @return Returns a signal which will send an instance of `MHHTTPResponse` for each parsed
     JSON object, then complete. If an error occurs at any point,
     the returned signal will send it immediately, then terminate.
     */
    -(RACSignal *)enqueueRequest:(MHHTTPRequest *) request
                   resultClass:(Class /*subclass of MHObject*/) resultClass;
    

    通過執行該方法,我們底層通過AFNetworking請求到JSON數據,并通過YYModelJSON數據的data字段對應的數據轉化為相應的resultClass,并最終包裹成MHHTTPResponse數據,然后通過ReactiveCocoa轉化成數據信號并返回的過程。當然平常大家在通過網絡請求工具做數據請求時,回調的數據絕大多數都是JSON數據(id responseObject),然后在對應的控制器里面做字典轉模型操作。當然筆者這種做法很依賴服務器返回的數據格式,顯然前提是你需要和你的服務端人員共同協商好一份合適數據返回格式,然后再來設計這套網絡工具。首先常用JSON數據最外層是一個字典,且字段主要是:codemsgdata

    code: 請求狀態碼。比如100:請求成功101:對應參數有誤...
    msg: 請求狀態說明,主要是對code對應的值的解釋。比如請求成功點贊成功...
    data請求的數據,且其對應的數據也是一個字典({})。YYModel主要對該字段對應的數據做字典轉模型處理,比如用戶數據,商品列表...

    這里筆者用偽代碼的形式詳述筆者與后臺協商的三種JSON數據格式(PS:主要是data對應的數據變化,以及我們著重需要其內部那些重要數據)。

    格式一: data對應的只是單個字典數據,比如用戶模型...

    {
      "code" : "100",
      "msg": "請求成功",
      "data":{
          "user_id" : "100013",
          "avatar" : "https://...",
          "nickname": "CodeMikeHe",
          ...
      }
    }
    

    類似這種情況請求數據時,則resultClass,傳[MHUser class]即可。則底層就會通過YYModeldata對應的字典轉化成用戶模型(MHUser`)。

    格式二: data對應的是字典且我們只需要該字典的list對應的數組([])列表,比如直播間列表...

    {
      "code" : "100",
      "msg": "請求成功",
      "data":{
          "list" : [
              {/** 直播間數據 */},
              {/** 直播間數據*/},
              {/** 直播間數據 */},
              {/** 直播間數據 */},
              ...
          ],
          "totalPage":4,
         "samecity":0,
         "hotswitch":null,
         "hotswitch2":Array[0],
         "hotConfig":0
          ...
      }
    }
    

    如果我們只想要獲取data[@"list"]對應的數據列表,比如筆者Demo中的首頁數據展示。那么首先我們必須要和后臺人員協商好,后期遇到這種列表(數組)的情況,必須是list這個字段對應數組列表即可。所以類似這種情況,則resultClass,傳[MHLive class]即可。則底層就會通過YYModeldata對應list的列表轉化成直播間模型數組的。當然如果你還想要data字典中的其他值,那么你就把這個當做 格式一 的方式去處理即可。靈活使用,才是關鍵。

    格式三: data對應的是空值,即<nil>,比如用戶點贊,一般不需要返回數據,因為我們通過code = 100就可以判斷是否點贊成功,一般這種data是個空值。...

    {
      "code" : "100",
      "msg": "點贊成功",
      "data": <nil>,
      
    }
    

    類似這種情況,則resultClassnil即可,這樣筆者會原封不動的把后臺的數據返回出去。而你只需要根據code的值來做相應的提示即可。
    需要注意的是: resultClass必須是MHObject的子類,或者為nil。否則會Crash掉。

    MHHTTPResponse主要是請求成功后返回的服務器數據模型,主要是將服務器最外層的數據(字典),剝離出來而已。其頭文件內容如下:

    @interface MHHTTPResponse : MHObject
    /// The parsed MHObject object corresponding to the API response.
    /// The developer need care this data 切記:若沒有數據是NSNull 而不是nil .對應于服務器json數據的 data
    @property (nonatomic, readonly, strong) id parsedResult;
    /// 自己服務器返回的狀態碼 對應于服務器json數據的 code
    @property (nonatomic, readonly, assign) MHHTTPResponseCode code;
    /// 自己服務器返回的信息 對應于服務器json數據的 code
    @property (nonatomic, readonly, copy) NSString *msg;
    
    // Initializes the receiver with the headers from the given response, and given the origin data and the
    // given parsed model object(s).
    - (instancetype)initWithResponseObject:(id)responseObject parsedResult:(id)parsedResult;
    @end
    

    這里的屬性與服務器返回的字段保持一致,只不過用parsedResult代替data罷了。這里需要強調的是,當我們在調用-(RACSignal *)enqueueRequest:(MHHTTPRequest *) request resultClass:(Class /*subclass of MHObject*/) resultClass;時,resultClass參數如果我們傳nil,那么筆者底層將不會利用YYModel去把data數據轉化成模型,而是原封不動的服務器的data數據賦值到parsedResult。當然,格式一對應的parsedResultMHUser模型;格式二對應的parsedResultNSArray <MHLiveRoom *> * parsedResult模型數組;特別強調的是格式三那種情況,則parsedResultNSNull對象,而不是nil。這里需要注意的!!!。

    RACSignal+MHHTTPServiceAdditions主要作用是解析MHHTTPResponse數據,通過ReactiveCocoamap方法,將MHHTTPResponseparsedResult映射出來。關鍵代碼如下:

    - (RACSignal *)mh_parsedResults {
       return [self map:^(MHHTTPResponse *response) {
         NSAssert([response isKindOfClass:MHHTTPResponse.class], @"Expected %@ to be an MHHTTPResponse.", response);
           return response.parsedResult;
       }];
    }
    

    首先開發中我們通過-(RACSignal *)enqueueRequest:(MHHTTPRequest *) request resultClass:(Class /*subclass of MHObject*/) resultClass;這個方法返回的一個數據信號resultSignal,如果訂閱(subscribeNext)該數據信號resultSignal其值是MHHTTPResponse。即偽代碼如下:

    /// 請求的數據信號 (PS:當然可以一句代碼搞定,這里只做演示)
    RACSignal *resultSignal = [[MHHTTPService sharedInstance] enqueueRequest:request resultClass:[MHUser class]];
    /// 訂閱數據信號
    [resultSignal subscribeNext:^(MHHTTPResponse * response) {
     /// 成功回調 response.parsedResult 為MHUser模型。
    
     } error:^(NSError *error) {
     /// 失敗回調
    
     } completed:^{
     /// 完成
    
     }];
    

    在開發中,我們主要是想要獲取的是data對應的數據(PS:即response.parsedResult的值)。而很少去關注最外層的codemsg對應的值。所以,就出現了mh_parsedResults來直接獲取response.parsedResult的值。所以上面的偽代碼可以改成以下:

    [[[[MHHTTPService sharedInstance] enqueueRequest:request resultClass:[MHUser class]] 
    mh_parsedResults] 
    subscribeNext:^(MHUser * user) {
     /// 成功回調 MHUser模型。
    
     } error:^(NSError *error) {
     /// 失敗回調
    
     } completed:^{
     /// 完成
    
     }];
    

    這樣是不是覺得高端大氣上檔次,低調奢華有內涵。當然特別需要注意的是,不是每一個信號(Signal),都可以調用mh_parsedResults,必須是訂閱(subscribeNext)該數據信號resultSignal其值是MHHTTPResponse才行,否則程序Crash

  • 關于使用
    這里筆者將喵播的熱門數據的API https://live.9158.com/Room/GetHotLive_v2?cache=3&lat=22.54192103514200&lon=113.96939828211362&page=1&province=%E5%B9%BF%E4%B8%9C%E7%9C%81&type=0&useridx=61856069為例,講講開發中如何具體使用一下MHHTTPService。代碼如下:

    /// 獲取直播間列表
    - (RACSignal *)fetchLivesWithUseridx:(NSString *)useridx type:(NSInteger)type page:(NSInteger)page lat:(NSNumber *)lat lon:(NSNumber *)lon province:(NSString *)province{
        /// 1. 配置參數
        MHKeyedSubscript *subscript = [MHKeyedSubscript subscript];
        subscript[@"useridx"] = useridx;
        subscript[@"type"] = @(type);
        subscript[@"page"] = @(page);
        if (lat == nil) subscript[@"lat"] = @(22.54192103514200);
        if (lon == nil) subscript[@"lon"] = @(113.96939828211362);
        if (province == nil) subscript[@"province"] = @"廣東省";
      
        /// 2. 配置參數模型 #define MH_GET_LIVE_ROOM_LIST  @"Room/GetHotLive_v2"
        MHURLParameters *paramters = [MHURLParameters urlParametersWithMethod:MH_HTTTP_METHOD_GET path:MH_GET_LIVE_ROOM_LIST parameters:subscript.dictionary];
      
        /// 3.發起請求 如果你想獲取data的數據而不是data[@"list"]的數據,則resultClass為`[MHLiveInfo class]`即可。
        return [[[MHHTTPRequest requestWithParameters:paramters]
               enqueueResultClass:[MHLiveRoom class]]
              mh_parsedResults];
    }
    

    當然,上面的步驟三(發起請求),其實正常情況下應該為下面的兩步:

      /// 配置請求模型
      MHHTTPRequest *request = [MHHTTPRequest requestWithParameters:paramters];
      /// 發起請求
      return [[MHHTTPService sharedInstance] enqueueRequest:request resultClass:[MHLiveRoom class]];
    

    但是由于其過于繁瑣,筆者通過為MHHTTPRequest創建了分類,能夠在配置完請求模型完成,就可以直接發起MHHTTPService中的請求,這樣就優雅的實現了化二為一的效果。關鍵代碼如下:

    /// 網絡服務層分類 方便MHHTTPRequest 主動發起請求
    @implementation MHHTTPRequest (MHHTTPService)
    /// 請求數據
    -(RACSignal *) enqueueResultClass:(Class /*subclass of MHObject*/) resultClass {
        return [[MHHTTPService sharedInstance] enqueueRequest:self resultClass:resultClass];
    }
    @end
    

    當然,細節注意的地方就是網路請求工具盡量將其作為MHHTTPService的分類來設計,且命名要規范,并與請求成功后的模型放在同一個文件夾,這樣更好的提現單一職責化。比如:請求的用戶數據,分類名稱為:MHHTTPService+User,主要負責的是: 請求用戶數據, 修改用戶信息 , .... 等API。

    調試細節注意:在開發過程中,我們可能事先對服務器的返回的數據還一無所知,這樣就無法新建模型,這時候建議先將resultClassnil,然后打印數據即可。更多細節還請查看筆者提供的Demo。

  • 錯誤處理
    請求服務器數據出錯在開發中可謂是家常便飯了,為了提高用戶體驗,我們前端必須處理和解析錯誤信息(NSError),以便我們更好的根據錯誤信息展示不同的UI以及顯示錯誤提示。當然,相信大部分開發者,都沒怎么好好處理AFNetworking請求的錯誤信息,都是在AFNetworking的請求錯誤的block里面,提示一個服務器不給力,請稍后重試網絡有問題,請稍后重試...等等。當然有提示總比沒提示強,有些開發者根本不處理錯誤信息,頂多是在AFNetworking的請求錯誤的block里面NSLog一下錯誤,這樣用戶體驗極其不佳。當然,前面的提示也是不準確的,或者說不友好。比如有時候是服務器有問題,你卻提示網絡有問題,請稍后重試;又或者是用戶網絡斷開連接,而你卻提示服務器不給力,請稍后重試等。當然,我們也沒必要把錯誤信息(NSError)整個提示出來,這樣也會導致你提示出來的信息可能是一大堆亂七八糟的英文信息,比如錯誤信息如下:

    {fails Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set.
    " UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.}
    

    可能有些錯誤,我們開發者自己都不清楚其原因,你覺得用戶會知道錯誤原因嗎?所以,最主要的是提示準確且簡單的錯誤信息,筆者的做法如下對于AFNetworking的錯誤(NSError),筆者這里只分為三種情況:①服務器請求出錯②請求超時③網路斷開連接。當然在開發人員調試(DEBUG)狀態下,筆者是會將錯誤碼一同提示出來,方便開發人員準確定位錯誤信息。當然在發布(Release)狀態下,是不會提示錯誤碼的。關鍵代碼如下所述:

    #pragma mark - Error Handling
    /// 請求錯誤解析
    - (NSError *)_errorFromRequestWithTask:(NSURLSessionTask *)task httpResponse:(NSHTTPURLResponse *)httpResponse responseObject:(NSDictionary *)responseObject error:(NSError *)error {
        /// 不一定有值,則HttpCode = 0;
        NSInteger HTTPCode = httpResponse.statusCode;
        NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
        /// default errorCode is MHHTTPServiceErrorConnectionFailed,意味著連接不上服務器
      NSInteger errorCode = MHHTTPServiceErrorConnectionFailed;
      NSString *errorDesc = @"服務器出錯了,請稍后重試~";
      /// 其實這里需要處理后臺數據錯誤,一般包在 responseObject
      /// HttpCode錯誤碼解析 https://www.guhei.net/post/jb1153
      /// 1xx : 請求消息 [100  102]
      /// 2xx : 請求成功 [200  206]
      /// 3xx : 請求重定向[300  307]
      /// 4xx : 請求錯誤  [400  417] 、[422 426] 、449、451
      /// 5xx 、600: 服務器錯誤 [500 510] 、600
      NSInteger httpFirstCode = HTTPCode/100;
      if (httpFirstCode>0) {
          if (httpFirstCode==4) {
              /// 請求出錯了,請稍后重試
              if (HTTPCode == 408) {
    #if defined(DEBUG)||defined(_DEBUG)
                  errorDesc = @"請求超時,請稍后再試(408)~"; /// 調試模式
    #else
                  errorDesc = @"請求超時,請稍后再試~";      /// 發布模式
    #endif
              }else{
    #if defined(DEBUG)||defined(_DEBUG)
                  errorDesc = [NSString stringWithFormat:@"請求出錯了,請稍后重試(%zd)~",HTTPCode];                   /// 調試模式
    #else
                  errorDesc = @"請求出錯了,請稍后重試~";      /// 發布模式
    #endif
                }
            }else if (httpFirstCode == 5 || httpFirstCode == 6){
                /// 服務器出錯了,請稍后重試
    #if defined(DEBUG)||defined(_DEBUG)
              errorDesc = [NSString stringWithFormat:@"服務器出錯了,請稍后重試(%zd)~",HTTPCode];                      /// 調試模式
    #else
              errorDesc = @"服務器出錯了,請稍后重試~";       /// 發布模式
    #endif
              
          }else if (!self.reachabilityManager.isReachable){
              /// 網絡不給力,請檢查網絡
              errorDesc = @"網絡開小差了,請稍后重試~";
          }
      }else{
          if (!self.reachabilityManager.isReachable){
              /// 網絡不給力,請檢查網絡
              errorDesc = @"網絡開小差了,請稍后重試~";
          }
      }
      switch (HTTPCode) {
          case 400:{
              errorCode = MHHTTPServiceErrorBadRequest;           /// 請求失敗
              break;
          }
          case 403:{
              errorCode = MHHTTPServiceErrorRequestForbidden;     /// 服務器拒絕請求
              break;
          }
          case 422:{
              errorCode = MHHTTPServiceErrorServiceRequestFailed; /// 請求出錯
              break;
          }
          default:
              /// 從error中解析
              if ([error.domain isEqual:NSURLErrorDomain]) {
    #if defined(DEBUG)||defined(_DEBUG)
                  errorDesc = [NSString stringWithFormat:@"請求出錯了,請稍后重試(%zd)~",error.code];                   /// 調試模式
    #else
                  errorDesc = @"請求出錯了,請稍后重試~";        /// 發布模式
    #endif
                  switch (error.code) {
                      case NSURLErrorSecureConnectionFailed:
                      case NSURLErrorServerCertificateHasBadDate:
                      case NSURLErrorServerCertificateHasUnknownRoot:
                      case NSURLErrorServerCertificateUntrusted:
                      case NSURLErrorServerCertificateNotYetValid:
                      case NSURLErrorClientCertificateRejected:
                      case NSURLErrorClientCertificateRequired:
                          errorCode = MHHTTPServiceErrorSecureConnectionFailed; /// 建立安全連接出錯了
                          break;
                      case NSURLErrorTimedOut:{
    #if defined(DEBUG)||defined(_DEBUG)
                          errorDesc = @"請求超時,請稍后再試(-1001)~"; /// 調試模式
    #else
                          errorDesc = @"請求超時,請稍后再試~";        /// 發布模式
    #endif
                          break;
                      }
                      case NSURLErrorNotConnectedToInternet:{
    #if defined(DEBUG)||defined(_DEBUG)
                          errorDesc = @"網絡開小差了,請稍后重試(-1009)~"; /// 調試模式
    #else
                          errorDesc = @"網絡開小差了,請稍后重試~";        /// 發布模式
    #endif
                          break;
                      }
                  }
              }
      }
      userInfo[MHHTTPServiceErrorHTTPStatusCodeKey] = @(HTTPCode);
      userInfo[MHHTTPServiceErrorDescriptionKey] = errorDesc;
      if (task.currentRequest.URL != nil) userInfo[MHHTTPServiceErrorRequestURLKey] = task.currentRequest.URL.absoluteString;
      if (task.error != nil) userInfo[NSUnderlyingErrorKey] = task.error;
      return [NSError errorWithDomain:MHHTTPServiceErrorDomain code:errorCode userInfo:userInfo];
    }
    

    當然,還有一種錯誤處理就是利用AFNetworking請求數據成功,但是后臺反饋/驗證錯誤信息(msg)。假設code = 100為獲取數據成功 , 而其他code ≠ 100的都是錯誤,且對應錯誤信息msg字段。這個我們也需要處理,并且也得在調試模式下把code提示出來,以便后臺開發人員根據code的值來定位BUG。處理代碼如下:

    {
                          
                          NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
                          userInfo[MHHTTPServiceErrorResponseCodeKey] = @(statusCode);
                          NSString *msgTips = responseObject[MHHTTPServiceResponseMsgKey];
    #if defined(DEBUG)||defined(_DEBUG)
                          msgTips = MHStringIsNotEmpty(msgTips)?[NSString stringWithFormat:@"%@(%zd)",msgTips,statusCode]:[NSString stringWithFormat:@"服務器出錯了,請稍后重試(%zd)~",statusCode];                 /// 調試模式
    #else
                          msgTips = MHStringIsNotEmpty(msgTips)?msgTips:@"服務器出錯了,請稍后重試~";  /// 發布模式
    #endif
                          userInfo[MHHTTPServiceErrorMessagesKey] = msgTips;
                          if (task.currentRequest.URL != nil) userInfo[MHHTTPServiceErrorRequestURLKey] = task.currentRequest.URL.absoluteString;
                          if (task.error != nil) userInfo[NSUnderlyingErrorKey] = task.error;
                          [subscriber sendError:[NSError errorWithDomain:MHHTTPServiceErrorDomain code:statusCode userInfo:userInfo]];
    }
    

    這樣一來,到時候我們提示錯誤信息就變得so easy。例如筆者的在項目中就利用MBProgressHUD來提示錯誤,當然筆者也為該錯誤(NSError)的解析提供了分類:關鍵代碼如下:

    + (NSString *)mh_tipsFromError:(NSError *)error{
        if (!error) return nil;
        NSString *tipStr = nil;
        /// 這里需要處理HTTP請求的錯誤
        if (error.userInfo[MHHTTPServiceErrorDescriptionKey]) {
            tipStr = [error.userInfo objectForKey:MHHTTPServiceErrorDescriptionKey];
        }else if (error.userInfo[MHHTTPServiceErrorMessagesKey]) {
            tipStr = [error.userInfo objectForKey:MHHTTPServiceErrorMessagesKey];
        }else if (error.domain) {
            tipStr = error.localizedFailureReason;
        } else {
            tipStr = error.localizedDescription;
        }
        return tipStr;
    }
    
期待
  1. 文章若對您有些許幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學無止境。
  2. 針對文章所述內容,閱讀期間任何疑問;請在文章底部評論指出,我會火速解決和修正問題。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源碼地址:WeChat
參考鏈接
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容