Mantle facilite l'écriture d'un calque de modèle simple pour votre application Cocoa ou Cocoa Touch.
Quel est le problème avec la façon dont les objets modèles sont généralement écrits en Objective-C ?
Utilisons l'API GitHub pour la démonstration. Comment représenterait-on généralement un problème GitHub en Objective-C ?
énumération typedef : NSUInteger { GHIssueStateOpen, GHIssueStateClosed } GHIssueState;@interface GHIssue : NSObject <NSCoding, NSCopying>@property (non atomique, copie, lecture seule) NSURL *URL;@property (non atomique, copie, lecture seule) NSURL *HTMLURL;@property (non atomique, copie, lecture seule) NSNumber * number;@property (nonatomique, assignation, lecture seule) GHIssueState state;@property (non atomique, copie, lecture seule) NSString *reporterLogin;@property (non atomique, copie, lecture seule) NSDate *updatedAt;@property (non atomique, fort, lecture seule) GHUser *assignee;@property (non atomique, copie, lecture seule) NSDate *retrievedAt; @property (non atomique, copie) NSString *title;@property (non atomique, copie) NSString *corps ; - (id)initWithDictionary:(NSDictionary *)dictionnaire;@end
@implementation GHIssue+ (NSDateFormatter *)dateFormatter {NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.dateFormat = @"aaaa-MM-jj'T'HH:mm:ss'Z'";return dateFormatter; } - (id)initWithDictionary:(NSDictionary *)dictionnaire { self = [self init];if (self == nil) return nil; _URL = [NSURL URLWithString:dictionary[@"url"]]; _HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]]; _number = dictionnaire[@"numéro"];if ([dictionnaire[@"state"] isEqualToString:@"open"]) { _state = GHIssueStateOpen ; } sinon if ([dictionary[@"state"] isEqualToString:@"closed"]) { _state = GHIssueStateClosed ; } _title = [copie du dictionnaire[@"title"]]; _retrievedAt = [date NSDate] ; _body = [dictionnaire[@"body"] copie]; _reporterLogin = [dictionnaire[@"user"][@"login"] copie]; _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 = [codeur decodeObjectForKey:@"URL"]; _HTMLURL = [codeur decodeObjectForKey:@"HTMLURL"]; _numéro = [codeur decodeObjectForKey:@"numéro"]; _state = [codeur decodeUnsignedIntegerForKey:@"state"]; _title = [codeur decodeObjectForKey:@"title"]; _retrievedAt = [date NSDate] ; _body = [codeur decodeObjectForKey:@"body"]; _reporterLogin = [codeur decodeObjectForKey:@"reporterLogin"]; _assignee = [codeur decodeObjectForKey:@"assignee"]; _updatedAt = [coder 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) [codeur encodeObject:self.title forKey:@"title"];if (self.body != nil) [codeur encodeObject:self.body forKey:@"body"];if (self .reporterLogin != nil) [codeur encodeObject:self.reporterLogin forKey:@"reporterLogin"];if (self.assignee != nil) [codeur encodeObject:self.assignee forKey:@"assignee"];if (self.updatedAt != nil) [codeur encodeObject:self.updatedAt forKey:@"updatedAt"]; [codeur encodeUnsignedInteger:self.state forKey:@"state"]; } - (id)copyWithZone:(NSZone *)zone { GHIssue *issue = [[self.class allocWithZone:zone] init]; problème->_URL = self.URL; problème->_HTMLURL = self.HTMLURL; problème->_number = self.number; problème->_state = self.state; problème->_reporterLogin = self.reporterLogin ; problème->_assignee = self.assignee; problème->_updatedAt = self.updatedAt ; problème.title = self.title; issue->_retrievedAt = [date NSDate] ; issue.body = self.body;return issue; } - (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]; }@fin
Ouf, c'est beaucoup de passe-partout pour quelque chose d'aussi simple ! Et même dans ce cas, il existe certains problèmes que cet exemple ne résout pas :
Il n'y a aucun moyen de mettre à jour un GHIssue
avec de nouvelles données du serveur.
Il n'y a aucun moyen de reconvertir un GHIssue
en JSON.
GHIssueState
ne doit pas être codé tel quel. Si l'énumération change à l'avenir, les archives existantes pourraient être brisées.
Si l'interface de GHIssue
change ultérieurement, les archives existantes risquent de se briser.
Core Data résout très bien certains problèmes. Si vous devez exécuter des requêtes complexes sur vos données, gérer un énorme graphique d'objets avec de nombreuses relations ou prendre en charge l'annulation et le rétablissement, Core Data est une excellente solution.
Cela s’accompagne cependant de quelques problèmes :
Il y a encore beaucoup de passe-partout. Les objets gérés réduisent une partie du passe-partout vu ci-dessus, mais Core Data en a beaucoup. La configuration correcte d'une pile Core Data (avec un magasin persistant et un coordinateur de magasin persistant) et l'exécution des extractions peuvent nécessiter de nombreuses lignes de code.
C'est difficile de réussir. Même les développeurs expérimentés peuvent commettre des erreurs lors de l’utilisation de Core Data, et le framework ne pardonne pas.
Si vous essayez simplement d'accéder à certains objets JSON, Core Data peut représenter beaucoup de travail pour peu de gain.
Néanmoins, si vous utilisez ou souhaitez déjà utiliser Core Data dans votre application, Mantle peut toujours constituer une couche de traduction pratique entre l'API et vos objets de modèle gérés.
Entrez MTLModel . Voici à quoi ressemble GHIssue
héritant de MTLModel
:
énumération typedef : NSUInteger { GHIssueStateOpen, GHIssueStateClosed } GHIssueState;@interface GHIssue : MTLModel <MTLJSONSerializing>@property (non atomique, copie, lecture seule) NSURL *URL;@property (non atomique, copie, lecture seule) NSURL *HTMLURL;@property (non atomique, copie, lecture seule) NSNumber *number; @property (non atomique, assignation, lecture seule) État GHIssueState ; @property (non atomique, copie, lecture seule) NSString *reporterLogin;@property (non atomique, fort, lecture seule) GHUser *assignee;@property (non atomique, copie, lecture seule) NSDate *updatedAt;@property (non atomique, copie) NSString *title;@property (non atomique, copie) NSString *body;@property (non atomique, copie, lecture seule) NSDate *récupéré à;@end
@implementation GHIssue+ (NSDateFormatter *)dateFormatter {NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.dateFormat = @"aaaa-MM-jj'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 [MTLJSONAdapterdictionaryTransformerWithModelClass:GHUser.class]; } + (NSValueTransformer *)updatedAtJSONTransformer {return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter dateFromString:dateString]; } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter stringFromDate:date]; }]; } - (type d'instance) initWithDictionary : (NSDictionary *) erreur dictionnaireValue : (NSError **) erreur { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil;// Stocke une valeur qui doit être déterminée localement lors de l'initialisation._retrievedAt = [NSDate date];return self; }@fin
Les implémentations de <NSCoding>
, <NSCopying>
, -isEqual:
et -hash
. En inspectant les déclarations @property
que vous avez dans votre sous-classe, MTLModel
peut fournir des implémentations par défaut pour toutes ces méthodes.
Les problèmes avec l'exemple d'origine ont également été résolus :
Il n'y a aucun moyen de mettre à jour un
GHIssue
avec de nouvelles données du serveur.
MTLModel
possède une méthode extensible -mergeValuesForKeysFromModel:
qui permet de spécifier facilement comment les nouvelles données de modèle doivent être intégrées.
Il n'y a aucun moyen de reconvertir un
GHIssue
en JSON.
C’est là que les transformateurs réversibles s’avèrent vraiment utiles. +[MTLJSONAdapter JSONDictionaryFromModel:error:]
peut transformer n'importe quel objet de modèle conforme à <MTLJSONSerializing>
en un dictionnaire JSON. +[MTLJSONAdapter JSONArrayFromModels:error:]
est identique mais transforme un tableau d'objets de modèle en un tableau JSON de dictionnaires.
Si l'interface de
GHIssue
change ultérieurement, les archives existantes risquent de se briser.
MTLModel
enregistre automatiquement la version de l'objet modèle qui a été utilisée pour l'archivage. Lors du désarchivage, -decodeValueForKey:withCoder:modelVersion:
sera invoqué en cas de substitution, vous offrant ainsi un point d'ancrage pratique pour mettre à niveau les anciennes données.
Afin de sérialiser vos objets de modèle depuis ou vers JSON, vous devez implémenter <MTLJSONSerializing>
dans votre sous-classe MTLModel
. Cela vous permet d'utiliser MTLJSONAdapter
pour convertir vos objets de modèle depuis JSON et inversement :
NSError *erreur = zéro ; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
NSError *error = nil;NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:erreur utilisateur:&erreur];
+JSONKeyPathsByPropertyKey
Le dictionnaire renvoyé par cette méthode spécifie comment les propriétés de votre objet modèle sont mappées aux clés de la représentation JSON, par exemple :
@interface XYUser : MTLModel@property (lecture seule, non atomique, copie) NSString *name;@property (lecture seule, non atomique, fort) NSDate *createdAt;@property (lecture seule, non atomique, assign, getter = isMeUser) BOOL meUser;@property ( lecture seule, non atomique, fort) XYHelper *helper;@end@implementation XYUser+ (NSDictionary *)JSONKeyPathsByPropertyKey {return @{@"name": @"name",@"createdAt": @"created_at"}; } - (type d'instance) initWithDictionary : (NSDictionary *) erreur dictionnaireValue : (NSError **) erreur { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil; _helper = [XYHelper helperWithName:self.name createAt:self.createdAt];return self; }@fin
Dans cet exemple, la classe XYUser
déclare quatre propriétés que Mantle gère de différentes manières :
name
est mappé à une clé du même nom dans la représentation JSON.
createdAt
est converti en son équivalent en forme de serpent.
meUser
n'est pas sérialisé en JSON.
helper
est initialisé exactement une fois après la désérialisation JSON.
Utilisez -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
si la superclasse de votre modèle implémente également MTLJSONSerializing
pour fusionner leurs mappages.
Si vous souhaitez mapper toutes les propriétés d'une classe Model sur elles-mêmes, vous pouvez utiliser la méthode d'assistance +[NSDictionary mtl_identityPropertyMapWithModel:]
.
Lors de la désérialisation de JSON à l'aide de +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
, les clés JSON qui ne correspondent pas à un nom de propriété ou qui n'ont pas de mappage explicite sont ignorées :
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];
Ici, le plan
serait ignoré car il ne correspond pas à un nom de propriété de XYUser
et n'est pas non plus mappé dans +JSONKeyPathsByPropertyKey
.
+JSONTransformerForKey:
Implémentez cette méthode facultative pour convertir une propriété d'un type différent lors de la désérialisation à partir de JSON.
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key { if ([key isEqualToString:@"createdAt"]) { return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName]; } return nil; }
key
est la clé qui s'applique à votre objet modèle ; pas la clé JSON d'origine. Gardez cela à l'esprit si vous transformez les noms de clés à l'aide de +JSONKeyPathsByPropertyKey
.
Pour plus de commodité, si vous implémentez +<key>JSONTransformer
, MTLJSONAdapter
utilisera plutôt le résultat de cette méthode. Par exemple, les dates qui sont généralement représentées sous forme de chaînes dans JSON peuvent être transformées en NSDate
comme ceci :
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter dateFromString:dateString]; } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter stringFromDate:date]; }]; }
Si le transformateur est réversible, il sera également utilisé lors de la sérialisation de l'objet en JSON.
+classForParsingJSONDictionary:
Si vous implémentez un cluster de classes, implémentez cette méthode facultative pour déterminer quelle sous-classe de votre classe de base doit être utilisée lors de la désérialisation d'un objet à partir de JSON.
@interface XYMessage : MTLModel@end@interface XYTextMessage : XYMessage@property (lecture seule, non atomique, copie) NSString *body;@end@interface XYPictureMessage : XYMessage@property (lecture seule, non atomique, fort) NSURL *imageURL;@end@implementation XYMessage+ (Classe)classForParsingJSONDictionary :(NSDictionary *)JSONDictionary {if (JSONDictionary[@"image_url"] != nil) {return XYPictureMessage.class; }if (JSONDictionary[@"body"] != nil) {return XYTextMessage.class; }NSAssert(NO, @"Aucune classe correspondante pour le dictionnaire JSON '%@'.", JSONDictionary);return self; }@fin
MTLJSONAdapter
sélectionnera ensuite la classe en fonction du dictionnaire JSON que vous transmettez :
NSDictionary *textMessage = @{@"id": @1,@"body": @"Hello World!"};NSDictionary *pictureMessage = @{@"id": @2,@"image_url": @"http: //exemple.com/lolcat.gif"} ; XYTextMessage *messageA = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:textMessage error:NULL]; XYPictureMessage *messageB = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:pictureMessage error:NULL];
Mantle ne conserve pas automatiquement vos objets pour vous. Cependant, MTLModel
est conforme à <NSCoding>
, de sorte que les objets de modèle peuvent être archivés sur le disque à l'aide de NSKeyedArchiver
.
Si vous avez besoin de quelque chose de plus puissant ou si vous souhaitez éviter de conserver l'intégralité de votre modèle en mémoire à la fois, Core Data peut être un meilleur choix.
Mantle prend en charge les cibles de déploiement de plateforme suivantes :
macOS 10.10+
iOS 9.0+
tvOS 9.0+
montreOS 2.0+
Pour ajouter Mantle à votre application :
Ajoutez le référentiel Mantle en tant que sous-module du référentiel de votre application.
Exécutez git submodule update --init --recursive
depuis le dossier Mantle.
Faites glisser et déposez Mantle.xcodeproj
dans le projet Xcode de votre application.
Dans l'onglet "Général" de votre cible d'application, ajoutez Mantle.framework
aux "Binaires intégrés".
Si vous développez Mantle seul, utilisez le fichier Mantle.xcworkspace
.
Ajoutez simplement Mantle à votre Cartfile
:
github "Mantle/Mantle"
Ajoutez Mantle à votre Podfile
sous la cible de build dans laquelle ils souhaitent qu'il soit utilisé :
target 'MyAppOrFramework' do pod 'Mantle' end
Exécutez ensuite une pod install
dans Terminal ou dans l'application CocoaPods.
Si vous écrivez une application, ajoutez Mantle aux dépendances de votre projet directement dans Xcode.
Si vous écrivez un package qui nécessite Mantle comme dépendance, ajoutez-le à la liste dependencies
dans son manifeste Package.swift
, par exemple :
dependencies: [ .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0")) ]
Mantle est publié sous licence MIT. Voir LICENSE.md.
Vous avez une question ? Veuillez ouvrir un problème !