本文转载至 http://imenjoe.com/2015/04/10/CoreData-transformable-20150410/
在开发过程中有一个需要在 Core Data 中存取 NSDictionary 的需求,但是在 Core Data 的 attributeType 中并没有直接支持 NSDictionary 的类型,基本上就是一些 scalar 类型和 date、binary data、undefined 和 transformable 类型。
最初的想法是把 NSDictionary 转为 NSData 然后用 binary data 的类型写入到 Core Data,然后要用的时候再从 Core Data 中读出 NSData,再转回 NSDictionary,但是想想这样其实也挺麻烦的,想看看 Core Data 有没有更好的对这类需求的支持,这个方法就作为最后的手段来使用吧。于是在扫了一遍 Core Data 的 attributeType 之后就盯上了 transformable 这个类型。
在 Apple 开发者文档中对 transformable 的描述是:
The idea behind transformable attributes is that you access an attribute as a non-standard type, but behind the scenes Core Data uses an instance of NSValueTransformer to convert the attribute to and from an instance of NSData. Core Data then stores the data instance to the persistent store.
大概意思就是把上面提到的想法隐藏在了 transformable 之下,使之可以直接用 non-standard 类型(在我们的例子中就是 NSDictionary)透明地访问 Core Data 中的 attribute。整个转换和持久化的过程都由 Core Data 自动完成。
这个转换过程 Core Data 需要一个 transformer 实例来进行 non-standard 类型跟 NSData 之间的转换,这个 transformer 实例是抽象类 NSValueTransform 的实现的一个实例,这个类的作用在于把一个值转换为另一个值,这个超出了这次的讨论范围,关于 NSValueTransform 可以参考这篇文章。
使用
要使用 transformable 类型有两种方式:
- 如果是在 .xcdatamodeld 中编辑,在 Type 的下拉菜单中选择 transformable 类型,在这个 attribute 的属性页的 Name 文本框中填 transformer name(如果要自定义 transformer 的话才需要填,否则就使用 Core Data 默认的)
- 如果是用代码写的,那么调用 setAttributeType: 时传入 NSTransformableAttributeType 作为参数,然后(如果需要的话)用 setValueTransformerName: 来指定 transformer 的 name。
我使用的是第一种方式,直接选择了 attribute 的 Type 为 transformable 后就完成了。由于 value transformer name 的文本框为空,所以 Core Data 会默认使用 NSKeyedUnarchiveFromDataTransformerName 为 transformer。这个 transformer 其实就是调用 NSKeyArchiver 来对 non-standard 类型转换。整个过程大概类似于这样:
NSDictionary -> NSData
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:myDictionary];
NSData -> NSDictionary
NSDictionary *dictionary = (NSDictionary*)[NSKeyedUnarchiver unarchiverObjectWithData:data];
由于我要存取的 NSDictionary 是直接支持用 NSKeyedArchiver 来转换的,所以我没有自定义 transformer,直接用默认的 transformer 就可以满足我的需要了。
接下来就是重新生成实体类,由于我用的是 mogenerator 来生成的,要让 mogenerator 在生成的实体类中对应的 attribute 是我想要的 NSDcitionary,那么还需要在 .xcdatamodeld 中给 attribute 的 User Info 添加一项
Key | Value | |
---|---|---|
attributeValueClassName | NSDictionary |
mogenerator 在生成实体类时就会从这里获取到信息,知道这个 attribute 要转换的 non-standard 是 NSDictionary,在生成的实体类中对应的 property 就会是 NSDictionary,从而使得我们可以直接以 NSDictionary 类型对这个 property 进行读写,而完全不用管底下的转换和存储的过程。
transformable 类型除了可以存取 NSDictionary 类型之外,还可以存取 UIColor、CGRect 等等类型,总之如果在 Core Data 提供的标准类型中找不到自己想要的类型,那么就可以用 transformable 来进行存取,方便快捷。
扩展
到这里其实这个问题就已经算是解决了,但是我们在上面提到了一个自定义 transformer,这里我们还可以对这个扩展一下。
上面提到了 transformable 可以存取 NSDictionary 之外的其他的 non-standard 类型,例如 Color、Image 之类的,但是如果用的时默认的 transformer 的话,那么这个 non-standard 必须实现 NSCoding 协议,才能将其实例序列化为 NSData,并且进行存取。我之所以直接用默认的 transformer 也是因为 NSDictionary 实现了 NSCoding 协议的。但并不是所有的 non-standard 类型都实现了 NSCoding 协议的,要存取这些没有实现 NSCoding 协议的类型怎么办?
这个时候就轮到自定义的 transformer 出场了,上面提到过 transformer 其实就是 NSValueTransform 抽象类的实现的一个实例,而这个类的作用就是把一个值按照指定的规则转换为另一个值,所以我们可以通过实现 NSValueTransform 来指定规则,创建实例来完成我们的自定义 transformer。
关于自定义 transformer 在 Apple 开发者文档上是这样描述的:
If you specify a custom transformer, it must transform an instance of the non-standard data type into an instance of NSData and support reverse transformation.
如果只是需要单向转换的话,那么只需要实现这几个方法即可:
+ (Class) transformedValueClass;
+ (BOOL) allowsReverseTransformation;
- (id) transformedValue:(id)value;
如果还要支持反向转换的话,那么还需要实现这个方法:
- (id)reverseTransformedValue:(id)value;
由于 Core Data 的 transformable 是需要支持双向转换的(从 non-standard type 到 NSData,还有从 NSData 到 non-standard type),所以上面的方法都要实现,下面是我根据网上两个例子( SO 和 Eugene’s blog) 改造而来的一个例子:
#import <UIKit/UIKit.h>
@interface UIImageToNSDataTransformer : NSValueTransformer
{
}
@end
#import "UIImageToNSDataTransformer.h"
@implementation UIImageToNSDataTransformer
+ (Class)transformedValueClass
{
return [NSData class];
}
+ (BOOL)allowsReverseTransformation
{
return YES;
}
- (id)transformedValue:(id)value
{
if (value == nil)
return nil;
// I pass in raw data when generating the image, save that directly to the database
if ([value isKindOfClass:[NSData class]])
return value;
return UIImagePNGRepresentation((UIImage *)value);
}
- (id)reverseTransformedValue:(id)value
{
return [UIImage imageWithData:(NSData *)value];
}
@end
实现了 NSValueTransform 后,最后要做的就是在 .xcdatamodeld 的 transformable attribute 的属性页中,把这个自定义 transformer 的类名填写在 Name 文本框中。
自定义的 transformer 还可以引申出另一个应用,就是可以用于对 entity 中某个 attribute 进行加密/解密,用自定义的加密/解密算法。单独对某个 attribute 进行加密/解密,可以避免对整个 entity 甚至是整个 database 进行加密/解密,提高了性能,降低了内存消耗。这个在 SO 这个答案中有一些说明。
(注:对于 NSValueTransform 的使用,在 Mantle 库的NSValueTransformer +MTLPredefinedTransformerAdditions、MTLValueTransformer 等类中有很好地学习例子,还有 mattt 的 TransformerKit 库也是。)