基于AFNetworking2.0和ReactiveCocoa2.1的iOS REST Client

suiling· 2014-01-26

转自无网不剩的博客

在开发iOS App时经常会遇到跟后端REST API通信的情况。这就涉及到错误处理,NSDictionary与Model的映射,用户登录与登出,权限验证,Archive/UnArchive,Copy,AccessToken过期处理等等,如果没有很好地处理这些点,就容易出现代码复杂度增大,结构散乱,不方便后期维护的现象。

 

正好最近在看AFNetworking2.0和ReactiveCocoa2.1,参考了github的octokit,重写了花瓣的iOS REST API,分享些心得。

 

基本结构

  1. |- HBPAPI.h 
  2. |- Classes 
  3.     |- HBPAPIManager.h 
  4.     |- HBPAPIManager.m 
  5.     |- Models 
  6.         |- HBPObject.h 
  7.         |- HBPObject.m 
  8.         |- HBPUser.h 
  9.         |- HBPUser.m 
  10.         ... 

使用时,直接引用HBPAPI.h即可,里面包含了所有的Class。因为使用了AFNetworking2.0,所以不再是HBPClient,而是HBPManager。 HBPAPIManager包含了所有的跟服务端通信的方法,通过Category来区分。

  1. #pragma mark - HBPAPIManager (Private) 
  2.  
  3. @interface HBPAPIManager (Private) 
  4.  
  5. // 内部统一使用这个方法来向服务端发送请求 
  6. // 
  7. // resultClass - 从服务端获取到JSON数据后,使用哪个Class来将JSON转换为OC的Model 
  8. // listKey - 如果不指定,表示返回的是一个object,如user,如果指定表示返回的是一个数组,listKey就表示这个列表的keyname,如{'users':[]}, 那么listName就为'user' 
  9. - (RACSignal *)requestWithMethod:(NSString *)method relativePath:(NSString *)relativePath parameters:(NSDictionary *)parameters resultClass:(Class)resultClass listKey:(NSString *)listKey; 
  10. @end 
  11.  
  12.  
  13. #pragma mark - HBPAPIManager (User) 
  14.  
  15. @interface HBPAPIManager (User) 
  16.  
  17. // signal会send user,如果没有user,就会sendError 
  18. // 必须当前用户已经登录的情况下调用 
  19. - (RACSignal *)fetchUserInfo; 
  20.  
  21. // ... 
  22. @end 
  23.  
  24.  
  25. #pragma mark - HBPAPIManager (Friendship) 
  26. // ... 

Models Group包含了所有跟服务端API对应的Model,比如HBPComment

HBPComment.h

  1. #import "HBPObject.h" 
  2.  
  3. @class HBPUser; 
  4.  
  5. @interface HBPComment : HBPObject 
  6. @property (nonatomic, assign) NSInteger commentID; 
  7. @property (nonatomic, copy) NSString *createdAt; 
  8. @property (nonatomic, strong) HBPUser *user; 
  9. //... 
  10. @end 

HBPComment.m

  1. #import "HBPComment.h" 
  2.  
  3. @implementation HBPComment 
  4.  
  5. - (NSDictionary *)JSONKeysToPropertyKeys 
  6.     return @{ 
  7.              @"comment_id": @"commentID"
  8.              @"user_id": @"userID"
  9.              @"created_at": @"createdAt"
  10.              //... 
  11.              }; 
  12.  
  13. @end 

 

Archive / UnArchive / Copy

每一个Object都要支持Archive / UnArchive / Copy,也就是要实现<NSCoding>和<NSCopying>协议,这两个协议的内容其实就是对Object的Property做些处理,所以如果可以在基类里把这些事都统一处理,就会方便许多。octokit使用Mantle来做这些事情,不过我觉得Mantle还是有些麻烦,于是写了个通过运行时来获取property,并实现<NSCoding> 和 <NSCopying>的基类,只有两个公共方法:

  1. #import <Foundation/Foundation.h> 
  2.  
  3. @interface HBPObject : NSObject <NSCopying, NSCoding> 
  4.  
  5. // 解析API返回的JSON,返回对应的Model 
  6. - (id)initWithDictionary:(NSDictionary *)JSON; 
  7.  
  8. // JSON key到property的映射关系 
  9. - (NSDictionary *)JSONKeysToPropertyKeys; 
  10. @end 

其中- (id)initWithDictionary:(NSDictionary *)JSON的作用是遍历Object的Property,如果Property的Class是HBPObject,那么就调用- (id)initWithDictionary:(NSDictionary *)JSO,不然就通过KVC的setValue:forKey:来设定值。

 

- (NSDictionary *)JSONKeysToPropertyKeys的内容大概是这样:

  1. - (NSDictionary *)JSONKeysToPropertyKeys 
  2.     return @{ 
  3.              @"id": @"ID"
  4.              @"nav_link": @"navLink"
  5.              }; 

这样通过一个HBPObject基类就完成了 Archive / UnArchive / Copy 。

 

用户的登录与登出

先来说说登录,由于使用RAC,在构造API时,就不需要传入Block了,随之而来的一个问题就是需要在注释中说明sendNext时会发送什么内容。

  1. + (RACSignal *)signInUsingUsername:(NSString *)username passowrd:(NSString *)password 
  2.     NSAssert(API_CLIENT_ID && API_CLIENT_SECRET, @"API_CLIENT_ID and API_CLIENT_SECRET must be setted"); 
  3.     NSDictionary *parameters = @{ 
  4.                                  @"grant_type": @"password"
  5.                                  @"username": username, 
  6.                                  @"password": password, 
  7.                                  }; 
  8.     HBPAPIManager *manager = [self createManager]; 
  9.      
  10.     return [[manager fetchTokenWithParameters:parameters] 
  11.             setNameWithFormat:@"+signInUsingUsername:%@ password:%@", username, password]; 

 看着还挺简单的吧,因为主要工作都是+fetchMoreData:parameters在做,看看它的实现

  1. - (RACSignal *)fetchTokenWithParameters:(NSDictionary *)parameters 
  2.     return [[[[[[[self rac_POST:@"oauth/access_token" parameters:parameters] 
  3.              // reduceEach的作用是传入多个参数,返回单个参数,是基于`map`的一种实现 
  4.              reduceEach:^id(AFHTTPRequestOperation *operation, NSDictionary *response){ 
  5.                  // 拿到token后,就设置token property 
  6.                  // setToken:方法会被触发,在那里会设置请求的头信息,如Authorization。 
  7.                  HBPAccessToken *token = [[HBPAccessToken alloc] initWithDictionary:response]; 
  8.                  self.token = token; 
  9.                  return self; 
  10.              }] 
  11.              catch:^RACSignal *(NSError *error) { 
  12.                  // 对Error进行处理,方便外部识别 
  13.                  NSInteger code = error.code == -1001 ? HBPAPIManagerErrorConnectionFailed : HBPAPIManagerErrorAuthenticatedFailed; 
  14.                  NSError *apiError = [[NSError alloc] initWithDomain:HBPAPIManagerErrorDomain code:code userInfo:nil]; 
  15.                  return [RACSignal error:apiError]; 
  16.              }] 
  17.              then:^RACSignal *{ 
  18.                  // 一切正常的话,顺便获取用户信息 
  19.                  return [self fetchUserInfo]; 
  20.              }] 
  21.              doNext:^(HBPUser *user) { 
  22.                  // doNext相当于一个钩子,是在sendNext时会被执行的一段代码 
  23.                  self.user = user; 
  24.              }] 
  25.              // 把发送内容换成self 
  26.              mapReplace:self] 
  27.              // 避免side effect 
  28.              replayLazily]; 

这里对signal进行了chain / modify / hook 等操作,主要作用是获取access token和用户信息。

 

用户的登出就简单了,直接设置user和token为nil就行了。

 

设置超时时间和缓存策略

因为AF2.0使用了新的架构,导致要设置Request的超时和缓存稍微有些麻烦,需要新建一个继承自AFHTTPRequestSerializer的Class

  1. @interface HBPAPIRequestSerializer : AFHTTPRequestSerializer @end 
  2.  
  3. @implementation HBPAPIRequestSerializer 
  4.  
  5. - (NSMutableURLRequest *)requestWithMethod:(NSString *)method URLString:(NSString *)URLString parameters:(NSDictionary *)parameters 
  6.     NSMutableURLRequest *request = [super requestWithMethod:method URLString:URLString parameters:parameters]; 
  7.     request.timeoutInterval = 10; 
  8.     request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; 
  9.     return request; 
  10.  
  11. @end 

 

然后将这个class设置为manager.requestSerializer

  1. HBPAPIManager *manager = [[HBPAPIManager alloc] initWithBaseURL:[NSURL URLWithString:API_SERVER]]; 
  2. manager.requestSerializer = [HBPAPIRequestSerializer serializer]; 

这样就行了。

 

权限验证

这个比较简单些,直接在方法里面加上判断

  1. - (RACSignal *)createCommentWithPinID:(NSInteger)pinID text:(NSString *)text 
  2.     if (!self.isAuthenticated) return [RACSignal error:[self.class authenticatedError]]; 
  3.   
  4.     return [self requestWithMethod:@"POST" relativePath:[NSString stringWithFormat:@"pins/%d/comments", pinID] parameters:@{@"text": text} resultClass:[HBPComment class] listKey:@"comment"]; 

 

AccessToken过期的处理

AccessToken过期和获取新的AccessToken可以交给使用者来做,但是会比较麻烦,最好的方法是过期后自动去获取新的AccessToken,拿到Token后自动去执行之前失败的请求,这块我是这么处理的

  1. - (RACSignal *)requestWithMethod:(NSString *)method relativePath:(NSString *)relativePath parameters:(NSDictionary *)parameters resultClass:(Class)resultClass listKey:(NSString *)listKey 
  2.     RACSignal *requestSignal; 
  3.     // create requestSignal 
  4.     // ... 
  5.  
  6.     return [requestSignal catch:^RACSignal *(NSError *error) { 
  7.         if (error.code == HBPAPIManagerErrorInvalidAccessToken) { 
  8.             return [[[self refreshToken] ignoreValues] concat:requestSignal]; 
  9.         } 
  10.         return [RACSignal error:error]; 
  11.     }]; 

 

HBPObject SubClass

那些继承自HBPObject的子类,有些事情是HBPObject无法处理的,比如NSArray的Property,因为Objective-C不支持generic,所以无法知道这个数组包含的究竟是怎样的Class,这时就需要在子类对这些property做处理。

 

比如画板(HBPBoard)有一个叫pins的NSArray属性,因为在HBPObject中使用了KVC,所以如果子类有类似setXXX:的方法的话,那么该方法就会被调用,利用这一点,就可以处理那些特殊情况。

  1. @implementation HBPBoard 
  2.  
  3. - (void)setPins:(NSArray *)pins 
  4.     _pins = [[pins.rac_sequence map:^id(id value) { 
  5.         return [[HBPPin alloc] initWithDictionary:value]; 
  6.     }] array]; 
  7.  
  8. @end 

 

再比如,返回的JSON内容中,有一个叫content的key,其中有type / date / color 等sub key,而你只想要type信息,只需添加一个type property,然后在setContent时,设置一下type即可。

  1. - (void)setContent:(NSDictionary *)content 
  2.     _type = content[@"type"]; 

 以上就是我在使用AFNetworking2.0和ReactiveCocoa2.1构建iOS REST Client时的一些小心得,确实能感觉到RAC带了不少方便,虽然也同时带来了一些弊端(如返回的内容不明确,学习成本高),但还是利大于弊。

 

有什么问题和想法,欢迎交流。