Mit Mantle können Sie ganz einfach eine einfache Modellebene für Ihre Cocoa- oder Cocoa Touch-Anwendung schreiben.
Was ist falsch an der Art und Weise, wie Modellobjekte normalerweise in Objective-C geschrieben werden?
Lassen Sie uns zur Demonstration die GitHub-API verwenden. Wie würde man normalerweise ein GitHub-Problem in Objective-C darstellen?
typedef enum: 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 * number;@property (nichtatomar, zuweisen, schreibgeschützt) 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 *retrievedAt;@property (nonatomic, copy) NSString *title;@property (nichtatomar, kopieren) 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 = @"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:dictionary[@"url"]]; _HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]]; _number = dictionary[@"number"];if ([dictionary[@"state"] isEqualToString:@"open"]) { _state = GHIssueStateOpen; } else if ([dictionary[@"state"] isEqualToString:@"closed"]) { _state = GHIssueStateClosed; } _title = [dictionary[@"title"] copy]; _retrievedAt = [NSDate-Datum]; _body = [dictionary[@"body"] copy]; _reporterLogin = [dictionary[@"user"][@"login"] copy]; _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 = [coder decodeObjectForKey:@"URL"]; _HTMLURL = [coder decodeObjectForKey:@"HTMLURL"]; _number = [coder decodeObjectForKey:@"number"]; _state = [coder decodeUnsignedIntegerForKey:@"state"]; _title = [coder decodeObjectForKey:@"title"]; _retrievedAt = [NSDate-Datum]; _body = [coder decodeObjectForKey:@"body"]; _reporterLogin = [coder decodeObjectForKey:@"reporterLogin"]; _assignee = [coder 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) [coder encodeObject:self.title forKey:@"title"];if (self.body != nil) [coder encodeObject:self.body forKey: @"body"];if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@"reporterLogin"];if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@"assignee"];if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"]; [coder encodeUnsignedInteger:self.state forKey:@"state"]; } - (id)copyWithZone:(NSZone *)zone { GHIssue *issue = [[self.class allocWithZone:zone] init]; Issue->_URL = self.URL; issue->_HTMLURL = self.HTMLURL; Issue->_number = self.number; issue->_state = self.state; issue->_reporterLogin = self.reporterLogin; Issue->_assignee = self.assignee; issue->_updatedAt = self.updatedAt; issue.title = self.title; issue->_retrievedAt = [NSDate date]; 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]; }@Ende
Puh, das ist eine Menge Standard für etwas so Einfaches! Und selbst dann gibt es einige Probleme, die in diesem Beispiel nicht behandelt werden:
Es gibt keine Möglichkeit, ein GHIssue
mit neuen Daten vom Server zu aktualisieren.
Es gibt keine Möglichkeit, ein GHIssue
wieder in JSON umzuwandeln.
GHIssueState
sollte nicht unverändert codiert werden. Wenn sich die Enumeration in Zukunft ändert, könnten bestehende Archive kaputt gehen.
Wenn sich die Schnittstelle von GHIssue
im Laufe der Zeit ändert, könnten bestehende Archive kaputt gehen.
Core Data löst bestimmte Probleme sehr gut. Wenn Sie komplexe Abfragen für Ihre Daten ausführen, ein großes Objektdiagramm mit vielen Beziehungen verwalten oder Rückgängigmachen und Wiederherstellen unterstützen müssen, ist Core Data eine hervorragende Lösung.
Es bringt jedoch einige Schwachstellen mit sich:
Es gibt noch viel Boilerplate. Verwaltete Objekte reduzieren einige der oben genannten Standardwerte, aber Core Data verfügt über zahlreiche eigene. Das korrekte Einrichten eines Kerndatenstapels (mit einem persistenten Speicher und einem persistenten Speicherkoordinator) und das Ausführen von Abrufen kann viele Codezeilen erfordern.
Es ist schwer, es richtig zu machen. Selbst erfahrene Entwickler können bei der Verwendung von Core Data Fehler machen, und das Framework verzeiht diese nicht.
Wenn Sie nur versuchen, auf einige JSON-Objekte zuzugreifen, kann Core Data eine Menge Arbeit für wenig Gewinn bedeuten.
Wenn Sie jedoch bereits Core Data in Ihrer App verwenden oder verwenden möchten, kann Mantle dennoch eine praktische Übersetzungsebene zwischen der API und Ihren verwalteten Modellobjekten sein.
Geben Sie MTLModel ein. So sieht GHIssue
aus, wenn es von MTLModel
erbt:
typedef enum: 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 (nichtatomar, zuweisen, schreibgeschützt) GHIssueState state;@property (nonatomic, copy, readonly) NSString *reporterLogin;@property (nonatomic, strong, readonly) GHUser *assignee;@property (nonatomic, copy, readonly) NSDate *updatedAt;@property (nonatomic, copy) NSString * title;@property (nonatomic, copy) NSString *body;@property (nonatomic, copy, schreibgeschützt) NSDate *retrievedAt;@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 dictionaryTransformerWithModelClass: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]; }]; } - (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil;// Einen Wert speichern, der bei der Initialisierung lokal ermittelt werden muss._retrievedAt = [NSDate date];return self; }@Ende
In dieser Version fehlen insbesondere Implementierungen von <NSCoding>
, <NSCopying>
, -isEqual:
und -hash
. Durch die Überprüfung der @property
Deklarationen in Ihrer Unterklasse kann MTLModel
Standardimplementierungen für alle diese Methoden bereitstellen.
Die Probleme mit dem Originalbeispiel wurden ebenfalls alle behoben:
Es gibt keine Möglichkeit, ein
GHIssue
mit neuen Daten vom Server zu aktualisieren.
MTLModel
verfügt über eine erweiterbare Methode -mergeValuesForKeysFromModel:
mit der Sie einfach angeben können, wie neue Modelldaten integriert werden sollen.
Es gibt keine Möglichkeit, ein
GHIssue
wieder in JSON umzuwandeln.
Hier kommen Umkehrtransformatoren zum Einsatz. +[MTLJSONAdapter JSONDictionaryFromModel:error:]
kann jedes Modellobjekt, das <MTLJSONSerializing>
entspricht, zurück in ein JSON-Wörterbuch umwandeln. +[MTLJSONAdapter JSONArrayFromModels:error:]
ist dasselbe, wandelt jedoch ein Array von Modellobjekten in ein JSON-Array von Wörterbüchern um.
Wenn sich die Schnittstelle von
GHIssue
im Laufe der Zeit ändert, könnten bestehende Archive kaputt gehen.
MTLModel
speichert automatisch die Version des Modellobjekts, die zur Archivierung verwendet wurde. Beim Dearchivieren wird -decodeValueForKey:withCoder:modelVersion:
aufgerufen, wenn es überschrieben wird, was Ihnen einen praktischen Haken zum Aktualisieren alter Daten bietet.
Um Ihre Modellobjekte von oder in JSON zu serialisieren, müssen Sie <MTLJSONSerializing>
in Ihrer MTLModel
Unterklasse implementieren. Dadurch können Sie MTLJSONAdapter
verwenden, um Ihre Modellobjekte von JSON und zurück zu konvertieren:
NSError *error = nil; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
NSError *error = nil;NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user error:&error];
+JSONKeyPathsByPropertyKey
Das von dieser Methode zurückgegebene Wörterbuch gibt an, wie die Eigenschaften Ihres Modellobjekts den Schlüsseln in der JSON-Darstellung zugeordnet werden, zum Beispiel:
@interface XYUser : MTLModel@property (schreibgeschützt, nichtatomar, kopieren) NSString *name;@property (schreibgeschützt, nichtatomar, stark) NSDate *createdAt;@property (schreibgeschützt, nichtatomar, zuweisen, getter = isMeUser) BOOL meUser;@property ( schreibgeschützt, nichtatomar, stark) 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];return self; }@Ende
In diesem Beispiel deklariert die XYUser
-Klasse vier Eigenschaften, die Mantle auf unterschiedliche Weise verarbeitet:
name
wird in der JSON-Darstellung einem Schlüssel mit demselben Namen zugeordnet.
createdAt
wird in das entsprechende Snake-Case-Äquivalent konvertiert.
meUser
wird nicht in JSON serialisiert.
helper
wird genau einmal nach der JSON-Deserialisierung initialisiert.
Verwenden Sie -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
wenn die Superklasse Ihres Modells auch MTLJSONSerializing
implementiert, um ihre Zuordnungen zusammenzuführen.
Wenn Sie alle Eigenschaften einer Model-Klasse sich selbst zuordnen möchten, können Sie die Hilfsmethode +[NSDictionary mtl_identityPropertyMapWithModel:]
verwenden.
Beim Deserialisieren von JSON mit +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
werden JSON-Schlüssel, die keinem Eigenschaftsnamen entsprechen oder keine explizite Zuordnung haben, ignoriert:
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];
Hier würde der plan
ignoriert, da er weder mit einem Eigenschaftsnamen von XYUser
übereinstimmt noch anderweitig in +JSONKeyPathsByPropertyKey
zugeordnet ist.
+JSONTransformerForKey:
Implementieren Sie diese optionale Methode, um bei der Deserialisierung von JSON eine Eigenschaft von einem anderen Typ zu konvertieren.
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key { if ([key isEqualToString:@"createdAt"]) { return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName]; } return nil; }
key
ist der Schlüssel, der für Ihr Modellobjekt gilt; nicht der ursprüngliche JSON-Schlüssel. Beachten Sie dies, wenn Sie die Schlüsselnamen mit +JSONKeyPathsByPropertyKey
umwandeln.
Wenn Sie +<key>JSONTransformer
implementieren, verwendet MTLJSONAdapter
für zusätzlichen Komfort stattdessen das Ergebnis dieser Methode. Beispielsweise können Datumsangaben, die in JSON üblicherweise als Zeichenfolgen dargestellt werden, wie folgt in NSDate
s umgewandelt werden:
return [MTLValueTransformer transformatorUsingForwardBlock:^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]; }]; }
Wenn der Transformator umkehrbar ist, wird er auch beim Serialisieren des Objekts in JSON verwendet.
+classForParsingJSONDictionary:
Wenn Sie einen Klassencluster implementieren, implementieren Sie diese optionale Methode, um zu bestimmen, welche Unterklasse Ihrer Basisklasse beim Deserialisieren eines Objekts aus JSON verwendet werden soll.
@interface XYMessage: MTLModel@end@interface XYTextMessage: XYMessage@property (schreibgeschützt, nichtatomar, kopieren) NSString *body;@end@interface XYPictureMessage: XYMessage@property (schreibgeschützt, nichtatomar, stark) NSURL *imageURL;@end@implementation XYMessage+ (Klasse)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {if (JSONDictionary[@"image_url"] != nil) {return XYPictureMessage.class; }if (JSONDictionary[@"body"] != nil) {return XYTextMessage.class; }NSAssert(NO, @"Keine passende Klasse für das JSON-Wörterbuch '%@'.", JSONDictionary);return self; }@Ende
MTLJSONAdapter
wählt dann die Klasse basierend auf dem von Ihnen übergebenen JSON-Wörterbuch aus:
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 speichert Ihre Objekte nicht automatisch für Sie. MTLModel
entspricht jedoch <NSCoding>
, sodass Modellobjekte mit NSKeyedArchiver
auf der Festplatte archiviert werden können.
Wenn Sie etwas Leistungsstärkeres benötigen oder vermeiden möchten, dass Ihr gesamtes Modell auf einmal im Speicher bleibt, ist Core Data möglicherweise die bessere Wahl.
Mantle unterstützt die folgenden Plattformbereitstellungsziele:
macOS 10.10+
iOS 9.0+
tvOS 9.0+
watchOS 2.0+
So fügen Sie Mantle zu Ihrer Anwendung hinzu:
Fügen Sie das Mantle-Repository als Submodul des Repositorys Ihrer Anwendung hinzu.
Führen Sie git submodule update --init --recursive
aus dem Mantle-Ordner aus.
Ziehen Sie Mantle.xcodeproj
per Drag & Drop in das Xcode-Projekt Ihrer Anwendung.
Fügen Sie auf der Registerkarte „Allgemein“ Ihres Anwendungsziels Mantle.framework
zu den „Embedded Binaries“ hinzu.
Wenn Sie Mantle stattdessen alleine entwickeln, verwenden Sie die Datei Mantle.xcworkspace
.
Fügen Sie Mantle einfach zu Ihrer Cartfile
hinzu:
github "Mantle/Mantle"
Fügen Sie Mantle zu Ihrer Podfile
unter dem Build-Ziel hinzu, in dem es verwendet werden soll:
target 'MyAppOrFramework' do pod 'Mantle' end
Führen Sie dann eine pod install
im Terminal oder in der CocoaPods-App durch.
Wenn Sie eine Anwendung schreiben, fügen Sie Mantle direkt in Xcode zu Ihren Projektabhängigkeiten hinzu.
Wenn Sie ein Paket schreiben, das Mantle als Abhängigkeit erfordert, fügen Sie es der dependencies
in seinem Package.swift
Manifest hinzu, zum Beispiel:
dependencies: [ .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0")) ]
Mantle wird unter der MIT-Lizenz veröffentlicht. Siehe LICENSE.md.
Haben Sie eine Frage? Bitte eröffnen Sie ein Problem!