Mantle позволяет легко написать простой слой модели для вашего приложения Cocoa или Cocoa Touch.
Что не так с тем, как объекты модели обычно пишутся на Objective-C?
Давайте воспользуемся API GitHub для демонстрации. Как обычно можно представить проблему GitHub в Objective-C?
typedef перечисление: NSUInteger { GHIssueStateOpen, GHIssueStateClosed } GHIssueState;@interface GHIssue : NSObject <NSCoding, NSCopying>@property (неатомарное, копирование, только чтение) NSURL *URL;@property (неатомарное, копирование, только чтение) NSURL *HTMLURL;@property (неатомарное, копирование, только чтение) NSNumber * номер;@property (неатомарный, назначить, только для чтения) GHIssueState state;@property (неатомарный, копировать, только для чтения) NSString *reporterLogin;@property (неатомарный, копировать, только для чтения) NSDate *updatedAt;@property (неатомарный, сильный, только для чтения) GHUser *assignee;@property (неатомарный, копировать) , только для чтения) NSDate *retrivedAt;@property (неатомарный, копия) NSString *title;@property (неатомарный, копия) NSString *body; - (id)initWithDictionary:(NSDictionary *)dictionary;@end
@implementation GHIssue+ (NSDateFormatter *)dateFormatter {NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.dateFormat = @"гггг-ММ-дд'Т'ЧЧ:мм:сс'Z'";return dateFormatter; } - (id)initWithDictionary:(NSDictionary *)dictionary { self = [self init];if (self == nil) return nil; _URL = [NSURL URLWithString:dictionary[@"url"]]; _HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]]; _number = словарь[@"number"];if ([dictionary[@"state"] isEqualToString:@"open"]) { _state = GHIssueStateOpen; } else if ([dictionary[@"state"] isEqualToString:@"closed"]) { _state = GHIssueStateClosed; } _title = [словарь[@"title"] копия]; _retrivedAt = [дата NSDate]; _body = [словарь[@"body"] копия]; _reporterLogin = [словарь[@"user"][@"login"] копия]; _assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]]; _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];return self; } - (id)initWithCoder:(NSCoder *)coder { self = [self init];if (self == nil) return nil; _URL = [кодер decodeObjectForKey:@"URL"]; _HTMLURL = [кодер decodeObjectForKey:@"HTMLURL"]; _number = [кодер decodeObjectForKey:@"number"]; _state = [кодер decodeUnsignedIntegerForKey:@"state"]; _title = [кодер decodeObjectForKey:@"title"]; _retrivedAt = [дата NSDate]; _body = [кодер decodeObjectForKey:@"body"]; _reporterLogin = [кодер decodeObjectForKey:@"reporterLogin"]; _assignee = [кодер decodeObjectForKey:@"assignee"]; _updatedAt = [кодер decodeObjectForKey:@"updatedAt"];return self; } - (void)encodeWithCoder:(NSCoder *)coder {if (self.URL != nil) [coder encodeObject:self.URL forKey:@"URL"];if (self.HTMLURL != nil) [coder encodeObject:self .HTMLURL forKey:@"HTMLURL"];if (self.number != nil) [кодер encodeObject:self.number forKey:@"number"];if (self.title != ноль) [кодер encodeObject:self.title forKey:@"title"];if (self.body != ноль) [кодер encodeObject:self.body forKey: @"body"];if (self.reporterLogin != ноль) [кодер encodeObject:self.reporterLogin forKey:@"reporterLogin"];if (self.assignee != ноль) [кодер encodeObject:self.assignee forKey:@"assignee"];if (self.updatedAt != ноль) [кодер encodeObject:self.updatedAt forKey:@"updatedAt"]; [кодер encodeUnsignedInteger:self.state forKey:@"state"]; } - (id)copyWithZone:(NSZone *)zone { GHIssue *issue = [[self.class allocWithZone:zone] init]; проблема->_URL = self.URL; проблема->_HTMLURL = self.HTMLURL; проблема->_номер = self.number; проблема->_state = self.state; проблема->_reporterLogin = self.reporterLogin; проблема->_assignee = self.assignee; проблема->_updatedAt = self.updatedAt; Issue.title = self.title; Issue->_retievedAt = [Дата NSDate]; Issue.body = self.body;вернуть проблему; } - (NSUInteger)хэш {return self.number.hash; } - (BOOL)isEqual:(GHIssue *)issue {if (![isSue isKindOfClass:GHIssue.class]) return NO;return [self.number isEqual:issue.number] && [self.title isEqual:issue.title] && [self.body isEqual:issue.body]; }@конец
Ох, это слишком много шаблонов для такой простой вещи! И даже в этом случае есть некоторые проблемы, которые этот пример не решает:
Невозможно обновить GHIssue
новыми данными с сервера.
Невозможно превратить GHIssue
обратно в JSON.
GHIssueState
не следует кодировать как есть. Если перечисление изменится в будущем, существующие архивы могут сломаться.
Если интерфейс GHIssue
изменится в будущем, существующие архивы могут сломаться.
Core Data очень хорошо решает определенные проблемы. Если вам нужно выполнять сложные запросы к вашим данным, обрабатывать огромный граф объектов с множеством связей или поддерживать отмену и повтор, Core Data отлично подойдет.
Однако у него есть несколько болевых точек:
Там еще много шаблонов. Управляемые объекты сокращают часть шаблонного шаблона, рассмотренного выше, но в Core Data имеется множество собственных. Правильная настройка стека Core Data (с постоянным хранилищем и координатором постоянного хранилища) и выполнение выборки могут занять много строк кода.
Трудно получить право. Даже опытные разработчики могут допускать ошибки при использовании Core Data, и фреймворк этого не прощает.
Если вы просто пытаетесь получить доступ к некоторым объектам JSON, Core Data может потребовать много работы без особой пользы.
Тем не менее, если вы уже используете или хотите использовать Core Data в своем приложении, Mantle все равно может быть удобным слоем перевода между API и объектами вашей управляемой модели.
Введите МТЛМодель . Вот как выглядит GHIssue
, наследующий от MTLModel
:
typedef перечисление: NSUInteger { GHIssueStateOpen, GHIssueStateClosed } GHIssueState;@interface GHIssue : MTLModel <MTLJSONSerializing>@property (неатомарный, копирование, только чтение) NSURL *URL;@property (неатомарный, копирование, только чтение) NSURL *HTMLURL;@property (неатомарный, копирование, только чтение) NSNumber *number; @property (неатомарный, присваиваемый, только для чтения) GHIssueState state;@property (неатомарное, копирование, только чтение) NSString *reporterLogin;@property (неатомарное, сильное, только чтение) GHUser *assignee;@property (неатомарное, копирование, только чтение) NSDate *updatedAt;@property (неатомарное, копирование) NSString *title;@property (неатомарный, копия) NSString *body;@property (неатомарный, копирование, только чтение) NSDate *retievedAt;@end
@implementation GHIssue+ (NSDateFormatter *)dateFormatter {NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.dateFormat = @"гггг-ММ-дд'Т'ЧЧ:мм:сс'Z'";return dateFormatter; } + (NSDictionary *)JSONKeyPathsByPropertyKey {return @{@"URL": @"url",@"HTMLURL": @"html_url",@"number": @"number",@"state": @"state", @"reporterLogin": @"user.login",@"assignee": @"assignee",@"updatedAt": @"updated_at"}; } + (NSValueTransformer *)URLJSONTransformer {return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)HTMLURLJSONTransformer {return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)stateJSONTransformer {return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{@"open": @(GHIssueStateOpen), @"closed": @(GHIssueStateClosed) }]; } + (NSValueTransformer *)assigneeJSONTransformer {return [MTLJSONAdapter словарьTransformerWithModelClass:GHUser.class]; } + (NSValueTransformer *)updatedAtJSONTransformer {return [MTLValueTransformer TransformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter dateFromString:dateString]; }verseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter stringFromDate:date]; }]; } - (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil;// Сохраняем значение, которое необходимо определить локально при инициализации._retievedAt = [NSDate date];return self; }@конец
Примечательно, что в этой версии отсутствуют реализации <NSCoding>
, <NSCopying>
, -isEqual:
и -hash
. Проверяя объявления @property
в вашем подклассе, MTLModel
может предоставить реализации по умолчанию для всех этих методов.
Все проблемы исходного примера также устранены:
Невозможно обновить
GHIssue
новыми данными с сервера.
MTLModel
имеет расширяемый метод -mergeValuesForKeysFromModel:
который позволяет легко указать, как следует интегрировать данные новой модели.
Невозможно превратить
GHIssue
обратно в JSON.
Вот здесь-то и пригодятся реверсивные трансформаторы. +[MTLJSONAdapter JSONDictionaryFromModel:error:]
может преобразовать любой объект модели, соответствующий <MTLJSONSerializing>
, обратно в словарь JSON. +[MTLJSONAdapter JSONArrayFromModels:error:]
— то же самое, но превращает массив объектов модели в массив словарей JSON.
Если интерфейс
GHIssue
изменится в будущем, существующие архивы могут сломаться.
MTLModel
автоматически сохраняет версию объекта модели, которая использовалась для архивирования. При разархивировании будет вызываться -decodeValueForKey:withCoder:modelVersion:
если он переопределен, что дает вам удобный способ обновления старых данных.
Чтобы сериализовать объекты модели из JSON или в него, вам необходимо реализовать <MTLJSONSerializing>
в своем подклассе MTLModel
. Это позволяет вам использовать MTLJSONAdapter
для преобразования объектов вашей модели из JSON и обратно:
NSError * ошибка = ноль; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
NSError *error = nil;NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel: ошибка пользователя:&error];
+JSONKeyPathsByPropertyKey
Словарь, возвращаемый этим методом, определяет, как свойства объекта модели сопоставляются с ключами в представлении JSON, например:
@interface XYUser : MTLModel@property (только чтение, неатомарный, копирование) NSString *name;@property (только чтение, неатомарный, сильный) NSDate *createdAt;@property (только чтение, неатомарный, присваивание, getter = isMeUser) BOOL meUser;@property ( только для чтения, неатомарный, сильный) XYHelper *helper;@end@implementation XYUser+ (NSDictionary *)JSONKeyPathsByPropertyKey {return @{@"name": @"name",@"createdAt": @"created_at"}; } - (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil; _helper = [XYHelper helperWithName:self.namecreatedAt:self.createdAt];вернуть себя; }@конец
В этом примере класс XYUser
объявляет четыре свойства, которые Mantle обрабатывает по-разному:
name
сопоставляется с одноименным ключом в представлении JSON.
createdAt
преобразуется в эквивалент в виде змеи.
meUser
не сериализуется в JSON.
helper
инициализируется ровно один раз после десериализации JSON.
Используйте -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
, если суперкласс вашей модели также реализует MTLJSONSerializing
для объединения их сопоставлений.
Если вы хотите сопоставить все свойства класса модели сами с собой, вы можете использовать вспомогательный метод +[NSDictionary mtl_identityPropertyMapWithModel:]
.
При десериализации JSON с помощью +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
ключи JSON, которые не соответствуют имени свойства или имеют явное сопоставление, игнорируются:
NSDictionary *JSONDictionary = @{@"name": @"john",@"created_at": @"2013/07/02 16:40:00 +0000",@"plan": @"lite"}; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
Здесь plan
будет проигнорирован, поскольку он не соответствует имени свойства XYUser
и не отображается иным образом в +JSONKeyPathsByPropertyKey
.
+JSONTransformerForKey:
Реализуйте этот необязательный метод, чтобы преобразовать свойство из другого типа при десериализации из JSON.
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key { if ([key isEqualToString:@"createdAt"]) { return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName]; } return nil; }
key
— это ключ, который применяется к вашему объекту модели; не оригинальный ключ JSON. Имейте это в виду, если вы преобразуете имена ключей с помощью +JSONKeyPathsByPropertyKey
.
Для дополнительного удобства, если вы реализуете +<key>JSONTransformer
, MTLJSONAdapter
вместо этого будет использовать результат этого метода. Например, даты, которые обычно представляются в виде строк в JSON, можно преобразовать в NSDate
следующим образом:
return [MTLValueTransformer TransformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter dateFromString:dateString]; }verseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter stringFromDate:date]; }]; }
Если преобразователь обратимый, он также будет использоваться при сериализации объекта в JSON.
+classForParsingJSONDictionary:
Если вы реализуете кластер классов, реализуйте этот дополнительный метод, чтобы определить, какой подкласс вашего базового класса следует использовать при десериализации объекта из JSON.
@interface XYMessage : MTLModel@end@interface XYTextMessage: XYMessage@property (только чтение, неатомарный, копирование) NSString *body;@end@interface XYPictureMessage : XYMessage@property (только чтение, неатомарный, сильный) NSURL *imageURL;@end@implementation XYMessage+ (Класс) classForParsingJSONDictionary: (NSDictionary *) JSONDictionary {if (JSONDictionary [@"image_url"]! = ноль) {return XYPictureMessage.class; }if (JSONDictionary[@"body"] != ноль) {return XYTextMessage.class; }NSAssert(NO, @"Нет соответствующего класса для словаря JSON '%@'.", JSONDictionary);return self; }@конец
Затем MTLJSONAdapter
выберет класс на основе переданного вами словаря JSON:
NSDictionary *textMessage = @{@"id": @1,@"body": @"Hello World!"};NSDictionary *pictureMessage = @{@"id": @2,@"image_url": @"http: //example.com/lolcat.gif"}; XYTextMessage *messageA = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:textMessage error:NULL]; XYPictureMessage *messageB = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:pictureMessage error:NULL];
Mantle не сохраняет ваши объекты автоматически. Однако MTLModel
соответствует <NSCoding>
, поэтому объекты модели можно архивировать на диск с помощью NSKeyedArchiver
.
Если вам нужно что-то более мощное или вы хотите избежать одновременного хранения всей модели в памяти, лучшим выбором может стать Core Data.
Mantle поддерживает следующие цели развертывания платформы:
macOS 10.10+
iOS 9.0+
ТВОС 9.0+
смотретьOS 2.0+
Чтобы добавить Mantle в ваше приложение:
Добавьте репозиторий Mantle в качестве подмодуля репозитория вашего приложения.
Запустите git submodule update --init --recursive
из папки Mantle.
Перетащите Mantle.xcodeproj
в проект Xcode вашего приложения.
На вкладке «Общие» целевого приложения добавьте Mantle.framework
в список «Встроенные двоичные файлы».
Если вместо этого вы разрабатываете Mantle самостоятельно, используйте файл Mantle.xcworkspace
.
Просто добавьте Mantle в свой Cartfile
:
github "Mantle/Mantle"
Добавьте Mantle в свой Podfile
под целью сборки, в которой они хотят его использовать:
target 'MyAppOrFramework' do pod 'Mantle' end
Затем запустите pod install
в Терминале или приложении CocoaPods.
Если вы пишете приложение, добавьте Mantle к зависимостям вашего проекта непосредственно в Xcode.
Если вы пишете пакет, для которого в качестве зависимости требуется Mantle, добавьте его в список dependencies
в манифесте Package.swift
, например:
dependencies: [ .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0")) ]
Mantle выпускается под лицензией MIT. См. LICENSE.md.
Есть вопросы? Пожалуйста, откройте тему!