Mantle を使用すると、Cocoa または Cocoa Touch アプリケーション用の単純なモデル層を簡単に作成できます。
モデル オブジェクトが通常 Objective-C で記述される方法の何が問題になっているのでしょうか?
デモンストレーションのために GitHub API を使用してみましょう。一般的に、GitHub の問題を Objective-C でどのように表現するでしょうか?
typedef enum : NSUInteger { GHIssueStateOpen、 GHI問題状態クローズ済み } GHIssueState;@interface GHIssue : NSObject <NSCoding, NSCopying>@property (非アトミック、コピー、読み取り専用) NSURL *URL;@property (非アトミック、コピー、読み取り専用) NSURL *HTMLURL;@property (非アトミック、コピー、読み取り専用) NSNumber * number;@property (非アトミック、割り当て、読み取り専用) GHIssueState state;@property (非アトミック、コピー、読み取り専用) NSString *reporterLogin;@property (非アトミック、コピー、読み取り専用) NSDate *updatedAt;@property (非アトミック、強力、読み取り専用) GHUser *assignee;@property (非アトミック、コピー、読み取り専用) NSDate *retrievedAt; @property (非アトミック、コピー) NSString *title;@property (非アトミック、コピー) NSString *ボディ; - (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"] コピー]; _retrievedAt = [NSD 日付]; _body = [dictionary[@"body"] コピー]; _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 = [コーダー decodeObjectForKey:@"URL"]; _HTMLURL = [コーダー decodeObjectForKey:@"HTMLURL"]; _number = [coder decodeObjectForKey:@"number"]; _state = [コーダー decodeUnsignedIntegerForKey:@"state"]; _title = [coder decodeObjectForKey:@"title"]; _retrievedAt = [NSD 日付]; _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;問題を返す; } - (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]; }@終わり
うーん、これほど単純なことに対して、定型文はたくさんありますね。それでも、この例では対処できない問題がいくつかあります。
サーバーからの新しいデータでGHIssue
更新する方法はありません。
GHIssue
JSON に戻す方法はありません。
GHIssueState
そのままエンコードしないでください。将来列挙型が変更されると、既存のアーカイブが破損する可能性があります。
GHIssue
のインターフェースが将来変更されると、既存のアーカイブが破損する可能性があります。
Core Data は特定の問題を非常にうまく解決します。データ全体にわたって複雑なクエリを実行する必要がある場合、多くの関係を含む巨大なオブジェクト グラフを処理する必要がある場合、または元に戻す/やり直しをサポートする必要がある場合、Core Data は最適です。
ただし、いくつかの問題点があります。
定型文はまだたくさんあります。管理対象オブジェクトは上記の定型文の一部を削減しますが、Core Data には独自の定型文がたくさんあります。 Core Data スタック (永続ストアと永続ストア コーディネーターを使用) を正しく設定し、フェッチを実行するには、多くのコード行が必要になる場合があります。
正しく理解するのは難しいです。経験豊富な開発者でも Core Data を使用する際には間違いを犯す可能性があり、フレームワークは容赦ありません。
一部の JSON オブジェクトにアクセスしようとしているだけの場合、Core Data は多大な労力を費やしてほとんど利益を得ることができない可能性があります。
それにもかかわらず、すでにアプリで Core Data を使用している、または使用したい場合、Mantle は API とマネージド モデル オブジェクト間の便利な変換レイヤーとして機能します。
MTLModelと入力します。 MTLModel
から継承したGHIssue
次のようになります。
typedef enum : NSUInteger { GHIssueStateOpen、 GHI問題状態クローズ済み } 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 (非アトミック、コピー、 readonly) 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",@"担当者": @"担当者",@"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 [MTLValueTransformerTransformerUsingForwardBlock:^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]; } }]; } - (インスタンスタイプ)initWithDictionary:(NSDictionary *)dictionaryValue エラー:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil;// 初期化時にローカルで決定する必要がある値を格納します。_retrievedAt = [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 との間でシリアル化するには、 MTLModel
サブクラスに<MTLJSONSerializing>
を実装する必要があります。これにより、 MTLJSONAdapter
使用してモデル オブジェクトを JSON から変換したり、その逆に変換したりできるようになります。
NSError *エラー = nil; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
NSError *error = nil;NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user error:&error];
+JSONKeyPathsByPropertyKey
このメソッドによって返されるディクショナリは、モデル オブジェクトのプロパティが JSON 表現のキーにどのようにマップされるかを指定します。次に例を示します。
@interface XYUser : MTLModel@property (読み取り専用、非アトミック、コピー) NSString *name;@property (読み取り専用、非アトミック、強力) NSDate *createdAt;@property (読み取り専用、非アトミック、割り当て、getter = isMeUser) BOOL meUser;@property ( readonly、非アトミック、強力) XYHelper *helper;@end@implementation XYUser+ (NSDictionary *)JSONKeyPathsByPropertyKey {return @{@"name": @"name",@"createdAt": @"created_at"}; } - (インスタンスタイプ)initWithDictionary:(NSDictionary *)dictionaryValue エラー:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil; _helper = [XYHelper helperWithName:self.name createdAt:self.createdAt];return self; }@終わり
この例では、 XYUser
クラスは、Mantle がさまざまな方法で処理する 4 つのプロパティを宣言します。
name
、JSON 表現内の同じ名前のキーにマップされます。
createdAt
スネークケースに相当するものに変換されます。
meUser
JSON にシリアル化されません。
helper
JSON 逆シリアル化後に 1 回だけ初期化されます。
モデルのスーパークラスもMTLJSONSerializing
を実装してマッピングをマージする場合は、 -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
を使用します。
Model クラスのすべてのプロパティをそれ自体にマップする場合は、 +[NSDictionary mtl_identityPropertyMapWithModel:]
ヘルパー メソッドを使用できます。
+[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
を使用して JSON を逆シリアル化する場合、プロパティ名に対応しない、または明示的なマッピングを持つ 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 トランスフォーマーUsingForwardBlock:^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]; } }]; }
トランスフォーマーが可逆であれば、オブジェクトを 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"] != nil) {return XYPictureMessage.class; }if (JSONDictionary[@"body"] != nil) {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以降
テレビOS 9.0以降
watchOS 2.0以降
Mantle をアプリケーションに追加するには:
Mantle リポジトリをアプリケーションのリポジトリのサブモジュールとして追加します。
Mantle フォルダー内からgit submodule update --init --recursive
実行します。
Mantle.xcodeproj
アプリケーションの Xcode プロジェクトにドラッグ アンド ドロップします。
アプリケーション ターゲットの [全般] タブで、 Mantle.framework
[埋め込みバイナリ] に追加します。
Mantle を独自に開発している場合は、 Mantle.xcworkspace
ファイルを使用してください。
Mantle をCartfile
に追加するだけです。
github "Mantle/Mantle"
Mantle を使用するビルド ターゲットの下のPodfile
に追加します。
target 'MyAppOrFramework' do pod 'Mantle' end
次に、ターミナルまたは CocoaPods アプリ内でpod install
実行します。
アプリケーションを作成している場合は、Xcode 内でプロジェクトの依存関係に Mantle を直接追加します。
Mantle を依存関係として必要とするパッケージを作成している場合は、それをPackage.swift
マニフェストのdependencies
リストに追加します。次に例を示します。
dependencies: [ .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0")) ]
Mantle は MIT ライセンスに基づいてリリースされています。 LICENSE.mdを参照してください。
質問がありますか?問題を開いてください。