Всем привет! Недавно, в обучающих целях, в моей компании мне дали задание попробовать написать максимально гибкий сетевой слой для API Инстаграмма. И теперь я хотел бы поделиться с Вами своим результатом, проделанным в несколько дней. Вероятно, глазами другими, написанный вариант будет выглядеть не таким уж хорошим и гибким, но мне понравилось то, до чего я дошел в конечном результате.

Начнем с того, что я сделал выбор в пользу замечательных AFNetworking и Mantle. Почему? Ответ очевиден: AFNetworking умеет правильно составлять NSURLRequest’ы с нужными нами параметрами и хэдэрами, а так же парсить ответ от сервера в правильный JSON. Mantle же тоже делает свое дело, парсит JSON в нужную нам модель класса. Это действительно волшебный тандем “AFNetworking & Mantle”, который глупо не использовать.

Какие главные цели когда мы пишем свой сетевой слой? Я хочу чтобы мой сетевой слой был эластичным, простым, с высокой сочетаемостью и гибким.

Поехали!

Структура слоя получилась следующая:

  1. VASResourceManager - менеджер, который отдает нам конечный ответ относительно конкретного запроса с нашими параметрами и методом, с ним будут работать непосредственно вью модели.
  2. VASNetworkRequestManager - умеет посылать запросы, который в качестве аргументов принимает наши параметры.

Помощниками Network Request Manager’a являются классы VASOperationManager, родителем которого является AFRequestOperationManager и VASJSONResponseSerializer, тоже родителем которого является AFJSONResponseSerializer.

Вероятно, было бы удобно, получать ответ от реквестов сразу в виде модели какого-либо класса? Мы вызываем метод, передаем ему аргументы с параметрами запроса, а так же передаем класс модели, в виде которой мы хотели бы получить уже готовый ответ.

В этом нам поможет AFJSONResponseSerializer, который собственно парсит ответ от сервера в JSON. Все, что нам потребуется, так это засабкласиться от данного класса, написать свой метод для инициализации, где в качестве аргумента мы укажем класс нашей модели, и дописать свою логику в следующем методе:

- (id)responseObjectForResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *__autoreleasing *)error

Инициализатор класса будет следующим:

#import "AFURLResponseSerialization.h"

@interface VASJSONResponseSerializer : AFJSONResponseSerializer

- (instancetype)initWithResultClass:(Class)resultClass;

@end

Теперь перейдем в файл имплементации и допишем нашу логику для парсинга JSON в готовую модель класса с помощью Mantle:

#import "VASJSONResponseSerializer.h"

@interface VASJSONResponseSerializer()

@property (nonatomic, strong) Class resultClass;
@property (nonatomic, strong) id resultResponseObject;

@end

@implementation VASJSONResponseSerializer

- (instancetype)initWithResultClass:(Class)resultClass
{
    if (self = [super init]) {
        self.resultClass = resultClass;
        self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", nil];
    }
    return self;
}

- (id)responseObjectForResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *__autoreleasing *)error
{
// Используем super метод класса, который возвращает нам готовый JSON
    id responseObject = [super responseObjectForResponse:response data:data error:error];
    
// Если мы передали класс модели, то парсим JSON в модель, иначе возвращаем обычный чистый JSON
    if (self.resultClass)
    {
        if ([responseObject[@"data"] isKindOfClass:[NSArray class]])
        {
            self.resultResponseObject = [MTLJSONAdapter modelsOfClass:self.resultClass
                                                        fromJSONArray:responseObject[@"data"]
                                                                error:NULL];
        }
        else if ([responseObject[@"data"] isKindOfClass:[NSDictionary class]])
        {
            self.resultResponseObject = [MTLJSONAdapter modelOfClass:self.resultClass
                                                  fromJSONDictionary:responseObject[@"data"]
                                                               error:NULL];
        }
    }
    else
    {
        return responseObject;
    }
    
    return self.resultResponseObject;
}

@end

На этом завершается работа с данным классом.

Перейдем к следующему классу - VASOperationManager. Данный класс менеджер будет уметь возвращать AFHTTPRequestOperation в методе, в который мы передадим параметры для запроса к серверу. Так же нужно добавить свой инициализатор.

Код заголовочного файла:

#import "AFHTTPRequestOperationManager.h"

typedef void(^OperationManagerCompletionBlockWithSuccess)(AFHTTPRequestOperation *operation, id responseObject);
typedef void(^OperationManagerCompletionBlockWithFailure)(AFHTTPRequestOperation *operation, NSError *error);

@interface VASOperationManager : AFHTTPRequestOperationManager

#pragma mark - Initialize

- (instancetype)initWithBaseURL:(NSURL *)url
               configurationAPI:(id)configuration;

#pragma mark - Operations
#pragma mark GET

- (AFHTTPRequestOperation *)operationWithGET:(NSString *)method
                                  parameters:(id)parameters
                                 resultClass:(Class)resultClass
                                     success:(OperationManagerCompletionBlockWithSuccess)success
                                     failure:(OperationManagerCompletionBlockWithFailure)failure;

@end

Файл имплементации:

#import "VASOperationManager.h"

#import "VASJSONResponseSerializer.h"

@interface VASOperationManager()

@property (nonatomic, strong) NSMutableDictionary *parameters;

@end

@implementation VASOperationManager

#pragma mark - Initialize

- (instancetype)initWithBaseURL:(NSURL *)url
               configurationAPI:(id)configuration
{
    if (self = [super initWithBaseURL:url]) {
        _parameters = [NSMutableDictionary dictionaryWithDictionary:configuration];
    }
    return self;
}

#pragma mark - Operations

- (AFHTTPRequestOperation *)operationWithGET:(NSString *)method
                                  parameters:(id)parameters
                                 resultClass:(Class)resultClass
                                     success:(OperationManagerCompletionBlockWithSuccess)success
                                     failure:(OperationManagerCompletionBlockWithFailure)failure
{
// Создаем реквест с методом и параметрами
    NSURLRequest *urlRequest = [self requestWithGET:method parameters:parameters];

// Затем создаем операцию c нашим реквестом
    AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithRequest:urlRequest
                                                                      success:^(AFHTTPRequestOperation *operation, id responseObject) {
                                                                          if (responseObject) {
                                                                              if (success)
                                                                                  success(operation, responseObject);
                                                                          }
                                                                      } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                                                          if (error) {
                                                                              if (failure)
                                                                                  failure(operation, error);
                                                                          }
                                                                      }];
// У класса AFRequestOperation имеется свойство с responseSerializer. Устанавливая наш сериализатор, операция в данном случае будет использовать наш сабкласс, и в итоге мы будем получать ответ в нужном нам виде. 
    VASJSONResponseSerializer *responseSerializer = [[VASJSONResponseSerializer alloc] initWithResultClass:resultClass];
    operation.responseSerializer = responseSerializer;
    
    return operation;
}

#pragma mark - Request's

- (NSURLRequest *)requestWithGET:(NSString *)method parameters:(id)parameters
{
    [self.parameters addEntriesFromDictionary:parameters];
    NSString *urlString = [[NSURL URLWithString:method? : [NSString string] relativeToURL:self.baseURL] absoluteString];
    
// То, зачем нужно использовать AFNetworking. Это способность создать правильный реквест с методом, параметрами и возможно еще дополнительными хэдэрами. 
    NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET"
                                                                             URLString:urlString
                                                                            parameters:self.parameters
                                                                                 error:NULL];
    return urlRequest;
}

На этом моменте мы уже можем делать запросы к серверу, используя данный менеджер.

Перейдем к реализации VASNetworkRequestManager:

Он будет иметь почти идентичный заголовочный файл, как и в случае с VASOperationManager:

#import <Foundation/Foundation.h>

@class AFHTTPRequestOperation;

typedef void(^NetworkRequestCompletionBlockWithSuccess)(id responseObject);
typedef void(^NetworkRequestCompletionBlockWithFailure)(NSError *error);

@interface VASNetworkRequestManager : NSObject

#pragma mark - Initialize

- (instancetype)initWithBaseURL:(NSURL *)baseURL
          baseRequestParameters:(NSDictionary *)parameters;

#pragma mark - Request's

- (AFHTTPRequestOperation *)sendGetRequestWithMethod:(NSString *)method
                                          parameters:(id)parameters
                                         resultClass:(Class)resultClass
                                             success:(NetworkRequestCompletionBlockWithSuccess)success
                                             failure:(NetworkRequestCompletionBlockWithFailure)failure;

@end

А так же файл имплементации:

#import "VASNetworkRequestManager.h"

#import "AFNetworking.h"
#import "VASOperationManager.h"

@interface VASNetworkRequestManager()

@property (nonatomic, strong) VASOperationManager *operationManager;

@end

@implementation VASNetworkRequestManager

#pragma mark - Initialize

- (instancetype)initWithBaseURL:(NSURL *)baseURL
          baseRequestParameters:(NSDictionary *)parameters
{
    if (self = [super init]) {
        _operationManager = [[VASOperationManager alloc] initWithBaseURL:baseURL
                                                        configurationAPI:parameters];
    }
    return self;
}

#pragma mark - Request's

- (AFHTTPRequestOperation *)sendGetRequestWithMethod:(NSString *)method
                                          parameters:(id)parameters
                                         resultClass:(Class)resultClass
                                             success:(NetworkRequestCompletionBlockWithSuccess)success
                                             failure:(NetworkRequestCompletionBlockWithFailure)failure
{
//  Используем метод нашего кастомного менеджера, в который передаем метод, параметры и класс модели
    AFHTTPRequestOperation *operation = [self.operationManager operationWithGET:method
                                                                     parameters:parameters
                                                                    resultClass:resultClass
                                                                        success:^(AFHTTPRequestOperation *operation, id responseObject) {
                                                                            if (responseObject) {
                                                                                if (success)
                                                                                    success(responseObject);
                                                                            }
                                                                        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                                                            if (error) {
                                                                                if (failure)
                                                                                    failure(error);
                                                                            }
                                                                        }];
// После запускаем операцию и возвращаем ее
    [operation start];
    
    return operation;
}

@end

И наконец мы добрались до финального класса, который будем всем этим воротить - VASResourceManager. В случае с API Инстаграмма, я хотел получать полную информацию о пользователе и его последние медиа.

Собственно, два метода:

#import <Foundation/Foundation.h>

typedef void(^CompletionBlockWithSuccess)(id responseObject);
typedef void(^CompletionBlockWithFailure)(NSError *error);

@interface VASResourceManager : NSObject

- (void)requestUserInfoWithSuccess:(CompletionBlockWithSuccess)success
                           failure:(CompletionBlockWithFailure)failure;
- (void)requestRecentUserMediaListWithSuccess:(CompletionBlockWithSuccess)success
                                      failure:(CompletionBlockWithFailure)failure;

@end

Перейдем к реализации:

#import "VASResourceManager.h"

#import "AFNetworking.h"
#import "VASNetworkRequestManager.h"
#import "VASUser.h"
#import "VASMedia.h"

static NSString *const kUserRecentMediaAPIMethod = @"media/recent";

@interface VASResourceManager()

@property (nonatomic, strong) VASNetworkRequestManager *manager;

@end

@implementation VASResourceManager

- (instancetype)init
{
    if (self = [super init])
    {
        _manager = [[VASNetworkRequestManager alloc] initWithBaseURL:[NSURL URLWithString:kInstagramBaseAPIUrl]
                                               baseRequestParameters:@{
                                                                       @"client_id" : kInstagramAPIClientID
                                                                       }];
    }
    return self;
}

- (void)requestUserInfoWithSuccess:(CompletionBlockWithSuccess)success
                           failure:(CompletionBlockWithFailure)failure
{
    [self.manager sendGetRequestWithMethod:nil
                                parameters:nil
                               resultClass:[VASUser class]
                                   success:^(id responseObject) {
                                       if (responseObject) {
                                           if (success)
                                               success(responseObject);
                                       }
                                   }
                                   failure:^(NSError *error) {
                                       
                                   }];
}

- (void)requestRecentUserMediaListWithSuccess:(CompletionBlockWithSuccess)success
                                      failure:(CompletionBlockWithFailure)failure
{
    [self.manager sendGetRequestWithMethod:kUserRecentMediaAPIMethod
                                parameters:nil
                               resultClass:[VASMedia class]
                                   success:^(id responseObject) {
                                       if (responseObject) {
                                           if (success)
                                               success(responseObject);
                                       }
                                   }
                                   failure:^(NSError *error) {
                                       
                                   }];
}

@end

В данном менеджере мы используем наш VASNetworkRequestManager, и его метод, который возвращает нам запущенную операцию, и в случае чего мы сможем ее остановить и проделать многие другие манипуляции в соответствии с определенной ситуацией.

Если поставить брэйкпоинт в методе запроса информации о пользователе в success блоке, мы увидим следующую красивую распарсенную готовую модель VASUser в responseObject:

image

Неправда ли удобно?!

На этом собственно все, что хотелось показать. С данным классом, как я уже написал, должны уже работать вьюмодели и получать готовые данные в виде моделей.

Пример кода лежит здесь. Всем спасибо за внимание!