Mantle memudahkan penulisan lapisan model sederhana untuk aplikasi Cocoa atau Cocoa Touch Anda.
Apa yang salah dengan cara penulisan objek model di Objective-C?
Mari gunakan GitHub API untuk demonstrasi. Bagaimana biasanya seseorang mewakili masalah GitHub di Objective-C?
typedef enum : NSUInteger { GHIIssueStateTerbuka, GHIssueStateDitutup } GHIssueState;@interface GHIssue : NSObject <NSCoding, NSCopying>@property (nonatomic, copy, readonly) NSURL *URL;@property (nonatomic, copy, readonly) NSURL *HTMLURL;@property (nonatomic, copy, readonly) NSNumber * nomor;@property (nonatomik, tetapkan, hanya baca) 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 *diambilDi;@property (nonatomik, salin) NSString *title;@property (nonatomik, salin) NSString *body; - (id)initWithDictionary:(NSDictionary *)kamus;@end
@implementasi GHIssue+ (NSDateFormatter *)dateFormatter {NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[alokasi NSLocale] initWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";return dateFormatter; } - (id)initWithDictionary:(NSDictionary *)kamus { self = [self init];if (self == nihil) mengembalikan nihil; _URL = [NSURL URLWithString:kamus[@"url"]]; _HTMLURL = [NSURL URLWithString:kamus[@"html_url"]]; _number = kamus[@"number"];if ([kamus[@"state"] isEqualToString:@"open"]) { _state = GHIssueStateOpen; } else if ([kamus[@"state"] isEqualToString:@"closed"]) { _state = GHIssueStateClosed; } _judul = [kamus[@"judul"] salinan]; _retrievedAt = [tanggal NSD]; _body = [kamus[@"body"] salin]; _reporterLogin = [kamus[@"pengguna"][@"login"] salin]; _assignee = [[alokasi GHUser] initWithDictionary:dictionary[@"assignee"]]; _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];kembalikan diri; } - (id)initWithCoder:(NSCoder *)coder { self = [self init];if (self == nihil) mengembalikan nihil; _URL = [pembuat kode decodeObjectForKey:@"URL"]; _HTMLURL = [pembuat kode decodeObjectForKey:@"HTMLURL"]; _number = [pembuat kode decodeObjectForKey:@"number"]; _state = [kode decodeUnsignedIntegerForKey:@"state"]; _title = [pembuat kode decodeObjectForKey:@"title"]; _retrievedAt = [tanggal NSD]; _body = [pembuat kode decodeObjectForKey:@"body"]; _reporterLogin = [pembuat kode decodeObjectForKey:@"reporterLogin"]; _assignee = [pembuat kode decodeObjectForKey:@"penerima tugas"]; _updatedAt = [coder decodeObjectForKey:@"updatedAt"];kembalikan diri; } - (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 != nihil) [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 *)zona { GHIssue *masalah = [[self.class allocWithZone:zone] init]; masalah->_URL = self.URL; masalah->_HTMLURL = self.HTMLURL; issue->_number = self.number; masalah->_state = self.state; isu->_reporterLogin = self.reporterLogin; issue->_assignee = self.assignee; masalah->_updatedAt = self.updatedAt; issue.title = self.title; issue->_retrievedAt = [tanggal NSDate]; issue.body = self.body;masalah kembali; } - (NSUInteger)hash {kembalikan 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]; }@akhir
Wah, itu banyak sekali contoh untuk sesuatu yang sangat sederhana! Meskipun demikian, ada beberapa masalah yang tidak dapat diatasi oleh contoh ini:
Tidak ada cara untuk memperbarui GHIssue
dengan data baru dari server.
Tidak ada cara untuk mengubah GHIssue
kembali menjadi JSON.
GHIssueState
tidak boleh dikodekan apa adanya. Jika enum berubah di masa mendatang, arsip yang ada mungkin rusak.
Jika antarmuka GHIssue
berubah, arsip yang ada mungkin rusak.
Data Inti memecahkan masalah tertentu dengan sangat baik. Jika Anda perlu menjalankan kueri kompleks di seluruh data Anda, menangani grafik objek besar dengan banyak hubungan, atau mendukung pembatalan dan pengulangan, Data Inti adalah pilihan yang tepat.
Namun, hal ini disertai dengan beberapa masalah:
Masih banyak boilerplate. Objek yang dikelola mengurangi beberapa boilerplate yang terlihat di atas, namun Core Data memiliki banyak kelebihannya sendiri. Menyiapkan tumpukan Data Inti dengan benar (dengan penyimpanan persisten dan koordinator penyimpanan persisten) dan mengeksekusi pengambilan dapat memerlukan banyak baris kode.
Sulit untuk menjadi benar. Bahkan pengembang berpengalaman pun bisa membuat kesalahan saat menggunakan Data Inti, dan kerangka kerja ini tidak bisa memaafkan.
Jika Anda hanya mencoba mengakses beberapa objek JSON, Data Inti bisa menjadi pekerjaan yang berat dengan sedikit keuntungan.
Meskipun demikian, jika Anda sudah menggunakan atau ingin menggunakan Data Inti di aplikasi Anda, Mantle masih bisa menjadi lapisan terjemahan yang nyaman antara API dan objek model terkelola Anda.
Masukkan MTLModel . Seperti inilah tampilan GHIssue
yang diwarisi dari MTLModel
:
typedef enum : NSUInteger { GHIIssueStateTerbuka, GHIssueStateDitutup } GHIssueState;@interface GHIssue : MTLModel <MTLJSONSerializing>@property (nonatomic, copy, readonly) NSURL *URL;@property (nonatomic, copy, readonly) NSURL *HTMLURL;@property (nonatomic, copy, readonly) NSNumber *number; @property (nonatomik, tetapkan, hanya baca) 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 (nonatomik, salin) NSString *body;@property (nonatomik, salin, hanya baca) NSDate *diambilPada;@end
@implementasi GHIssue+ (NSDateFormatter *)dateFormatter {NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[alokasi NSLocale] 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 {kembalikan [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)HTMLURLJSONTransformer {kembalikan [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)stateJSONTransformer {kembalikan [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{@"open": @(GHIssueStateOpen),@"closed": @(GHIssueStateClosed) }]; } + (NSValueTransformer *)assigneeJSONTransformer {kembalikan [MTLJSONAdapter kamusTransformerWithModelClass:GHUser.class]; } + (NSValueTransformer *)updatedAtJSONTransformer {kembalikan [MTLValueTransformer transformatorUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {kembali [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;// Menyimpan nilai yang perlu ditentukan secara lokal saat inisialisasi._retrievedAt = [tanggal NSDate];return self; }@akhir
Yang paling tidak ada dalam versi ini adalah implementasi dari <NSCoding>
, <NSCopying>
, -isEqual:
, dan -hash
. Dengan memeriksa deklarasi @property
yang Anda miliki di subkelas, MTLModel
dapat menyediakan implementasi default untuk semua metode ini.
Masalah dengan contoh asli semuanya telah diperbaiki juga:
Tidak ada cara untuk memperbarui
GHIssue
dengan data baru dari server.
MTLModel
memiliki metode -mergeValuesForKeysFromModel:
yang dapat diperluas, yang memudahkan untuk menentukan bagaimana data model baru harus diintegrasikan.
Tidak ada cara untuk mengubah
GHIssue
kembali menjadi JSON.
Di sinilah transformator reversibel sangat berguna. +[MTLJSONAdapter JSONDictionaryFromModel:error:]
dapat mengubah objek model apa pun yang sesuai dengan <MTLJSONSerializing>
kembali ke kamus JSON. +[MTLJSONAdapter JSONArrayFromModels:error:]
sama tetapi mengubah array objek model menjadi array kamus JSON.
Jika antarmuka
GHIssue
berubah, arsip yang ada mungkin rusak.
MTLModel
secara otomatis menyimpan versi objek model yang digunakan untuk pengarsipan. Saat membatalkan pengarsipan, -decodeValueForKey:withCoder:modelVersion:
akan dipanggil jika diganti, sehingga memberi Anda kemudahan untuk mengupgrade data lama.
Untuk membuat serialisasi objek model dari atau ke JSON, Anda perlu mengimplementasikan <MTLJSONSerializing>
di subkelas MTLModel
Anda. Ini memungkinkan Anda menggunakan MTLJSONAdapter
untuk mengonversi objek model Anda dari JSON dan sebaliknya:
NSError *kesalahan = nihil; XYUser *pengguna = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
NSError *error = nihil;NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:kesalahan pengguna:&error];
+JSONKeyPathsByPropertyKey
Kamus yang dikembalikan oleh metode ini menentukan bagaimana properti objek model Anda dipetakan ke kunci dalam representasi JSON, misalnya:
@interface XYUser : MTLModel@property (hanya baca, nonatomik, salin) NSString *nama;@property (hanya baca, nonatomik, kuat) NSDate *createdAt;@property (hanya baca, nonatomik, tetapkan, pengambil = isMeUser) BOOL meUser;@property ( hanya dapat dibaca, nonatomik, kuat) 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 == nihil) mengembalikan nihil; _helper = [XYHelper helperWithName:self.name createAt:self.createdAt];mengembalikan diri; }@akhir
Dalam contoh ini, kelas XYUser
mendeklarasikan empat properti yang ditangani Mantle dengan cara berbeda:
name
dipetakan ke kunci dengan nama yang sama dalam representasi JSON.
createdAt
dikonversi ke kasus ular yang setara.
meUser
tidak diserialkan ke dalam JSON.
helper
diinisialisasi tepat satu kali setelah deserialisasi JSON.
Gunakan -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
jika superkelas model Anda juga mengimplementasikan MTLJSONSerializing
untuk menggabungkan pemetaannya.
Jika Anda ingin memetakan semua properti kelas Model ke dirinya sendiri, Anda dapat menggunakan metode pembantu +[NSDictionary mtl_identityPropertyMapWithModel:]
.
Saat melakukan deserialisasi JSON menggunakan +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
, kunci JSON yang tidak sesuai dengan nama properti atau memiliki pemetaan eksplisit akan diabaikan:
NSDictionary *JSONDictionary = @{@"name": @"john",@"created_at": @"2013/07/02 16:40:00 +0000",@"plan": @"lite"}; XYUser *pengguna = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
Di sini, plan
tersebut akan diabaikan karena tidak cocok dengan nama properti XYUser
atau dipetakan dalam +JSONKeyPathsByPropertyKey
.
+JSONTransformerForKey:
Terapkan metode opsional ini untuk mengonversi properti dari tipe berbeda saat melakukan deserialisasi dari JSON.
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key { if ([key isEqualToString:@"createdAt"]) { return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName]; } return nil; }
key
adalah kunci yang diterapkan pada objek model Anda; bukan kunci JSON asli. Ingatlah hal ini jika Anda mengubah nama kunci menggunakan +JSONKeyPathsByPropertyKey
.
Untuk menambah kenyamanan, jika Anda menerapkan +<key>JSONTransformer
, MTLJSONAdapter
akan menggunakan hasil dari metode tersebut. Misalnya, tanggal yang biasanya direpresentasikan sebagai string di JSON dapat diubah menjadi NSDate
seperti:
kembalikan [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]; }]; }
Jika trafo bersifat reversibel, trafo juga akan digunakan saat membuat serial objek ke dalam JSON.
+classForParsingJSONDictionary:
Jika Anda mengimplementasikan klaster kelas, terapkan metode opsional ini untuk menentukan subkelas mana dari kelas dasar Anda yang harus digunakan saat melakukan deserialisasi objek dari JSON.
@interface XYMessage : MTLModel@end@interface XYTextMessage: XYMessage@property (hanya baca, nonatomik, salin) NSString *body;@end@interface XYPictureMessage : XYMessage@property (hanya baca, nonatomik, kuat) NSURL *imageURL;@end@implementation XYMessage+ (Kelas)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {if (JSONDictionary[@"image_url"] != nihil) {return XYPictureMessage.class; }if (JSONDictionary[@"body"] != nihil) {return XYTextMessage.class; }NSAssert(NO, @"Tidak ada kelas yang cocok untuk kamus JSON '%@'.", JSONDictionary);return self; }@akhir
MTLJSONAdapter
kemudian akan memilih kelas berdasarkan kamus JSON yang Anda berikan:
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 tidak secara otomatis menyimpan objek untuk Anda. Namun, MTLModel
mematuhi <NSCoding>
, sehingga objek model dapat diarsipkan ke disk menggunakan NSKeyedArchiver
.
Jika Anda memerlukan sesuatu yang lebih kuat, atau ingin menghindari menyimpan seluruh model dalam memori sekaligus, Data Inti mungkin merupakan pilihan yang lebih baik.
Mantle mendukung target penerapan platform berikut:
macOS 10.10+
iOS 9.0+
tvOS 9.0+
tontonOS 2.0+
Untuk menambahkan Mantle ke aplikasi Anda:
Tambahkan repositori Mantle sebagai submodul repositori aplikasi Anda.
Jalankan git submodule update --init --recursive
dari dalam folder Mantle.
Seret dan lepas Mantle.xcodeproj
ke proyek Xcode aplikasi Anda.
Pada tab "Umum" target aplikasi Anda, tambahkan Mantle.framework
ke "Binari Tertanam".
Jika Anda mengembangkan Mantle sendiri, gunakan file Mantle.xcworkspace
.
Cukup tambahkan Mantle ke Cartfile
Anda:
github "Mantle/Mantle"
Tambahkan Mantle ke Podfile
Anda di bawah target build yang mereka inginkan untuk menggunakannya:
target 'MyAppOrFramework' do pod 'Mantle' end
Kemudian jalankan pod install
di Terminal atau aplikasi CocoaPods.
Jika Anda sedang menulis aplikasi, tambahkan Mantle ke dependensi proyek Anda langsung di dalam Xcode.
Jika Anda menulis paket yang memerlukan Mantle sebagai dependensi, tambahkan paket tersebut ke daftar dependencies
di manifes Package.swift
, misalnya:
dependencies: [ .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0")) ]
Mantle dirilis di bawah lisensi MIT. Lihat LISENSI.md.
Punya pertanyaan? Silakan buka terbitan!