Mantle facilita la escritura de una capa de modelo simple para su aplicación Cocoa o Cocoa Touch.
¿Qué hay de malo en la forma en que normalmente se escriben los objetos modelo en Objective-C?
Usemos la API de GitHub para demostración. ¿Cómo se representaría normalmente un problema de GitHub en Objective-C?
enumeración typedef: NSUInteger { GHIssueStateAbierto, GHIssueStateCerrado } GHIssueState;@interface GHIssue : NSObject <NSCoding, NSCopying>@property (no atómico, copia, solo lectura) NSURL *URL;@property (no atómico, copia, solo lectura) NSURL *HTMLURL;@property (noatómico, copia, solo lectura) NSNumber * número;@property (no atómico, asignar, solo lectura) GHIssueState state;@property (noatómico, copia, solo lectura) NSString *reporterLogin;@property (nonatomic, copia, solo lectura) NSDate *updatedAt;@property (nonatomic, fuerte, solo lectura) GHUser *assignee;@property (nonatomic, copia, solo lectura) NSDate *retrivedAt;@property (no atómico, copia) NSString *title;@property (no atómico, copia) NSString *body; - (id)initWithDictionary:(NSDictionary *)diccionario;@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 *)diccionario { self = [self init]; si (self == nil) devuelve nil; _URL = [NSURL URLWithString:diccionario[@"url"]]; _HTMLURL = [NSURL URLWithString:diccionario[@"html_url"]]; _número = diccionario[@"número"];if ([diccionario[@"estado"] isEqualToString:@"open"]) { _state = GHIssueStateOpen; } else if ([diccionario[@"estado"] isEqualToString:@"cerrado"]) { _state = GHIssueStateClosed; } _title = [diccionario[@"title"] copia]; _retrivedAt = [fecha NSDate]; _body = [diccionario[@"cuerpo"] copia]; _reporterLogin = [diccionario[@"usuario"][@"iniciar sesión"] copiar]; _assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]]; _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];return self; } - (id)initWithCoder:(NSCoder *)codificador { self = [self init]; si (self == nil) devuelve nil; _URL = [codificador decodeObjectForKey:@"URL"]; _HTMLURL = [codificador decodeObjectForKey:@"HTMLURL"]; _número = [codificador decodeObjectForKey:@"número"]; _state = [codificador decodeUnsignedIntegerForKey:@"estado"]; _title = [codificador decodeObjectForKey:@"title"]; _retrivedAt = [fecha NSDate]; _body = [codificador decodeObjectForKey:@"cuerpo"]; _reporterLogin = [codificador decodeObjectForKey:@"reporterLogin"]; _assignee = [codificador decodeObjectForKey:@"assignee"]; _updatedAt = [codificador decodeObjectForKey:@"updatedAt"];return self; } - (void)encodeWithCoder:(NSCoder *)coder {if (self.URL!= nil) [codificador encodeObject:self.URL forKey:@"URL"];if (self.HTMLURL!= nil) [codificador encodeObject:self .HTMLURL paraClave:@"HTMLURL"];if (self.number != nil) [codificador encodeObject:self.number paraClave:@"número"];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)copiaConZona:(NSZona *)zona { GHIssue *issue = [[self.class allocWithZone:zone] init]; problema->_URL = self.URL; problema->_HTMLURL = self.HTMLURL; problema->_número = self.número; problema->_estado = self.estado; problema->_reporterLogin = self.reporterLogin; problema->_assignee = self.assignee; problema->_updatedAt = self.updatedAt; problema.título = self.título; problema->_retrievedAt = [fecha NSDate]; problema.cuerpo = self.cuerpo; problema de devolución; } - (NSUInteger)hash {return self.number.hash; } - (BOOL)isEqual:(GHIssue *)problema {if (![problema isKindOfClass:GHIssue.class]) return NO;return [self.number isEqual:issue.number] && [self.title isEqual:issue.title] && [self.body esEqual:issue.body]; }@fin
¡Vaya, eso es mucho texto repetitivo para algo tan simple! E incluso entonces, hay algunos problemas que este ejemplo no aborda:
No hay forma de actualizar un GHIssue
con nuevos datos del servidor.
No hay forma de volver a convertir un GHIssue
en JSON.
GHIssueState
no debe codificarse tal cual. Si la enumeración cambia en el futuro, los archivos existentes podrían romperse.
Si la interfaz de GHIssue
cambia en el futuro, los archivos existentes podrían romperse.
Core Data resuelve muy bien ciertos problemas. Si necesita ejecutar consultas complejas a través de sus datos, manejar un gráfico de objetos enorme con muchas relaciones o admitir deshacer y rehacer, Core Data es una excelente opción.
Sin embargo, presenta un par de puntos débiles:
Todavía hay mucho texto repetitivo. Los objetos administrados reducen parte del texto estándar visto anteriormente, pero Core Data tiene mucho propio. Configurar correctamente una pila de Core Data (con un almacén persistente y un coordinador de almacén persistente) y ejecutar recuperaciones puede requerir muchas líneas de código.
Es difícil hacerlo bien. Incluso los desarrolladores experimentados pueden cometer errores al utilizar Core Data y el marco no perdona.
Si solo está intentando acceder a algunos objetos JSON, Core Data puede suponer mucho trabajo y obtener pocos beneficios.
No obstante, si ya está utilizando o desea utilizar Core Data en su aplicación, Mantle aún puede ser una capa de traducción conveniente entre la API y los objetos de su modelo administrado.
Ingrese MTLModel . Así es como se ve GHIssue
heredado de MTLModel
:
enumeración typedef: NSUInteger { GHIssueStateAbierto, GHIssueStateCerrado } GHIssueState;@interface GHIssue : MTLModel <MTLJSONSerializing>@property (noatómico, copia, solo lectura) NSURL *URL;@property (noatómico, copia, solo lectura) NSURL *HTMLURL;@property (noatómico, copia, solo lectura) NSNumber *número; @property (no atómico, asignar, solo lectura) GHIssueState state;@property (noatómico, copia, solo lectura) NSString *reporterLogin;@property (nonatomic, fuerte, solo lectura) GHUser *assignee;@property (nonatomic, copia, solo lectura) NSDate *updatedAt;@property (nonatomic, copia) NSString * título;@property (no atómico, copia) NSString *body;@property (no atómico, copia, solo lectura) NSDate *retrivedAt;@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",@"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 diccionarioTransformerWithModelClass:GHUser.class]; } + (NSValueTransformer *)updatedAtJSONTransformer {return [MTLValueTransformer transformadorUsingForwardBlock:^id(NSString *dateString, BOOL *éxito, NSError *__autoreleasing *error) {return [self.dateFormatter dateFromString:dateString]; } ReverseBlock:^id(NSDate *fecha, BOOL *éxito, NSError *__autoreleasing *error) {return [self.dateFormatter stringFromDate:date]; }]; } - (tipo de instancia)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil;// Almacena un valor que debe determinarse localmente tras la inicialización._retrievedAt = [NSDate date];return self; }@fin
Notablemente ausentes en esta versión están las implementaciones de <NSCoding>
, <NSCopying>
, -isEqual:
y -hash
. Al inspeccionar las declaraciones @property
que tiene en su subclase, MTLModel
puede proporcionar implementaciones predeterminadas para todos estos métodos.
Todos los problemas con el ejemplo original también se solucionaron:
No hay forma de actualizar un
GHIssue
con nuevos datos del servidor.
MTLModel
tiene un método extensible -mergeValuesForKeysFromModel:
que facilita especificar cómo se deben integrar los datos del nuevo modelo.
No hay forma de volver a convertir un
GHIssue
en JSON.
Aquí es donde los transformadores reversibles resultan realmente útiles. +[MTLJSONAdapter JSONDictionaryFromModel:error:]
puede transformar cualquier objeto de modelo que se ajuste a <MTLJSONSerializing>
nuevamente en un diccionario JSON. +[MTLJSONAdapter JSONArrayFromModels:error:]
es lo mismo pero convierte una matriz de objetos del modelo en una matriz JSON de diccionarios.
Si la interfaz de
GHIssue
cambia en el futuro, los archivos existentes podrían romperse.
MTLModel
guarda automáticamente la versión del objeto modelo que se utilizó para el archivo. Al desarchivar, se invocará -decodeValueForKey:withCoder:modelVersion:
si se anula, lo que le brinda un gancho conveniente para actualizar datos antiguos.
Para serializar los objetos de su modelo desde o hacia JSON, debe implementar <MTLJSONSerializing>
en su subclase MTLModel
. Esto le permite usar MTLJSONAdapter
para convertir los objetos de su modelo desde JSON y viceversa:
NSError *error = nulo; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
NSError *error = nil;NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:error de usuario:&error];
+JSONKeyPathsByPropertyKey
El diccionario devuelto por este método especifica cómo se asignan las propiedades del objeto de modelo a las claves en la representación JSON, por ejemplo:
@interface XYUser : MTLModel@property (solo lectura, no atómico, copia) NSString *name;@property (solo lectura, no atómico, fuerte) NSDate *createdAt;@property (solo lectura, no atómico, asignar, getter = isMeUser) BOOL meUser;@property ( solo lectura, no atómico, fuerte) XYHelper *helper;@end@implementation XYUser+ (NSDictionary *)JSONKeyPathsByPropertyKey {return @{@"name": @"name",@"createdAt": @"created_at"}; } - (tipo de instancia)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil; _helper = [XYHelper helperWithName:self.name creado en: self.createdAt];return self; }@fin
En este ejemplo, la clase XYUser
declara cuatro propiedades que Mantle maneja de diferentes maneras:
name
se asigna a una clave del mismo nombre en la representación JSON.
createdAt
se convierte a su equivalente en caso de serpiente.
meUser
no está serializado en JSON.
helper
se inicializa exactamente una vez después de la deserialización JSON.
Utilice -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
si la superclase de su modelo también implementa MTLJSONSerializing
para fusionar sus asignaciones.
Si desea asignar todas las propiedades de una clase de modelo a sí mismas, puede utilizar el método auxiliar +[NSDictionary mtl_identityPropertyMapWithModel:]
.
Al deserializar JSON usando +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
, las claves JSON que no corresponden a un nombre de propiedad o que no tienen una asignación explícita se ignoran:
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];
Aquí, el plan
se ignoraría ya que no coincide con un nombre de propiedad de XYUser
ni está asignado de otra manera en +JSONKeyPathsByPropertyKey
.
+JSONTransformerForKey:
Implemente este método opcional para convertir una propiedad de un tipo diferente al deserializar desde JSON.
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key { if ([key isEqualToString:@"createdAt"]) { return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName]; } return nil; }
key
es la clave que se aplica a su objeto modelo; no la clave JSON original. Tenga esto en cuenta si transforma los nombres de las claves usando +JSONKeyPathsByPropertyKey
.
Para mayor comodidad, si implementa +<key>JSONTransformer
, MTLJSONAdapter
utilizará el resultado de ese método. Por ejemplo, las fechas que comúnmente se representan como cadenas en JSON se pueden transformar en NSDate
de esta manera:
return [MTLValueTransformer transformadorUsingForwardBlock:^id(NSString *dateString, BOOL *éxito, NSError *__autoreleasing *error) {return [self.dateFormatter dateFromString:dateString]; } ReverseBlock:^id(NSDate *fecha, BOOL *éxito, NSError *__autoreleasing *error) {return [self.dateFormatter stringFromDate:date]; }]; }
Si el transformador es reversible, también se utilizará al serializar el objeto en JSON.
+classForParsingJSONDictionary:
Si está implementando un clúster de clases, implemente este método opcional para determinar qué subclase de su clase base debe usarse al deserializar un objeto desde JSON.
@interface XYMessage : MTLModel@end@interface XYTextMessage: XYMessage@property (solo lectura, no atómico, copia) NSString *body;@end@interface XYPictureMessage : XYMessage@property (solo lectura, no atómico, fuerte) NSURL *imageURL;@end@implementation XYMessage+ (Clase)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {if (JSONDictionary[@"image_url"]! = nil) {return XYPictureMessage.class; }if (JSONDictionary[@"body"]! = nil) {return XYTextMessage.class; }NSAssert(NO, @"No hay clase coincidente para el diccionario JSON '%@'.", JSONDictionary);return self; }@fin
MTLJSONAdapter
luego seleccionará la clase según el diccionario JSON que usted pase:
NSDictionary *textMessage = @{@"id": @1,@"body": @"¡Hola mundo!"};NSDictionary *pictureMessage = @{@"id": @2,@"image_url": @"http: //ejemplo.com/lolcat.gif"}; XYTextMessage *messageA = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:textMessage error:NULL]; XYPictureMessage *messageB = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:pictureMessage error:NULL];
Mantle no conserva automáticamente sus objetos por usted. Sin embargo, MTLModel
se ajusta a <NSCoding>
, por lo que los objetos del modelo se pueden archivar en el disco usando NSKeyedArchiver
.
Si necesita algo más potente o desea evitar mantener todo el modelo en la memoria a la vez, Core Data puede ser una mejor opción.
Mantle admite los siguientes objetivos de implementación de plataforma:
macOS 10.10+
iOS 9.0+
tvOS 9.0+
relojOS 2.0+
Para agregar Mantle a su aplicación:
Agregue el repositorio de Mantle como un submódulo del repositorio de su aplicación.
Ejecute git submodule update --init --recursive
desde la carpeta Mantle.
Arrastre y suelte Mantle.xcodeproj
en el proyecto Xcode de su aplicación.
En la pestaña "General" del destino de su aplicación, agregue Mantle.framework
a los "Binarios integrados".
Si, en cambio, estás desarrollando Mantle por tu cuenta, utiliza el archivo Mantle.xcworkspace
.
Simplemente agregue Mantle a su Cartfile
:
github "Mantle/Mantle"
Agregue Mantle a su Podfile
bajo el objetivo de compilación en el que quieren que se use:
target 'MyAppOrFramework' do pod 'Mantle' end
Luego ejecute una pod install
dentro de Terminal o la aplicación CocoaPods.
Si está escribiendo una aplicación, agregue Mantle a las dependencias de su proyecto directamente dentro de Xcode.
Si está escribiendo un paquete que requiere Mantle como dependencia, agréguelo a la lista de dependencies
en su manifiesto Package.swift
, por ejemplo:
dependencies: [ .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0")) ]
Mantle se lanza bajo la licencia MIT. Consulte LICENCIA.md.
¿Tiene alguna pregunta? ¡Abre un problema!