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阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容