يُسهل Mantle كتابة طبقة نموذجية بسيطة لتطبيق Cocoa أو Cocoa Touch.
ما الخطأ في الطريقة التي تتم بها كتابة الكائنات النموذجية عادةً في لغة Objective-C؟
دعونا نستخدم GitHub API للتوضيح. كيف يمكن عادةً تمثيل مشكلة GitHub في Objective-C؟
تعداد typedef: NSUInteger { غيسويستاتيوبين، GHIssueStateClosed } GHIssueState;@interface GHIssue : NSObject <NSCoding, NSCopying>@property (غير ذري، نسخ، للقراءة فقط) NSURL *URL؛@property (غير ذري، نسخ، للقراءة فقط) NSURL *HTMLURL؛@property (غير ذري، نسخ، للقراءة فقط) NSNumber * رقم؛@ الخاصية (غير ذرية، تعيين، للقراءة فقط) حالة GHIssueState؛@property (غير ذرية، نسخة، للقراءة فقط) NSString *reporterLogin؛@property (غير ذرية، نسخة، للقراءة فقط) NSDate *updatedAt؛@property (غير ذرية، قوي، للقراءة فقط) GHUser *signee؛@property (غير ذرية، نسخة) ، للقراءة فقط) NSDate *retrievedAt;@property (غير ذرية، نسخة) NSString *title;@property (غير ذرية، نسخة) 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; } - (معرف)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 ([قاموس[@"state"] isEqualToString:@"open"]) { _state = GHIssueStateOpen; } else if ([dictionary[@"state"] isEqualToString:@" Closed"]) { _state = GHIssueStateClosed; } _title = [نسخة القاموس[@"title"]]; _retrievedAt = [تاريخ NSDate]؛ _body = [نسخة القاموس[@"body"]]؛ _reporterLogin = [نسخة القاموس[@"user"][@"تسجيل الدخول"]]; _assistance = [[GHUser alloc] initWithDictionary:dictionary[@"assistance"]]; _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];return self; } - (معرف)initWithCoder:(NSCoder *)coder { self = [self init];if (self == nil) return nil; _URL = [المبرمج decodeObjectForKey:@"URL"]; _HTMLURL = [المبرمج decodeObjectForKey:@"HTMLURL"]; _number = [decodeObjectForKey:@"number"]; _state = [decodeUnsignedIntegerForKey:@"state"]; _title = [coder decodeObjectForKey:@"title"]; _retrievedAt = [تاريخ NSDate]؛ _body = [coder decodeObjectForKey:@"body"]; _reporterLogin = [coder decodeObjectForKey:@"reporterLogin"]; _assistance = [coder decodeObjectForKey:@"asseeee"]; _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.signee != nil) [coder encodeObject:self.signee forKey:@"assennee"];if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"]; [coder encodeUnsignedInteger:self.state forKey:@"state"]; } - (معرف)copyWithZone:(NSZone *)zone { GHIssue *issue = [[self.class allocWithZone:zone] init]; المشكلة->_URL = self.URL; المشكلة->_HTMLURL = self.HTMLURL; المشكلة->_number = self.number; المشكلة->_state = self.state; المشكلة->_reporterLogin = self.reporterLogin; المشكلة->_assistance = self.assistance; المشكلة->_updatedAt = self.updatedAt; Issue.title = self.title; المشكلة->_retrievedAt = [تاريخ NSDate]؛ Issue.body = self.body;إرجاع المشكلة; } - (NSUInteger) التجزئة {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 مناسب تمامًا.
ومع ذلك، فإنه يأتي مع بضع نقاط الألم:
لا يزال هناك الكثير من النموذجي. تعمل الكائنات المُدارة على تقليل بعض النموذج الموضح أعلاه، ولكن البيانات الأساسية لديها الكثير من الميزات الخاصة بها. يمكن أن يستغرق إعداد مكدس البيانات الأساسية بشكل صحيح (مع مخزن دائم ومنسق تخزين مستمر) وتنفيذ عمليات الجلب عدة أسطر من التعليمات البرمجية.
من الصعب الحصول على الحق. حتى المطورين ذوي الخبرة يمكن أن يرتكبوا أخطاء عند استخدام البيانات الأساسية، وإطار العمل ليس متسامحًا.
إذا كنت تحاول فقط الوصول إلى بعض كائنات JSON، فقد تتطلب البيانات الأساسية الكثير من العمل لتحقيق مكاسب قليلة.
ومع ذلك، إذا كنت تستخدم أو تريد استخدام Core Data في تطبيقك بالفعل، فلا يزال بإمكان Mantle أن يكون طبقة ترجمة ملائمة بين واجهة برمجة التطبيقات (API) وكائنات النموذج المُدارة.
أدخل نموذج MTL . هذا ما يبدو عليه GHIssue
وراثة من MTLModel
:
تعداد typedef: NSUInteger { غيسويستاتيوبين، GHIssueStateClosed } GHIssueState;@interface GHIssue: MTLModel <MTLJSONSerializing>@property (غير ذرية، نسخ، للقراءة فقط) NSURL *URL؛@property (غير ذرية، نسخ، للقراءة فقط) NSURL *HTMLURL؛@property (غير ذرية، نسخ، للقراءة فقط) NSNumber *number; @property (غير ذرية، تعيين، للقراءة فقط) GHIssueState State;@property (غير ذرية، نسخ، للقراءة فقط) NSString *reporterLogin؛@property (غير ذرية، قوي، للقراءة فقط) GHUser *تعيين؛@property (غير ذرية، نسخ، للقراءة فقط) NSDate *updatedAt;@property (غير ذرية، نسخة) NSString *title;@property (غير ذرية، نسخة) NSString *body;@property (غير ذرية، نسخة، للقراءة فقط) NSDate *تم الاسترجاع في؛@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"،@"assistance": @"assistance"،@"updatedAt": @"updated_at"}; } + (NSValueTransformer *)URLJSONTransformer {return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)HTMLURLJSONTransformer {return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; } + (NSValueTransformer *)stateJSONTransformer {return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{@"open": @(GHIssueStateOpen)،@"مغلق": @(GHIssueStateClosed) }]; } + (NSValueTransformer *)signeeJSONTransformer {return [MTLJSONAdapter DictionaryTransformerWithModelClass:GHUser.class]; } + (NSValueTransformer *)updatedAtJSONTransformer {return [MTLValueTransformer converterUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter dateFromString:dateString]; }verseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter stringFromDate:date]; }]; } - (نوع المثيل)initWithDictionary:(NSDictionary *)dictionaryValue error:(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، تحتاج إلى تنفيذ <MTLJSONSerializing>
في فئة MTLModel
الفرعية الخاصة بك. يتيح لك هذا استخدام MTLJSONAdapter
لتحويل كائنات النموذج الخاصة بك من JSON والعكس:
NSError *error = nil; XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
NSError *error = nil;NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:خطأ المستخدم:&خطأ];
+JSONKeyPathsByPropertyKey
يحدد القاموس الذي يتم إرجاعه بهذه الطريقة كيفية تعيين خصائص كائن النموذج الخاص بك إلى المفاتيح الموجودة في تمثيل JSON، على سبيل المثال:
@interface XYUser : MTLModel@property (للقراءة فقط، غير ذرية، نسخة) NSString *name;@property (للقراءة فقط، غير ذرية، قوي) NSDate *createdAt;@property (للقراءة فقط، غير ذرية، تعيين، getter = isMeUser) BOOL meUser;@property ( للقراءة فقط، غير ذرية، قوي) XYHelper *helper;@end@implementation XYUser+ (NSDictionary *)JSONKeyPathsByPropertyKey {return @{@"name": @"name",@"createdAt": @"created_at"}; } - (نوع المثيل)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error { self = [super initWithDictionary:dictionaryValue error:error];if (self == nil) return nil; _helper = [XYHelper helperWithName:self.name createAt:self.createdAt];return self; }@نهاية
في هذا المثال، تعلن فئة XYUser
عن أربع خصائص يعالجها Mantle بطرق مختلفة:
يتم تعيين name
إلى مفتاح يحمل نفس الاسم في تمثيل JSON.
يتم تحويل createdAt
إلى ما يعادله من حالة الثعبان.
لم يتم إجراء تسلسل meUser
إلى JSON.
تتم تهيئة helper
مرة واحدة بالضبط بعد إلغاء تسلسل JSON.
استخدم -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
إذا كانت الفئة الفائقة لنموذجك تنفذ أيضًا MTLJSONSerializing
لدمج تعييناتها.
إذا كنت ترغب في تعيين جميع خصائص فئة النموذج إلى نفسها، فيمكنك استخدام الأسلوب المساعد +[NSDictionary mtl_identityPropertyMapWithModel:]
.
عند إلغاء تسلسل JSON باستخدام +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
، يتم تجاهل مفاتيح 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 TransformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {return [self.dateFormatter dateFromString:dateString]; }verseBlock:^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+ (Class)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
.
إذا كنت بحاجة إلى شيء أكثر قوة، أو تريد تجنب الاحتفاظ بنموذجك بالكامل في الذاكرة مرة واحدة، فقد تكون البيانات الأساسية خيارًا أفضل.
يدعم Mantle أهداف نشر النظام الأساسي التالية:
ماك 10.10+
دائرة الرقابة الداخلية 9.0+
نظام التشغيل تي في او اس 9.0+
واتش او اس 2.0+
لإضافة Mantle إلى تطبيقك:
أضف مستودع Mantle كوحدة فرعية لمستودع تطبيقك.
قم بتشغيل git submodule update --init --recursive
من داخل مجلد Mantle.
قم بسحب وإسقاط Mantle.xcodeproj
في مشروع Xcode الخاص بالتطبيق الخاص بك.
في علامة التبويب "عام" الخاصة بهدف التطبيق الخاص بك، قم بإضافة Mantle.framework
إلى "الثنائيات المضمنة".
إذا كنت تقوم بدلاً من ذلك بتطوير Mantle بمفرده، فاستخدم ملف Mantle.xcworkspace
.
ما عليك سوى إضافة Mantle إلى Cartfile
الخاص بك:
github "Mantle/Mantle"
أضف Mantle إلى Podfile
الخاص بك ضمن هدف البناء الذي يريدون استخدامه فيه:
target 'MyAppOrFramework' do pod 'Mantle' end
ثم قم بتشغيل pod install
داخل Terminal أو تطبيق CocoaPods.
إذا كنت تكتب تطبيقًا، أضف Mantle إلى تبعيات مشروعك مباشرة داخل Xcode.
إذا كنت تكتب حزمة تتطلب Mantle كتبعية، فأضفها إلى قائمة dependencies
في بيان Package.swift
الخاص بها، على سبيل المثال:
dependencies: [ .package(url: "https://github.com/Mantle/Mantle.git", .upToNextMajor(from: "2.0.0")) ]
تم إصدار Mantle بموجب ترخيص MIT. راجع LICENSE.md.
هل لديك سؤال؟ يرجى فتح قضية!