how to do true deep copy for NSArray and NSDictionary with have nested arrays/dictionary?
Asked Answered
I

4

22

Question: Is there a way to use existing objective-c methods to do a full deep copy of a NSDictionary or NSArray, that themselves have nested dictionaries or arrays within them?

That is I have read the problem may be when it hits a nested dictionary or array it only copies the pointer to the nested item, and not copy the item truely.

Background: So as an example for me I'm trying to load/save the following config with NSUserDefaults and when loading need to convert the immutable copies one gets from NSUserDefault to mutable prior to making changes.

  • Items (NSDictionary)
    • Item (NSDictionary)
      • aString: NSString
      • aString2: NSString
      • aDate: NSDate
      • aDate2: NSDate
      • aBool: BOOL
      • aTI1: NSTimeInterval
      • aTI2: NSTimeInterval
      • Keywords (NSArray)
        • keyword: NSString
        • keyword: NSString
Illuviation answered 28/3, 2011 at 0:20 Comment(0)
G
36

A couple of years ago, I wrote a few category methods for exactly the same reason, transforming a whole tree of user defaults to mutable. Here they are - use them at your own risk! :-)

//
//  SPDeepCopy.h
//
//  Created by Sherm Pendley on 3/15/09.
//

#import <Cocoa/Cocoa.h>

// Deep -copy and -mutableCopy methods for NSArray and NSDictionary

@interface NSArray (SPDeepCopy)

- (NSArray*) deepCopy;
- (NSMutableArray*) mutableDeepCopy;

@end

@interface NSDictionary (SPDeepCopy)

- (NSDictionary*) deepCopy;
- (NSMutableDictionary*) mutableDeepCopy;

@end

//
//  SPDeepCopy.m
//
//  Created by Sherm Pendley on 3/15/09.
//

#import "SPDeepCopy.h"


@implementation NSArray (SPDeepCopy)

- (NSArray*) deepCopy {
    unsigned int count = [self count];
    id cArray[count];

    for (unsigned int i = 0; i < count; ++i) {
        id obj = [self objectAtIndex:i];
        if ([obj respondsToSelector:@selector(deepCopy)])
            cArray[i] = [obj deepCopy];
        else
            cArray[i] = [obj copy];
    }

    NSArray *ret = [[NSArray arrayWithObjects:cArray count:count] retain];

    // The newly-created array retained these, so now we need to balance the above copies
    for (unsigned int i = 0; i < count; ++i)
        [cArray[i] release];

    return ret;
}
- (NSMutableArray*) mutableDeepCopy {
    unsigned int count = [self count];
    id cArray[count];

    for (unsigned int i = 0; i < count; ++i) {
        id obj = [self objectAtIndex:i];

        // Try to do a deep mutable copy, if this object supports it
        if ([obj respondsToSelector:@selector(mutableDeepCopy)])
            cArray[i] = [obj mutableDeepCopy];

        // Then try a shallow mutable copy, if the object supports that
        else if ([obj respondsToSelector:@selector(mutableCopyWithZone:)])
            cArray[i] = [obj mutableCopy];

        // Next try to do a deep copy
        else if ([obj respondsToSelector:@selector(deepCopy)])
            cArray[i] = [obj deepCopy];

        // If all else fails, fall back to an ordinary copy
        else
            cArray[i] = [obj copy];
    }

    NSMutableArray *ret = [[NSMutableArray arrayWithObjects:cArray count:count] retain];

    // The newly-created array retained these, so now we need to balance the above copies
    for (unsigned int i = 0; i < count; ++i)
        [cArray[i] release];

    return ret;
}

@end

@implementation NSDictionary (SPDeepCopy)

- (NSDictionary*) deepCopy {
    unsigned int count = [self count];
    id cObjects[count];
    id cKeys[count];

    NSEnumerator *e = [self keyEnumerator];
    unsigned int i = 0;
    id thisKey;
    while ((thisKey = [e nextObject]) != nil) {
        id obj = [self objectForKey:thisKey];

        if ([obj respondsToSelector:@selector(deepCopy)])
            cObjects[i] = [obj deepCopy];
        else
            cObjects[i] = [obj copy];

        if ([thisKey respondsToSelector:@selector(deepCopy)])
            cKeys[i] = [thisKey deepCopy];
        else
            cKeys[i] = [thisKey copy];

        ++i;
    }

    NSDictionary *ret = [[NSDictionary dictionaryWithObjects:cObjects forKeys:cKeys count:count] retain];

    // The newly-created dictionary retained these, so now we need to balance the above copies
    for (unsigned int i = 0; i < count; ++i) {
        [cObjects[i] release];
        [cKeys[i] release];
    }

    return ret;
}
- (NSMutableDictionary*) mutableDeepCopy {
    unsigned int count = [self count];
    id cObjects[count];
    id cKeys[count];

    NSEnumerator *e = [self keyEnumerator];
    unsigned int i = 0;
    id thisKey;
    while ((thisKey = [e nextObject]) != nil) {
        id obj = [self objectForKey:thisKey];

        // Try to do a deep mutable copy, if this object supports it
        if ([obj respondsToSelector:@selector(mutableDeepCopy)])
            cObjects[i] = [obj mutableDeepCopy];

        // Then try a shallow mutable copy, if the object supports that
        else if ([obj respondsToSelector:@selector(mutableCopyWithZone:)])
            cObjects[i] = [obj mutableCopy];

        // Next try to do a deep copy
        else if ([obj respondsToSelector:@selector(deepCopy)])
            cObjects[i] = [obj deepCopy];

        // If all else fails, fall back to an ordinary copy
        else
            cObjects[i] = [obj copy];

        // I don't think mutable keys make much sense, so just do an ordinary copy
        if ([thisKey respondsToSelector:@selector(deepCopy)])
            cKeys[i] = [thisKey deepCopy];
        else
            cKeys[i] = [thisKey copy];

        ++i;
    }

    NSMutableDictionary *ret = [[NSMutableDictionary dictionaryWithObjects:cObjects forKeys:cKeys count:count] retain];

    // The newly-created dictionary retained these, so now we need to balance the above copies
    for (unsigned int i = 0; i < count; ++i) {
        [cObjects[i] release];
        [cKeys[i] release];
    }

    return ret;
}

@end
Grabowski answered 28/3, 2011 at 0:43 Comment(6)
excellent Sherm thanks - so there's nothing that Apple has that does this for you obviously I guess then? one would wonder if this was the case whether there might be some popular open source library that captures such a useful method?Illuviation
No, Apple doesn't supply anything like that. There may be an FOSS library that includes it, but to be honest I didn't look very hard, since this took all of an hour to write anyway.Grabowski
Just a note, for iOS, the #import statement in the .h file needs to be #import <Foundation/Foundation.h>Seagraves
If I use mutableDeepCopy for a dictionary, do I have to release it myself? Also, does it work with very deep dictionaries? (Like a dictionary within an array within another dictionary within yet another dictionary... etc...)Quincentenary
@Voldermord Find SPDeepCopy.m in Compile Sources in the Build Phases section of your Project Target and add "-fno-objc-arc" as the compiler tags and then clean and build. You can copy his code verbatim this way.Martinmartina
Actually Apple DOES provide such deep copies, only you need to go to the underlying CoreFoundation APIs for that. see CFPropertyListCreateDeepCopy() and friends.Catholic
M
33

I think something like this should work.

NSData *buffer;
NSMutableDictionary *_dict1, *_dict2;

// Deep copy "all" objects in _dict1 pointers and all to _dict2  
buffer = [NSKeyedArchiver archivedDataWithRootObject: _dict1];  
_dict2 = [NSKeyedUnarchiver unarchiveObjectWithData: buffer];  
Metalinguistic answered 9/8, 2012 at 4:32 Comment(2)
Genius.. Especially since I already have the encoder/decoder methods implemented for all the custom objects.Unimproved
Apple even recommends this method in official docs: developer.apple.com/library/content/documentation/Cocoa/…Chandrachandragupta
C
25

The easy way for a DEEP copy is to simply use CFPropertyListCreateDeepCopy. Here is example (with ARC):

    NSDictionary *newDictionary =
 (__bridge NSDictionary *)(CFPropertyListCreateDeepCopy(kCFAllocatorDefault,
         (__bridge CFPropertyListRef)(originalDictionary),
          kCFPropertyListImmutable));

my originalDictionary in this example is an NSDictionary

CFPropertyListRef can simply be cast to NSDictionary as long as the top level entity of the CFPropertyListRef is a CFDictionary.

Clean answered 19/12, 2012 at 20:0 Comment(3)
This didn't work for me. I also found some other code snippets which looked nearly the same just switched with the CFProperties which are available. Looks to me like you have to be careful on different things when using thisRondelet
see the ARC solution here: #1950861Ratify
This is very nice and useful to know about, but pay attention this only works on PropertyList hierarchies - that are very limited in the types of objects stored, and won't accept any custom classes at all.Catholic
T
1

If someone want the mutable copy then please use below code:

NSMutableDictionary *newDictionary = (NSMutableDictionary *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)dict, kCFPropertyListMutableContainers));

Tripletail answered 30/5, 2019 at 15:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.