O Mantle facilita a gravação de uma camada de modelo simples para seu aplicativo Cocoa ou Cocoa Touch.
O que há de errado com a maneira como os objetos do modelo são geralmente escritos em Objective-C?
Vamos usar a API GitHub para demonstração. Como alguém normalmente representaria um problema do GitHub no Objective-C?
enumeração typedef: NSUInteger { GHIssueStateOpen, GHIssueStateClosed } GHIssueState;@interface GHIssue : NSObject <NSCoding, NSCopying>@property (nonatomic, copy, readonly) NSURL *URL;@property (nonatomic, copy, readonly) NSURL *HTMLURL;@property (nonatomic, copy, readonly) NSNumber * número;@property (não atômico, atribuído, somente leitura) GHIssueState state;@property (nonatomic, copy, readonly) NSString *reporterLogin;@property (nonatomic, copy, readonly) NSDate *updatedAt;@property (nonatomic, strong, readonly) GHUser *assignee;@property (nonatomic, copy, readonly) NSDate *recuperadoAt;@property (não atômico, cópia) NSString *title;@property (não atômico, cópia) NSString *corpo; - (id)initWithDictionary:(NSDictionary *)dictionary;@end
@implementation GHIssue+ (NSDateFormatter *)dateFormatter {NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";return dateFormatter; } - (id)initWithDictionary:(NSDictionary *)dictionary { self = [self init];if (self == nil) return nil; _URL = [NSURL URLWithString:dicionário[@"url"]]; _HTMLURL = [NSURL URLWithString:dicionário[@"html_url"]]; _número = dicionário[@"número"];if ([dicionário[@"estado"] isEqualToString:@"open"]) { _estado = GHIssueStateOpen; } else if ([dicionário[@"estado"] isEqualToString:@"fechado"]) { _state = GHIssueStateClosed; } _title = [dicionário[@"title"] cópia]; _retrievedAt = [data NSDate]; _body = [dicionário[@"body"] cópia]; _reporterLogin = [dicionário[@"usuário"][@"login"] cópia]; _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 = [codificador decodeObjectForKey:@"URL"]; _HTMLURL = [codificador decodeObjectForKey:@"HTMLURL"]; _número = [codificador decodeObjectForKey:@"número"]; _state = [codificador decodeUnsignedIntegerForKey:@"state"]; _title = [codificador decodeObjectForKey:@"title"]; _retrievedAt = [data NSDate]; _body = [codificador decodeObjectForKey:@"body"]; _reporterLogin = [codificador decodeObjectForKey:@"reporterLogin"]; _assignee = [codificador decodeObjectForKey:@"assignee"]; _updatedAt = [codificador 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) [coder encodeObject:self.number forKey:@"number"];if (self.title != nil) [codificador encodeObject:self.title forKey:@"title"];if (self.body != nil) [codificador encodeObject:self.body forKey:@"body"];if (self .reporterLogin != nil) [codificador encodeObject:self.reporterLogin forKey:@"reporterLogin"];if (self.assignee != nil) [codificador encodeObject:self.assignee forKey:@"assignee"];if (self.updatedAt != nil) [codificador encodeObject:self.updatedAt forKey:@"updatedAt"]; [codificador encodeUnsignedInteger:self.state forKey:@"state"]; } - (id)copyWithZone:(NSZone *)zona { GHIssue *issue = [[self.class allocWithZone:zone] init]; problema->_URL = self.URL; problema->_HTMLURL = self.HTMLURL; problema->_número = self.número; problema->_state = self.state; problema->_reporterLogin = self.reporterLogin; issue->_assignee = self.assignee; problema->_updatedAt = self.updatedAt; emissão.título = self.título; issue->_retrievedAt = [data NSDate]; issue.body = self.body; retornar problema; } - (NSUInteger)hash {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]; }@fim
Uau, isso é muito clichê para algo tão simples! E, mesmo assim, existem alguns problemas que este exemplo não aborda:
Não há como atualizar um GHIssue
com novos dados do servidor.
Não há como transformar um GHIssue
novamente em JSON.
GHIssueState
não deve ser codificado como está. Se a enumeração for alterada no futuro, os arquivos existentes poderão quebrar.
Se a interface do GHIssue
mudar no futuro, os arquivos existentes poderão quebrar.
Core Data resolve muito bem certos problemas. Se você precisar executar consultas complexas em seus dados, lidar com um enorme gráfico de objetos com muitos relacionamentos ou oferecer suporte para desfazer e refazer, Core Data é uma excelente opção.
No entanto, ele vem com alguns pontos problemáticos:
Ainda há muito clichê. Os objetos gerenciados reduzem alguns dos padrões vistos acima, mas o Core Data tem muitos deles. Configurar corretamente uma pilha Core Data (com um armazenamento persistente e um coordenador de armazenamento persistente) e executar buscas pode exigir muitas linhas de código.
É difícil acertar. Mesmo desenvolvedores experientes podem cometer erros ao usar Core Data, e a estrutura não perdoa.
Se você está apenas tentando acessar alguns objetos JSON, Core Data pode ser muito trabalhoso e com pouco ganho.
No entanto, se você já estiver usando ou quiser usar Core Data em seu aplicativo, Mantle ainda pode ser uma camada de tradução conveniente entre a API e seus objetos de modelo gerenciado.
Digite MTLModel . Esta é a aparência GHIssue
herdado do MTLModel
:
enumeração typedef: NSUInteger { GHIssueStateOpen, GHIssueStateClosed } GHIssueState;@interface GHIssue : MTLModel <MTLJSONSerializing>@property (nonatomic, copy, readonly) NSURL *URL;@property (nonatomic, copy, readonly) NSURL *HTMLURL;@property (nonatomic, copy, readonly) NSNumber *number; @property (não atômico, atribuído, somente leitura) GHIssueState state;@property (nonatomic, copy, readonly) NSString *reporterLogin;@property (nonatomic, strong, readonly) GHUser *assignee;@property (nonatomic, copy, readonly) NSDate *updatedAt;@property (nonatomic, copy) NSString * título;@property (não atômico, cópia) NSString *corpo;@property (não atômico, cópia, somente leitura) NSDate *recuperadoAt;@end
@implementation GHIssue+ (NSDateFormatter *)dateFormatter {NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";return dateFormatter; } + (NSDictionary *)JSONKeyPathsByPropertyKey {return @{@"URL": @"url",@"HTMLURL": @"html_url",@"número": @"número",@"estado": @"estado", @"reporterLogin": @"user.login",@"assignee": @"assignee",@"updatedAt": @"updated_at"}; } + (NSValueTransformer *)URLJSONTransformer {retorna [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)HTMLURLJSONTransformer {retorna [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)stateJSONTransformer {return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{@"open": @(GHIssueStateOpen),@"closed": @(GHIssueStateClosed) }]; } + (NSValueTransformer *)assigneeJSONTransformer {retornar [MTLJSONAdapter dicionárioTransformerWithModelClass:GHUser.class]; } + (NSValueTransformer *) atualizadoAtJSONTransformer {return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *sucesso, NSError *__autoreleasing *erro) {return [self.dateFormatter dateFromString:dateString]; } ReverseBlock:^id(NSDate *data, BOOL *sucesso, NSError *__autoreleasing *erro) {return [self.dateFormatter stringFromDate:date]; }]; } - (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue erro:(NSError **)erro { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil;// Armazena um valor que precisa ser determinado localmente na inicialização._retrievedAt = [NSDate date];return self; }@fim
Notavelmente ausentes nesta versão estão as implementações de <NSCoding>
, <NSCopying>
, -isEqual:
e -hash
. Ao inspecionar as declarações @property
que você possui em sua subclasse, MTLModel
pode fornecer implementações padrão para todos esses métodos.
Todos os problemas com o exemplo original também foram corrigidos:
Não há como atualizar um
GHIssue
com novos dados do servidor.
MTLModel
possui um método -mergeValuesForKeysFromModel:
extensível, que facilita a especificação de como os novos dados do modelo devem ser integrados.
Não há como transformar um
GHIssue
novamente em JSON.
É aqui que os transformadores reversíveis são realmente úteis. +[MTLJSONAdapter JSONDictionaryFromModel:error:]
pode transformar qualquer objeto de modelo em conformidade com <MTLJSONSerializing>
de volta em um dicionário JSON. +[MTLJSONAdapter JSONArrayFromModels:error:]
é o mesmo, mas transforma uma matriz de objetos de modelo em uma matriz JSON de dicionários.
Se a interface do
GHIssue
mudar no futuro, os arquivos existentes poderão quebrar.
MTLModel
salva automaticamente a versão do objeto de modelo que foi usado para arquivamento. Ao desarquivar, -decodeValueForKey:withCoder:modelVersion:
será invocado se for substituído, fornecendo um gancho conveniente para atualizar dados antigos.
Para serializar seus objetos de modelo de ou para JSON, você precisa implementar <MTLJSONSerializing>
em sua subclasse MTLModel
. Isso permite que você use MTLJSONAdapter
para converter seus objetos de modelo de JSON e vice-versa:
NSError *erro = nulo; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary erro:&error];
NSError *error = nil;NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:erro do usuário:&error];
+JSONKeyPathsByPropertyKey
O dicionário retornado por esse método especifica como as propriedades do objeto modelo são mapeadas para as chaves na representação JSON, por exemplo:
@interface XYUser: MTLModel@property (somente leitura, não atômico, cópia) NSString *nome;@property (somente leitura, não atômico, forte) NSDate *createdAt;@property (somente leitura, não atômico, atribuir, getter = isMeUser) BOOL meUser;@property ( somente leitura, não atômico, forte) XYHelper *helper;@end@implementação XYUser+ (NSDictionary *)JSONKeyPathsByPropertyKey {return @{@"nome": @"nome",@"createdAt": @"created_at"}; } - (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue erro:(NSError **)erro { self = [super initWithDictionary:dictionaryValue erro:error];if (self == nil) return nil; _helper = [XYHelper helperWithName:self.name criadoAt:self.createdAt];return self; }@fim
Neste exemplo, a classe XYUser
declara quatro propriedades que Mantle trata de maneiras diferentes:
name
é mapeado para uma chave com o mesmo nome na representação JSON.
createdAt
é convertido em seu equivalente em caixa de cobra.
meUser
não é serializado em JSON.
helper
é inicializado exatamente uma vez após a desserialização do JSON.
Use -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
se a superclasse do seu modelo também implementar MTLJSONSerializing
para mesclar seus mapeamentos.
Se quiser mapear todas as propriedades de uma classe Model para si mesmas, você pode usar o método auxiliar +[NSDictionary mtl_identityPropertyMapWithModel:]
.
Ao desserializar JSON usando +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
, as chaves JSON que não correspondem a um nome de propriedade ou têm um mapeamento explícito são ignoradas:
NSDictionary *JSONDictionary = @{@"nome": @"john",@"created_at": @"2013/07/02 16:40:00 +0000",@"plan": @"lite"}; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary erro:&error];
Aqui, o plan
seria ignorado, pois não corresponde a um nome de propriedade XYUser
nem é mapeado de outra forma em +JSONKeyPathsByPropertyKey
.
+JSONTransformerForKey:
Implemente este método opcional para converter uma propriedade de um tipo diferente ao desserializar de JSON.
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key { if ([key isEqualToString:@"createdAt"]) { return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName]; } return nil; }
key
é a chave que se aplica ao seu objeto de modelo; não a chave JSON original. Tenha isso em mente se você transformar os nomes das chaves usando +JSONKeyPathsByPropertyKey
.
Para maior comodidade, se você implementar +<key>JSONTransformer
, MTLJSONAdapter
usará o resultado desse método. Por exemplo, datas que são comumente representadas como strings em JSON podem ser transformadas em NSDate
s da seguinte forma:
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *sucesso, NSError *__autoreleasing *erro) {return [self.dateFormatter dateFromString:dateString]; } ReverseBlock:^id(NSDate *data, BOOL *sucesso, NSError *__autoreleasing *erro) {return [self.dateFormatter stringFromDate:date]; }]; }
Se o transformador for reversível, ele também será utilizado na serialização do objeto em JSON.
+classForParsingJSONDictionary:
Se você estiver implementando um cluster de classe, implemente este método opcional para determinar qual subclasse da sua classe base deve ser usada ao desserializar um objeto do JSON.
@interface XYMessage: MTLModel@end@interface XYTextMessage: XYMessage@property (somente leitura, não atômico, cópia) NSString *body;@end@interface XYPictureMessage: XYMessage@property (somente leitura, não atômico, forte) NSURL *imageURL;@end@implementação XYMessage+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {if (JSONDictionary[@"image_url"] != nil) {return XYPictureMessage.class; }if (JSONDictionary[@"body"] != nil) {return XYTextMessage.class; }NSassert(NO, @"Nenhuma classe correspondente para o dicionário JSON '%@'.", JSONDictionary);return self; }@fim
MTLJSONAdapter
escolherá a classe com base no dicionário JSON que você passar:
NSDictionary *textMessage = @{@"id": @1,@"body": @"Olá mundo!"};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 não persiste automaticamente seus objetos para você. No entanto, MTLModel
está em conformidade com <NSCoding>
, portanto, os objetos do modelo podem ser arquivados em disco usando NSKeyedArchiver
.
Se você precisar de algo mais poderoso ou quiser evitar manter todo o seu modelo na memória de uma só vez, o Core Data pode ser uma escolha melhor.
Mantle oferece suporte aos seguintes alvos de implantação de plataforma:
macOS 10.10+
iOS 9.0+
tvOS 9.0+
watchOS 2.0+
Para adicionar Mantle ao seu aplicativo:
Adicione o repositório Mantle como um submódulo do repositório do seu aplicativo.
Execute git submodule update --init --recursive
de dentro da pasta Mantle.
Arraste e solte Mantle.xcodeproj
no projeto Xcode do seu aplicativo.
Na guia "Geral" do destino do seu aplicativo, adicione Mantle.framework
aos "Binários incorporados".
Se você estiver desenvolvendo o Mantle sozinho, use o arquivo Mantle.xcworkspace
.
Basta adicionar Mantle ao seu Cartfile
:
github "Mantle/Mantle"
Adicione Mantle ao seu Podfile
no destino de construção em que eles desejam usá-lo:
target 'MyAppOrFramework' do pod 'Mantle' end
Em seguida, execute uma pod install
no Terminal ou no aplicativo CocoaPods.
Se você estiver escrevendo um aplicativo, adicione Mantle às dependências do seu projeto diretamente no Xcode.
Se você estiver escrevendo um pacote que requer Mantle como dependência, adicione-o à lista de dependencies
em seu manifesto Package.swift
, por exemplo:
dependencies: [ .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0")) ]
Mantle é lançado sob a licença do MIT. Consulte LICENSE.md.
Tem alguma pergunta? Por favor, abra um problema!