Супер простой iOS JSON mapper

в 10:49, , рубрики: iOS, велосипед, разработка под iOS, метки: , , ,

Каждый, кто хотя бы раз писал клиент-серверное приложение под iOS, так или иначе сталкивался с маппингом json/xml/прочее в объекты. Иногда это бывает сложно, иногда вообще хочется работать просто со словарями, есть уже много готовых решений типа RestKit, который вообще являет собой универсальный комбайн на все случаи жизни, так зачем же писать очередной велосипед?

Причин может быть много, несколько из них, которые и сподвигли меня написать свой мини-маппер, это:

  • не хочется разбираться с чем-то большим и сложным;
  • мне нужен только маппер, без дополнительных плюшек типа работы с сетью или интеграции с Core Data;
  • если фреймворк работает не так как я хочу, часто разобраться и поправить в нем что-то становится реальной головной болью, особенно если стадия проекта далеко не начальная и отказаться от фреймворка проблемно;
  • мне не нужен в проекте на 3 экрана фреймворк еще на 50 классов и 4 МБ весом;
  • свое всегда роднее.


Первым делом ссылка на GitHub, где лежит исходник базового класса для наших будующих моделей и пример проекта с использованием маппера.

Прежде чем хвалить или еще что-то, опишу минусы:

  • на каждый json объект, какой бы крошечный он не был, придется создать Objective-c класс (хотя может это плюс?);
  • нет никаких проверок входных данных;
  • внимательность разработчика, некоторые вещи не проверяются (нужно мапить массив кастомных объектов, но не указано в какие классы будет оно мапиться, например);
  • еще что-то, доказывающее что маппер абсолютно неправильный и использовать его ни в коем случае нельзя, о чем непременно обоснованно напишут в комментариях.

Из плюсов — простой в использовании, крошечный как по размеру так и по количеству классов, в которых в случае чего придется разбираться, делает свое дело.

Как с ним работать? Маппер состоит из одного класса — TinyMappingModel, это базовый класс для всех последующих классов модели. На каждый объект json создается по наследнику TinyMappingModel, который должен содержать в себе свойства для хранения нужных данных. В идеальном случае стоит называть их так же, как называются соответствующие поля в json — тогда маппинг будет происходить сам по себе, как по волшебству (KVC), о случаях, когда это невозможно (например, поле с название im:name, id, 1work), напишу ниже.

TinyMappingModel содержит в себе 4 метода:

//public
+ (instancetype)mapObjectFromDictionary:(NSDictionary *)data;
+ (NSArray *)mapArrayOfObjects:(NSArray *)data;

//protected methods
- (NSDictionary *)keyToClassMappingRules;
- (NSDictionary *)keyToPropertyNameReplacementRules;

Hаследники по необходимости должны переопределять два последних.

— (NSDictionary *)keyToClassMappingRules; — переопределяется в случае, если нам нужно замапить json объект в какой-то кастомный класс (наследник TinyMappingModel) или в массив. Метод должен возвращать словарь с парами ключ — имя поля в json, значение — класс в который будет мапиться объект или, в случае коллекции, из каких объектов будет состоять коллекция. Например:

- (NSDictionary *)keyToClassMappingRules {
    return @{@"im:name":[TitleModel class], @"im:image":[ImageModel class]};
}

— (NSDictionary *)keyToPropertyNameReplacementRules; — переопределяется в случае, если мы не можем/хотим по каким-то причинам назвать свойство в классе так же, как оно называется в json. Ключ — имя поля в json, значение — название свойства в классе, например:

- (NSDictionary *)keyToPropertyNameReplacementRules {
    return @{@"im:name":@"name",@"im:image":@"images"};
}

Два данных метода из примера будут единственным, что минимально необходимо реализовать в имплементации. В хедере нашего класса (например, EntryModel) будет:

@class TitleModel;

@interface EntryModel : TinyMappingModel

@property (nonatomic, strong) TitleModel *name;
@property (nonatomic, strong) NSArray *images;

@end

С name все понятно, в массиве images будут храниться объекты типа ImageModel, и, само собой, в имплементации нужно импортить нужные классы (в данном случае ImageModel и TitleModel), можно было бы, конечно, сделать на строках, а потом NSClassFromString, но нет.

Далее, как же работать-то? Когда мы получили из сети данные и каким-то образом преобразовали их в json (например, я часто в небольших проектах использую AFNetworking), следует:

//data - NSDictionary c json
EntryModel *model= [EntryModel mapObjectFromDictionary:data];
//или data - NSArray
EntryModel *modelArray= [EntryModel mapArrayOfObjects:data];

Возможны вариации. Главное передать правильный объект в правильный метод, оно там разберется, ничего сложного.

В двух словах опишу как работает маппер.
Самый важный метод + (instancetype)mapObjectFromDictionary:(NSDictionary *)data, он по сути и делает всю работу — создает будующую модель, итерируется по ключам json, решает во что мапить будем (класс – в зависимости от того, что у нас в keyToClassMappingRules и в какое именное свойство – в зависимости от keyToPropertyNameReplacementRules). Далее, в зависимости от того, что из себя представляют данные для текущего ключа и того, во что будем мапить, есть три пути развития событий:

  • данные массив — будет вызван mapArrayOfObjects классу, в который мапим, и результат установится в соответствующее свойство;
  • данные объект — будет вызван mapObjectFromDictionary (этот же метод, но другого класса, класса из -keyToClassMappingRules), в который мапим, и результат установится в соответствующее свойство;
  • данные «примитив» (NSString, NSNumber etc) и keyToPropertyNameReplacementRules ничего не вернул для данного ключа — KVC установит в нужное свойство без вопросов.

Вот в общем и все. Если подытожить, у нас есть три «сущности» — что мапить, в какой класс мапить или в «примитив», в какое свойство мапить (или если не указано, то в свойство с именем ключа json), и в зависимости от того массив ли значение, которое мапиться или нет, вызывается mapObjectFromDictionary или mapArrayOfObjects, соответсвенно. mapArrayOfObjects никакой работы по сути не делает, создает массив и кладет в него [self mapArrayOfObjects:value] или [mappedArray addObject:[self mapObjectFromDictionary:value], где value — значение, получаемое в ходе итерации по входному массиву. Вот и все.

Спасибо за внимание, интересно ваше мнение об удобстве использования, и, если вы посмотрели исходник, о маппере.

Автор: Disee

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js