Mutable Transformable Properties in Core Data

Previously I've written about my efforts to keep my Core Data models from becoming overly complex by using fewer entities and storing Plain Old Objective-C Objects or POOCO's (that's an awful name) in transformable properties.

If you're going to store mutable values in transformable properties, things can get tricky. Here's how to do it.

The Easy Way

The easiest (and probably best) way is to use immutable objects. Following the pattern from foundation, if your value objects are immutable or have both mutable and immutable variants, always store immutable values and simply replace existing values with entirely ones.

This is the technique I've used for storing data received from the server that the client does not change; I create a new NSArray instance and store it in the property replacing what was there before. This works well for my case since I always want what the server has and it logically belongs to the entity it's stored in.

If you have custom classes you store data into and not just NSArray, NSDictionary and so on, you may want to consider implementing them as immutable objects, generating new ones on each change much like NSString does.

The Slightly Less Easy Way

If you do try to store mutable objects in a transformable property it will work, but probably not behave the way you expect it to. The thing to be aware of is that Core Data keeps track of what needs to get saved using KVO. In order for your transformable property to get updated on save the property itself needs to be seen to have been changed by Core Data.

Let's say you put an NSMutableArray instance into a transformable property, add one or more other objects into that array and then save the entity. Core Data will save everything you added to the array and will do this because when you initially set it on the transformable property that property was marked as dirty. On save that property will get serialized.

entity.tags = [NSMutableArray array];
[entity.tags addObject:@"red"];
[entity.tags addObject:@"blue"];
[entity.managedObjectContext save:nil];

Now, let's add a few more objects to that same array and save the entity.

[entity.tags addObject:@"green"];
[entity.managedObjectContext save:nil];

It will appear this worked; checking the entity's tag array will show red, blue and green. However, when the entity is reloaded from the store, it will only have red and blue in its tags array; green will be missing.

Why is this? Because we changed the contents of the array in a way that Core Data can't see. It is watching for the tags property to be set to a new value, but we didn't do that. It's still the same array so from Core Data's perspective there's nothing to save. In order to get changes to the contents of the array to save the property needs to get assigned a new value; we need to create a new array and set it back to the property.

NSMutableArray *tags = [NSMutableArray arrayWithArray:entity.tags];
[tags addObject:@"green"];
entity.tags = tags;

This is horrible code and you're just asking for nasty bugs if pepper this throughout your app. If you go this way, encapsulate it in the model. For example, change the tags property to something like tagsArray on the Core Data model and make it private. Expose a public accessor that will lazily recreate the array.

// Entity.h
@interface Entity : NSManagedObject
@property (nonatomic, readonly) NSMutableArray *tags;
@end
// Entity.m
#import "Entity.h"
@interface Entity () {
    NSMutableArray *_tags;
}
@property (nonatomic, strong) NSArray *tagsArray;
@end

@implementation Entity
@dynamic tagsArray;
- (NSMutableArray *)tags {
    if (!_tags) {
            _tags = [NSMutableArray arrayWithArray:self.tagsArray];
            self.tagsArray = _tags;
        }
        return _tags;
}
@end

Now we can safely reload an entity, add or remove items from it's tags property and be sure that when we save it our changes will also be saved.

Warning About Background Updates

Be aware that if you update the entity in another context (e.g. a background update), you may get surprising behavior. If the background update changes the transformable property it will effectively "override" this scheme. Your wrapping array will not see the changes from the other context and that change will take precedence on save.

If you want to handle this case, use KVO to observe a change to the hidden "shadow" property (tagsArray in the example) and handle it in whatever way makes sense to you; replace the contents of the wrapping array, merge them, throw up an alert, etc.

The Very Hard Way

The correct way to do this is to take over Core Data's responsibilities for the transformable property. Namely, setup KVO handlers for all changes to stored objects like arrays and sets and all of the objects stored in them. You can then inform Core Data that the property has been changed.

However, if you find yourself in this situation, you're probably better off letting Core Data handle it and not using a transformable property to hold the objects.

Home