#Motis 对象映射
使用 Cocoa 的键值编码 (KVC) 将 JSON 易于映射到 NSObject
Motis 提供了一个用户友好的界面,具有键值编码,为您的 NSObjects 提供了将存储在字典中的键值映射到自身的工具。使用 Motis,您的对象将负责每个映射(分布式映射定义)并且您不必担心数据验证,因为 Motis 会为您验证对象类型。
查看我们的博客文章 使用 KVC 解析 JSON 以了解更多关于 Motis 的信息。
如何获取 Motis
如果您使用 CocoaPods,您可以通过在 podfile 中添加以下代码来获取 Motis
pod 'Motis', '~>1.4.0'
使用 Motis
- 导入文件
#import <Motis/Motis.h>
- 设置您的 motis 对象(见下文)。
- 获取一些要映射到模型对象的 JSON。
- 在您的模型对象上调用
mts_setValuesForKeysWithDictionary:
,使用要映射的 JSON 字典作为参数。
- (void)motisTest
{
// Some JSON object
NSDictionary *jsonObject = [...];
// Creating an instance of your class
MyClass instance = [[MyClass alloc] init];
// Parsing and setting the values of the JSON object
[instance mts_setValuesForKeysWithDictionary:jsonObject];
}
设置Motis对象
1. 定义Motis映射字典
您自定义的对象(Objective-C的子类)需要重写+mts_mapping
方法,并定义从JSON键到Objective-C属性名的映射。
例如,如果接收到以下JSON
{
{
"user_name" : "john.doe",
"user_id" : 42,
"creation_date" : "1979-11-07 17:23:51",
"webiste" : "http://www.domain.com",
"user_stats" : {
"views" : 431,
"ranking" : 12,
},
"user_avatars": [{
"avatar_type": "standard",
"image_url": "http://www.avatars.com/john.doe"
}, {
"avatar_type": "large",
"image_url": "http://www.avatars.com/john.doe/large"
}]
}
}
那么,在我们的+mts_mapping
方法
// --- User.h --- //
@interface User : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSIntger userId;
@property (nonatomic, strong) NSDate *creationDate;
@property (nonatomic, strong) NSURL *website;
@property (nonatomic, assign) NSInteger views;
@property (nonatomic, assign) NSInteger ranking;
@property (nonatomic, assign) NSURL *avatar;
@end
// --- User.m --- //
@implementation User
+ (NSDictionary*)mts_mapping
{
return @{@"user_name": mts_key(name),
@"user_id": mts_key(userId),
@"creation_date": mts_key(creationDate),
@"website": mts_key(website),
@"user_stats.views": mts_key(views), // <-- KeyPath access
@"user_stats.ranking": mts_key(ranking), // <-- KeyPath access
@"user_avatars.0.image_url": mts_key(avatar) // <-- KeyPath access using array index
};
}
@end
如上示例所示,支持通过KeyPath访问引用字典的JSON内容。当需要使用固定的数组索引将值映射到属性时,KeyPath访问支持数组索引查找。
另外,如你所见,我们指定了自定义类型,如NSDate
或NSURL
。Motis自动尝试将JSON值转换为定义的对象类型。有关自动验证的更多信息,请参阅下方。
1.1 映射过滤
默认情况下,Motis尝试通过KVC设置任何属性名。因此,即使你没有在方法+mts_mapping:
中定义映射,但你的JSON字典中包含与你的对象属性名称匹配的键,Motis也会分配和验证这些值。
这可能是个问题,如果你无法控制你的JSON字典。因此,Motis将限制接受的映射键只到Motis映射中定义的键。你可以通过重写方法+mts_shouldSetUndefinedKeys
来改变这种行为。
+ (BOOL)mts_shouldSetUndefinedKeys
{
// By default this method return NO unless you haven't defined a mapping.
// You can override it and return YES to only accept any key.
return NO;
}
1.2 值映射
使用方法 +mts_valueMappingForKey:
,对象可以定义值映射。当需要将字符串值映射到枚举,例如,这非常有用。查看以下如何实现此方法的示例
对于以下JSON...
{
{
"user_name" : "john.doe",
"user_gender": "male",
}
}
...我们定义以下类和Motis行为
typedef NS_ENUM(NSUInteger, MJUserGender)
{
MJUserGenderUndefined,
MJUserGenderMale,
MJUserGenderFemale,
};
@interface User : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assing) MJUserGender gender;
@end
@implementation User
+ (NSDictionary*)mts_mapping
{
return @{"user_name": mts_key(name),
"user_gender": mts_key(gender),
};
}
+ (NSDictionary*)mts_valueMappingForKey:(NSString*)key
{
if ([key isEqualToString:mts_key(gender)])
{
return @{"male": @(MJUserGenderMale),
"female": @(MJUserGenderFemale),
MTSDefaultValue: @(MJUserGenderUndefined),
};
}
return nil;
}
@end
上述代码将自动将 "male"/"female" 值转换为枚举 MJUserGender。使用 MTSDefaultValue
键,我们还可以指定当值不在字典中或为 null 时要使用的默认值。如果没有定义 MTSDefalutValue
,那么 Motis 将尝试设置原始接收值。
2. 值验证
2.1 自动验证
Motis 在从 JSON 响应映射时检查值的对象类型。系统将尝试匹配您定义的属性类型(在你的类中进行逆向检查)。JSON 对象仅包含字符串、数字、字典和数组。自动验证尝试将这几种类型转换为您的属性类型。如果无法实现,则不会执行那些属性的映射,并且值不会被设置。
例如,如果您指定一个属性类型为 NSURL
,而您的 JSON 正在发送字符串值,Motis 将自动将字符串转换为 NSURL。但是,如果您的属性类型是 "UIImage" 并且您的 json 正在发送数字,Motis 将忽略该值且不会设置它。自动验证适用于类型如 NSURL
、NSData
、NSDate
、NSNumber
等。更多信息请参考此文件底部的表格。
除非用户手动验证,否则将始终执行自动验证。因此,在这种情况下,用户负责正确验证值。
2.1.1 自动日期验证
每次您将属性类型指定为 NSDate
时,Motis 将尝试将收到的 JSON 值转换为日期。
- 如果 JSON 值是一个数字或包含数字的字符串,Motis 将在 1979-1-1 以来流逝的秒数作为数字来创建日期。
- 如果 JSON 值是一个字符串,Motis 将使用通过方法
+mts_validationDateFormatter
返回的NSDateFormatter
。Motis 未实现任何默认日期格式化器(因此,此方法默认返回 nil)。您负责提供默认日期格式化器。
然而,如果您的不同键有多个格式,您将需要使用手动验证来验证日期(见下文)。
2.1.2 自动数组验证
为了支持数组内容(数组的对象)的自动验证,您必须重写方法 +mts_arrayClassMapping
并返回一个包含 数组属性名 和 类类型 对的字典。
例如,有以下 JSON
{
"user_name": "john.doe",
"user_id" : 42,
...
"user_followers": [
{
"user_name": "william",
"user_id": 55,
...
},
{
"user_name": "jenny",
"user_id": 14,
...
},
...
]
}
因此,我们的 User
类有一个名为 followers
的 NSArray
属性,它包含类型为 User
的对象列表,我们将会重写该方法并实现如下
@implementation User
+ (NSDictionary*)mts_mapping
{
return @{@"user_name": mts_key(name),
@"user_id": mts_key(userId),
...
@"user_followers": mts_key(followers),
};
}
+ (NSDictionary*)mts_arrayClassMapping
{
return @{mts_key(followers): User.class};
}
@end
2.1.3 自动对象创建
在自动验证过程中,Motis 可能会尝试创建新的自定义对象实例。例如,如果 JSON 值是一个字典,并且与键关联的属性类型是自定义对象,Motis 将尝试递归地创建相应类型的新对象,并通过 -mts_setValuesForKeysWithDictionary:
设置它。
这种自动的“对象创建”,也适用于数组的元素,可以通过以下两种方法进行自定义和跟踪:一种“will”样式的通知方法,用于通知用户将创建一个对象;另一种“did”样式的通知方法,用于通知用户已创建一个对象。
@implementation User
- (id)mts_willCreateObjectOfClass:(Class)typeClass withDictionary:(NSDictionary*)dictionary forKey:(NSString*)key abort:(BOOL*)abort
{
// Return "nil" if you want Motis to handle the object creation and mapping.
// Otherwise, create/reuse an object of the given "typeClass" and map the values from the dictionary and return it.
// If you set "abort" to yes, the value for the given "key" won't be set.
// This method is also used for array contents. In this case, "key" will be the name of the array.
}
- (void)mts_didCreateObject:(id)object forKey:(NSString *)key
{
// Motis notifies you the new created object.
// This method is also used for array contents. In this case, "key" will be the name of the array.
}
@end
2.2 手动验证
手动验证在自动验证之前执行,并允许用户在执行任何自动验证之前手动验证一个值。当然,如果用户为特定的键手动验证了一个值,则任何自动验证都将执行。
Motis调用以下方法来执行手动验证
- (BOOL)mts_validateValue:(inout __autoreleasing id *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing *)outError;
此方法的默认实现是触发KVC验证模式。要手动验证值,您可以实现相应键的KVC验证方法
- (BOOL)validate<Key>:(id *)ioValue error:(NSError * __autoreleasing *)outError
{
// Check *ioValue and assign new value to ioValue if needed.
// Return YES if *ioValue can be assigned to the attribute, NO otherwise
return YES;
}
但是,您也可以覆盖上述Motis手动验证方法,并执行自定义的手动验证(而不使用KVC验证模式)。
2.2.1 手动数组验证
您还可以对数组内容执行手动验证。在映射数组时,Motis会尝试使用在方法+mts_arrayClassMapping
中定义的类型验证数组内容。然而,如果您愿意,可以手动进行验证。通过手动验证,将不会执行自动验证。
要手动验证数组内容,您必须覆盖以下方法
- (BOOL)mts_validateArrayObject:(inout __autoreleasing id *)ioValue forArrayKey:(NSString *)arrayKey error:(out NSError *__autoreleasing *)outError;
{
// Check *ioValue and assign new value to ioValue if needed.
// Return YES if *ioValue can be included into the array, NO otherwise
return YES;
}
2.3 在映射完成后添加自定义操作
可以在Motis完成映射后添加自定义验证。要实现这一点,请覆盖您自定义类中的mts_setValuesForKeysWithDictionary:
方法。
以下示例展示了如何映射JSON字典
{
"a": 2,
"b": 5
}
指向以下对象
@interface MyCustomObject : NSObject
@property (nonatomic, assign) NSInteger a; // JSON property
@property (nonatomic, assign) NSInteger b; // JSON property
@property (nonatomic, assign) NSInteger c; // Computed property
@end
@implementation MyCustomObject
- (void) mts_setValuesForKeysWithDictionary:(NSDictionary*)dictionary
{
// Call the Motis mapping method
[super mts_setValuesForKeysWithDictionary:dictionary];
// After mapping has finished, call the validation method
[self pfx_objectDidFinishMapping];
}
- (void)pfx_objectDidFinishMapping
{
// Perform validations after mapping is finished.
self.c = self.a + self.b;
}
@end
此外,您可以在模型超类中创建一个通用方法,然后在所有子类中继承它。
Motis 对象
您可以通过重新使用 Motis 定义来子类化 MTSMotisObject
以获取一个自动实现 NSCoding
和 NSCopying
的对象。
主要地,MTSMotisObject
使用在 +mts_mapping
字典中定义的所有属性来实现 NSCopying
和 NSCoding
。属性通过键值编码进行获取/设置。
例如,如果我们有以下实现
@interface User : MTSMotisObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSDate *birthday;
@property (nonatomic, strong) NSURL *webiste;
@end
@implementation MTSMotisObject
+ (NSDictionary*)mts_mapping
{
return @{@"user_name": mts_key(name),
@"birth_day": mts_key(birthday),
@"website_url": mts_key(website),
};
}
@end
那么我们可以这样做
- (void)foo
{
User *user1 = [[User alloc] init];
user1.name = @"John Doe";
user1.birthday = [NSDate date];
user1.website = [NSURL URLWithString:@"http://www.google.com"];
// Create a copy of user1
User *user2 = [user1 copy];
// Transform user into NSData
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:user1];
// Transform the data into a User instance
User *user3 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
}
子类也可以通过覆盖方法 +mts_motisPropertyNames
来配置属性名称列表,并返回属性名称的不同子集。默认情况下,此方法返回 Motis 定义的属性名称。
附录
线程安全
从版本 1.0.2 开始,Motis 可以在多个线程中同时使用(线程安全)。
要了解 Motis 出现这个问题的原因,我们需要知道 Motis 将 Objective-C 运行时信息缓存以增加效率。此缓存通过 NSMapTable 和 NSMutableDictionary 实例来完成,这些对象在读写其内容时不是线程安全的,导致 Motis 在线程安全方面失败。
然而,重要的是要理解,最终 Motis 使用 KVC 来访问和处理对象。因此,开发者必须负责使对象的获取者和设置者线程安全,否则 Motis 无法为您做到这一点。
Motis & Core Data
如果你的项目使用 CoreData 并且你在处理 NSManagedObject
实例,你仍然可以使用 Motis 将 JSON 值映射到你的类实例。但是,你需要注意以下几点
####1. 不要使用 KVC 验证
CoreData 在执行 -saveContext:
操作时会使用 KVC 验证来验证 NSManagedObject
属性。因此,你不得使用 KVC 验证来检查 JSON 值的完整性和一致性。因此,你必须重写 Motis 的手动验证方法以避免执行 KVC 验证。
- (BOOL)mts_validateValue:(inout __autoreleasing id *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing *)outError
{
// Do manual validation for the given "inKey"
return YES;
}
2. 帮助 Motis 创建新的实例
在将 JSON 内容解析为对象时,Motis 可能会找到 NSDictionay
实例,这些实例可能被转换为新的模型对象实例。默认情况下,Motis 通过自省为相应的类类型创建一个新实例,并将找到的字典中的值递归地映射过来。但是,当你使用 CoreData 时,你必须分配并初始化 NSManagedObject
实例,提供 NSManagedObjectContext
。
因此,你必须重写方法 - (id)mts_willCreateObjectOfClass:(Class)typeClass withDictionary:(NSDictionary*)dictionary forKey:(NSString*)key abort:(BOOL*)abort
并且返回一个经过 Motis 处理的具有 dictionary
的 typeClass
实例。
例如,我们可以为相应的类创建一个新的托管对象,执行 Motis 并返回
- (id)mts_willCreateObjectOfClass:(Class)typeClass withDictionary:(NSDictionary*)dictionary forKey:(NSString*)key abort:(BOOL*)abort
{
if ([typeClass isSubclassOfClass:NSManagedObject.class])
{
// Get the entityName
NSString *entityName = [typeClass entityName]; // <-- This is a custom method that returns the entity name
// Create a new managed object for the given class (for example).
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:entityName inManagedObjectContext:self.context];
NSManagedObject *object = [[typeClass alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:self.context];
// Perform Motis on the object instance
[object mts_setValuesForKeysWithDictionary:dictionary];
return object;
}
return nil;
}
3. 启用等性检查
为了防止在我们的托管对象中出现不必要的 "hasChanges" 标志,重写方法 -mts_checkValueEqualityBeforeAssignmentForKey:
并返回 YES
,这样 Motis 就会在通过 KVC 分配值之前检查等性。这样,如果一个新值(通过 isEqual:
方法)与当前已设置的值相等,Motis 将不会进行分配。
- (BOOL)mts_checkValueEqualityBeforeAssignmentForKey:(NSString*)key
{
// Return YES to make Motis check for equality before making an assignment via KVC.
// Return NO to make Motis always assign a value via KVC without checking for equality before.
return YES;
}
自动验证
下表表明了当前Motis版本中支持的有效性验证。
+------------+---------------------+------------------------------------------------------------------------------------+
| JSON Type | Property Type | Comments |
+------------+---------------------+------------------------------------------------------------------------------------+
| string | NSString | No validation is requried |
| number | NSNumber | No validation is requried |
| number | basic type (1) | No validation is requried |
| array | NSArray | No validation is requried |
| dictionary | NSDictionary | No validation is requried |
| - | - | - |
| string | bool | string parsed with method -boolValue and by comparing with "true" and "false" |
| string | unsigned long long | string parsed with NSNumberFormatter (allowFloats disabled) |
| string | basic types (2) | value generated automatically by KVC (NSString's '-intValue', '-longValue', etc) |
| string | NSNumber | string parsed with method -doubleValue |
| string | NSURL | created using [NSURL URLWithString:] |
| string | NSData | attempt to decode base64 encoded string |
| string | NSDate | default date format "2011-08-23 10:52:00". Check '+mts_validationDateFormatter.' |
| - | - | - |
| number | NSDate | timestamp since 1970 |
| number | NSString | string by calling NSNumber's '-stringValue' |
| - | - | - |
| array | NSMutableArray | creating new instance from original array |
| array | NSSet | creating new instance from original array |
| array | NSMutableSet | creating new instance from original array |
| array | NSOrderedSet | creating new instance from original array |
| array | NSMutableOrderedSet | creating new instance from original array |
| - | - | - |
| dictionary | NSMutableDictionary | creating new instance from original dictionary |
| dictionary | custom NSObject | Motis recursive call. Check '-mts_willCreateObject..' and '-mtd_didCreateObject:' |
| - | - | - |
| null | nil | if property is type object |
| null | <UNDEFINED> | if property is basic type (3). Check KVC method '-setNilValueForKey:' |
+------------+---------------------+------------------------------------------------------------------------------------+
* basic type (1) : int, unsigned int, long, unsigned long, long long, unsigned long long, float, double)
* basic type (2) : int, unsigned int, long, unsigned long, float, double)
* basic type (3) : any basic type (non-object type).
项目维护者
此开源项目由Joan Martin维护。
许可证
Copyright 2016 Mobile Jazz
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://apache.ac.cn/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.