Skip to content

Instantly share code, notes, and snippets.

@jpmhouston
Last active May 19, 2017 18:46
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jpmhouston/7958fceae9216f69178d4719a3492577 to your computer and use it in GitHub Desktop.
Save jpmhouston/7958fceae9216f69178d4719a3492577 to your computer and use it in GitHub Desktop.
NSManagedObject+JPHClone
//
// NSManagedObject+Clone.h
// TResActivityLog
//
// Created by Pierre Houston on 2016-05-30.
// Copyright © 2016 Resilience software. All rights reserved.
//
// When no context provided, uses [NSManagedObjectContext MR_defaultContext].
// Note, I was going to add unique prefix "jph_" to these category method names,
// but currently I've left those out.
//
#import <CoreData/CoreData.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSManagedObject (JPHCloneWithMagicalRecord)
- (instancetype)shallowClone;
- (instancetype)shallowCloneInContext:(NSManagedObjectContext *)context;
- (instancetype)deepClone;
- (instancetype)deepCloneInContext:(NSManagedObjectContext *)context;
- (instancetype)deepCloneExceptForRelationshipKeyPaths:(NSArray *)omitKeyPaths;
- (instancetype)deepCloneExceptForRelationshipKeyPaths:(NSArray *)omitKeyPaths inContext:(NSManagedObjectContext *)context;
@end
NS_ASSUME_NONNULL_END
//
// NSManagedObject+Clone.m
// TResActivityLog
//
// Created by Pierre Houston on 2016-05-30.
// Copyright © 2016 Resilience software. All rights reserved.
//
// Inspired by http://stackoverflow.com/a/2936660 & http://pastebin.com/efkji4sy
//
#import "NSManagedObject+Clone.h"
#import <MagicalRecord/MagicalRecord.h>
NS_ASSUME_NONNULL_BEGIN
@implementation NSManagedObject (CloneWithMagicalRecord)
- (instancetype)shallowClone
{
return [self cloneDeep:NO exceptForRelationshipKeyPaths:@[] omittingInverseKeyName:nil inContext:[NSManagedObjectContext MR_defaultContext]];
}
- (instancetype)deepClone
{
return [self cloneDeep:YES exceptForRelationshipKeyPaths:@[] omittingInverseKeyName:nil inContext:[NSManagedObjectContext MR_defaultContext]];
}
- (instancetype)deepCloneExceptForRelationshipKeyPaths:(NSArray *)omitKeyPaths
{
return [self cloneDeep:YES exceptForRelationshipKeyPaths:omitKeyPaths omittingInverseKeyName:nil inContext:[NSManagedObjectContext MR_defaultContext]];
}
- (instancetype)shallowCloneInContext:(NSManagedObjectContext *)context
{
return [self cloneDeep:NO exceptForRelationshipKeyPaths:@[] omittingInverseKeyName:nil inContext:context];
}
- (instancetype)deepCloneInContext:(NSManagedObjectContext *)context
{
return [self cloneDeep:YES exceptForRelationshipKeyPaths:@[] omittingInverseKeyName:nil inContext:context];
}
- (instancetype)deepCloneExceptForRelationshipKeyPaths:(NSArray *)omitKeyPaths inContext:(NSManagedObjectContext *)context
{
return [self cloneDeep:YES exceptForRelationshipKeyPaths:omitKeyPaths omittingInverseKeyName:nil inContext:context];
}
- (instancetype)cloneDeep:(BOOL)deep exceptForRelationshipKeyPaths:(NSArray *)omitKeyPaths omittingInverseKeyName:(nullable NSString *)inverseKeyName inContext:(NSManagedObjectContext *)context
{
// create new object in data store
NSManagedObject *cloned = [[self class] MR_createEntityInContext:context];
if (cloned == nil) {
return nil;
}
NSEntityDescription *entityDescription = [[self class] MR_entityDescriptionInContext:context];
// loop through all attributes and assign then to the clone
NSDictionary *attributes = [entityDescription attributesByName];
for (NSString *attr in attributes) {
[cloned setValue:[self valueForKey:attr] forKey:attr];
}
// loop through all relationships, and clone them
NSDictionary *relationships = [entityDescription relationshipsByName];
for (NSString *keyName in relationships.allKeys) {
// don't create inverse relationship
if (inverseKeyName != nil && [keyName isEqualToString:inverseKeyName]) {
continue;
}
NSRelationshipDescription *rel = relationships[keyName];
BOOL clone = deep;
// parse `omitKeyPaths` removing all except if prefixed by `keyName` - include those with `keyName` stripped
// and if `omitKeyPaths` includes `keyName` exactly then don't clone this relationship
NSMutableArray *subKeyPaths = nil;
if (omitKeyPaths != nil) {
subKeyPaths = [NSMutableArray array];
for (NSString *keyPath in omitKeyPaths) {
NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."];
if ([keyPathComponents.firstObject isEqualToString:keyName]) {
if (keyPathComponents.count == 1) {
clone = NO;
} else {
NSArray *remainingKeyPathComponents = [keyPathComponents subarrayWithRange:NSMakeRange(1, keyPathComponents.count - 1)];
[subKeyPaths addObject:[remainingKeyPathComponents componentsJoinedByString:@"."]];
}
}
}
}
// also omits the inverse relationship back to this object so as not to recursively clone self
// get a set of all objects in the relationship
if (rel.toMany && rel.ordered) {
NSMutableOrderedSet *sourceSet = [self mutableOrderedSetValueForKey:keyName];
NSMutableOrderedSet *clonedSet = [cloned mutableOrderedSetValueForKey:keyName];
if (sourceSet == nil || clonedSet == nil) {
NSLog(@"while cloning %@ %p, unable to access both source & destination relationship orderedsets, src %p dst %p", self.class, self, sourceSet, clonedSet);
}
else if (clone) {
for (NSManagedObject *relatedObject in [sourceSet reverseObjectEnumerator]) { // iterate in reverse! because found that when added in the correct order, they end up backwards!
// clone it, and add clone to set
NSManagedObject *clonedRelatedObject = [relatedObject cloneDeep:YES exceptForRelationshipKeyPaths:subKeyPaths omittingInverseKeyName:rel.inverseRelationship.name inContext:context];
if (clonedRelatedObject == nil) {
NSLog(@"while cloning %@ %p, failed to clone relationship object %@ %@. NB. partial failure handling could be improved.", self.class, self, relatedObject.class, keyName);
} else {
[clonedSet addObject:clonedRelatedObject];
}
}
}
else if (!rel.inverseRelationship || rel.inverseRelationship.toMany) { // if not cloning and reverse relationship is to-one, can't duplicate since that would voilate the to-one-ness
for (NSManagedObject *relatedObject in [sourceSet reverseObjectEnumerator]) { // iterate in reverse, see above
[clonedSet addObject:relatedObject];
}
}
}
else if (rel.toMany) {
NSMutableSet *sourceSet = [self mutableSetValueForKey:keyName];
NSMutableSet *clonedSet = [cloned mutableSetValueForKey:keyName];
if (sourceSet == nil || clonedSet == nil) {
NSLog(@"while cloning %@ %p, unable to access both source & destination relationship orderedsets, src %p dst %p", self.class, self, sourceSet, clonedSet);
}
else if (clone) {
for (NSManagedObject *relatedObject in sourceSet) {
// clone it, and add clone to set
NSManagedObject *clonedRelatedObject = [relatedObject cloneDeep:YES exceptForRelationshipKeyPaths:subKeyPaths omittingInverseKeyName:rel.inverseRelationship.name inContext:context];
if (clonedRelatedObject == nil) {
NSLog(@"while cloning %@ %p, failed to clone relationship object %@ %@. NB. partial failure handling could be improved.", self.class, self, relatedObject.class, keyName);
} else {
[clonedSet addObject:clonedRelatedObject];
}
}
}
else if (!rel.inverseRelationship || rel.inverseRelationship.toMany) { // if not cloning and reverse relationship is to-one, can't duplicate since that would voilate the to-one-ness
[clonedSet unionSet:sourceSet];
}
}
else {
NSManagedObject *sourceObject = (NSManagedObject *)[self valueForKey:keyName];
if (sourceObject == nil) {
NSLog(@"while cloning %@ %p, unable to access source relationship object, %p", self.class, self, sourceObject);
}
else if (clone) {
NSManagedObject *clonedObject = [sourceObject cloneDeep:YES exceptForRelationshipKeyPaths:subKeyPaths omittingInverseKeyName:rel.inverseRelationship.name inContext:context];
if (clonedObject == nil) {
NSLog(@"while cloning %@ %p, failed to clone relationship object %@ %@. NB. partial failure handling could be improved.", self.class, self, sourceObject.class, keyName);
} else {
[cloned setValue:clonedObject forKey:keyName];
}
}
else if (!rel.inverseRelationship || rel.inverseRelationship.toMany) { // if not cloning and reverse relationship is to-one, can't duplicate since that would voilate the to-one-ness
[cloned setValue:sourceObject forKey:keyName];
}
}
}
return cloned;
}
@end
NS_ASSUME_NONNULL_END
@mbaumard
Copy link

#import "NSManagedObject+Clone.h"

#import "NSManagedObject+JPHClone.h"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment