38

I am trying to create a deep-copy of a NSMutableDictionary and assign it to another NSMutableDictionary. The dictionary contains a bunch of arrays, each array containing names, and the key is an alphabet (the first letter of those names). So one entry in the dictionary is 'A' -> 'Adam', 'Apple'. Here's what I saw in a book, but I'm not sure if it works:

- (NSMutableDictionary *) mutableDeepCopy
{
    NSMutableDictionary * ret = [[NSMutableDictionary alloc] initWithCapacity: [self count]];
    NSArray *keys = [self allKeys];

    for (id key in keys)
    {
        id oneValue = [self valueForKey:key]; // should return the array
        id oneCopy = nil;

        if ([oneValue respondsToSelector: @selector(mutableDeepCopy)])
        {
            oneCopy = [oneValue mutableDeepCopy];
        }
        if ([oneValue respondsToSelector:@selector(mutableCopy)])
        {
            oneCopy = [oneValue mutableCopy];
        }

        if (oneCopy == nil) // not sure if this is needed
        {   
            oneCopy = [oneValue copy];
        }
        [ret setValue:oneCopy forKey:key];

        //[oneCopy release];
    }
    return ret;
}
  • should the [onecopy release] be there or not?
  • Here's how I'm going to call this method:

    self.namesForAlphabets = [self.allNames mutableDeepCopy];

Will that be ok? Or will it cause a leak? (assume that I declare self.namesForAlphabets as a property, and release it in dealloc).

Z S
  • 7,039
  • 12
  • 53
  • 105
  • There is nothing "deep" in the structure you describe (a simple address-book style list of names by their first letter) -- so why you even attempt a "deep copy" ? what do you need this for? – Motti Shneor Jan 27 '22 at 10:15

8 Answers8

77

Because of toll-free bridging, you can also use the CoreFoundation function CFPropertyListCreateDeepCopy:

NSMutableDictionary *mutableCopy = (NSMutableDictionary *)CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)originalDictionary, kCFPropertyListMutableContainers);
Wevah
  • 28,182
  • 7
  • 83
  • 72
  • Cool. This works great. Except, for some reason it registers as a leak in performance tool. Any idea what that's about? – Jonah Aug 25 '10 at 13:54
  • 2
    It follows Core Foundation's "create" rule, so you need to make sure you release or autorelease the returned dictionary (or just not retain it if you want to keep it around). – Wevah Aug 25 '10 at 16:01
  • So it seems you'd have to call: CFRelease(mutableCopy); when you need to release the object. Thanks! – Jonah Aug 25 '10 at 23:40
  • I think you want to cast the NSDictionary as a CFDictionaryRef, not as CFDictionary, in the 2nd argument to CFPropertyListCreateDeepCopy. – Eric G Aug 18 '11 at 03:55
  • 13
    this only works if there are only property-list-values though. if it encounters anything else it just returns NULL – Ahti Nov 30 '11 at 15:32
  • `NSNull` isn't a valid property list value, unfortunately. – Wevah Dec 11 '13 at 02:08
  • 4
    I think you have to use CFBridgeRelease. For an array it is, mutableArray = (NSMutableArray *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFArrayRef)oldArrat, kCFPropertyListMutableContainers)); – honkskillet Aug 29 '14 at 09:13
  • This didn't work for me. Looks like there are more details you have to be careful when using it. – Alex Cio Feb 10 '15 at 14:59
  • 1
    It only works if all the elements are property list objects (strings, arrays, dictionaries, numbers, dates, data). Do you have any other objects in your dictionary? – Wevah Feb 11 '15 at 17:54
  • Wonderful. That's what I was looking for - I'm trying to use KVC to modify lower-level items in [NSUserDefaults standardDefaults]. The hierarchy retrieved is all immutable, and techniques involved with extending NSDictionary and NSArray don't work - since the underlying objects seem to be all of the _NSCFDictionary set of apple private classes. They will hopefully yield to CFPropertyListCreateDeepCopy better. – Motti Shneor Aug 21 '19 at 18:56
12

Assuming all elements of the array implement the NSCoding protocol, you can do deep copies via archiving because archiving will preserve the mutability of objects.

Something like this:

id DeepCopyViaArchiving(id<NSCoding> anObject)
{
    NSData* archivedData = [NSKeyedArchiver archivedDataWithRootObject:anObject];
    return [[NSKeyedUnarchiver unarchiveObjectWithData:archivedData] retain];
}

This isn't particularly efficient, though.

Tom Dalling
  • 23,305
  • 6
  • 62
  • 80
  • Does this method return a mutable deep copy or an immutable deep copy? – dreamlax Dec 23 '09 at 03:04
  • 3
    Whatever you put into it. If you put in an NSMutableArray, you get back an NSMutableArray. – Tom Dalling Dec 23 '09 at 04:28
  • @dreamlax if not, you could just create a mutable version of it with `mutableCopy` – Alex Cio Feb 10 '15 at 14:57
  • 1
    @亚历山大: That would just create a mutable "top-level" but any nested container objects would still be immutable. – dreamlax Feb 12 '15 at 02:35
  • Ok, sorry your right. In my case, I didn't have any nested containers which needed to be mutable. – Alex Cio Feb 16 '15 at 11:42
  • That won't make anything Immutable - mutable, which is the purpose of the question. – Motti Shneor Aug 21 '19 at 18:42
  • Apple recommends this way for creating "real deep and all-level mutable copies" in their old documentation. Lower levels WILL be mutable/immutable exactly as the original, because there is no copying per se - there is serializing and de-serializing which preserves that original classes used. Docs: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Collections/Articles/Copying.html – Motti Shneor Jan 27 '22 at 10:25
10

IMPORTANT: The question (and my code below) both deal with a very specific case, in which the NSMutableDictionary contains only arrays of strings. These solutions will not work for more complex examples. For more general case solutions, see the following:


Answer for this specific case:

Your code should work, but you will definitely need the [oneCopy release]. The new dictionary will retain the copied objects when you add them with setValue:forKey, so if you do not call [oneCopy release], all of those objects will be retained twice.

A good rule of thumb: if you alloc, retain or copy something, you must also release it.

Note: here is some sample code that would work for certain cases only. This works because your NSMutableDictionary contains only arrays of strings (no further deep copying required):

- (NSMutableDictionary *)mutableDeepCopy
{
    NSMutableDictionary * ret = [[NSMutableDictionary alloc]
                                  initWithCapacity:[self count]];

    NSMutableArray * array;

    for (id key in [self allKeys])
    {
        array = [(NSArray *)[self objectForKey:key] mutableCopy];
        [ret setValue:array forKey:key];
        [array release];
    }

    return ret;
}
Community
  • 1
  • 1
e.James
  • 116,942
  • 41
  • 177
  • 214
  • 1
    Thanks, but the "copy" makes the NSArray (or NSMutableArray) immutable in the new dictionary. So that's not going to work. – Z S Dec 23 '09 at 04:54
  • 3
    replaced 'copy' with 'mutableCopy' and it's fine. – Z S Dec 23 '09 at 05:25
  • Ah, yes. Sorry! That should definitely have been `mutableCopy`. I made the change in my answer. – e.James Dec 23 '09 at 14:59
  • 1
    There is also a typo in the middle of `for` loop - replace `setValue:copy` with `setValue:array` – Nik May 09 '11 at 22:44
  • @e.James do we need to have NSCopying for this function to work? I am getting error **-[MyClass copyWithZone:]: unrecognized selector sent to instance** – Krishnabhadra Jul 18 '11 at 11:29
  • 1
    @Krishnabhadra: Yes, you will definitely need to implement `NSCopying` if you are using custom objects. The original question dealt only with `NSArray` objects. – e.James Jul 19 '11 at 16:33
  • @hfossli: as stated in the question, the NSDictionary contains only arrays of strings, so there is no need to worry about deeper levels. However, I can see that some visitors might not realize that this solution only works in special cases. I will attempt to make it more clear. – e.James Jan 28 '14 at 20:52
10

Another technique that I have seen (which is not at all very efficient) is to use an NSPropertyListSerialization object to serialise your dictionary, then you de-serialise it but specify that you want mutable leaves and containers.


NSString *errorString = nil;
NSData *binData = 
  [NSPropertyListSerialization dataFromPropertyList:self.allNames
                                             format:NSPropertyListBinaryFormat_v1_0
                                        errorString:&errorString];

if (errorString) {
    // Something bad happened
    [errorString release];
}

self.namesForAlphabets = 
 [NSPropertyListSerialization propertyListFromData:binData
                                  mutabilityOption:NSPropertyListMutableContainersAndLeaves
                                            format:NULL
                                  errorDescription:&errorString];

if (errorString) {
    // something bad happened
    [errorString release];
}

Again, this is not at all efficient.

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
dreamlax
  • 93,976
  • 29
  • 161
  • 209
5

Trying to figure out by checking respondToSelector(@selector(mutableCopy)) won't give the desired results as all NSObject-based objects respond to this selector (it's part of NSObject). Instead we have to query if an object conforms to NSMutableCopying or at least NSCopying. Here's my answer based on this gist mentioned in the accepted answer:

For NSDictionary:

@implementation NSDictionary (MutableDeepCopy)

//  As seen here (in the comments): https://gist.github.com/yfujiki/1664847
- (NSMutableDictionary *)mutableDeepCopy
{
    NSMutableDictionary *returnDict = [[NSMutableDictionary alloc] initWithCapacity:self.count];

    NSArray *keys = [self allKeys];

    for(id key in keys) {
        id oneValue = [self objectForKey:key];
        id oneCopy = nil;

        if([oneValue respondsToSelector:@selector(mutableDeepCopy)]) {
            oneCopy = [oneValue mutableDeepCopy];
        } else if([oneValue conformsToProtocol:@protocol(NSMutableCopying)]) {
            oneCopy = [oneValue mutableCopy];
        } else if([oneValue conformsToProtocol:@protocol(NSCopying)]){
            oneCopy = [oneValue copy];
        } else {
            oneCopy = oneValue;
        }

        [returnDict setValue:oneCopy forKey:key];
    }

    return returnDict;
}

@end

For NSArray:

@implementation NSArray (MutableDeepCopy)

- (NSMutableArray *)mutableDeepCopy
{
    NSMutableArray *returnArray = [[NSMutableArray alloc] initWithCapacity:self.count];

    for(id oneValue in self) {
        id oneCopy = nil;

        if([oneValue respondsToSelector:@selector(mutableDeepCopy)]) {
            oneCopy = [oneValue mutableDeepCopy];
        } else if([oneValue conformsToProtocol:@protocol(NSMutableCopying)]) {
            oneCopy = [oneValue mutableCopy];
        } else if([oneValue conformsToProtocol:@protocol(NSCopying)]){
            oneCopy = [oneValue copy];
        } else {
            oneCopy = oneValue;
        }

        [returnArray addObject:oneCopy];
    }

    return returnArray;
}

@end

Both methods have the same internal to-copy-or-not-to-copy logic and that could be extracted into a separate method but I left it like this for clarity.

ierceg
  • 418
  • 1
  • 8
  • 11
  • I thought this beautiful, Tried to use the technique, and it seemed to work... until it didn't. I need to use KVC 'setValue:forKeyPath:' to modify lower-level items in a dictionary retrieved from '[NSUserDefaults standardDefaults]' - which always returns immutable objects (all the way down their hierarchy). The technique failed, because the NSDictionaries/NSArrays retrieved - aren't really NSDictionaries. They are bridged _NSCFDictionary objects - which don't conform to either your 'mutableDeepCopy' protocol, nor to 'NSMutableCopying' protocol. So the code breaks. I'm still looking for a cure. – Motti Shneor Aug 21 '19 at 18:46
2

For ARC - note kCFPropertyListMutableContainersAndLeaves for truly deep mutability.

    NSMutableDictionary* mutableDict = (NSMutableDictionary *)
      CFBridgingRelease(
          CFPropertyListCreateDeepCopy(kCFAllocatorDefault, 
           (CFDictionaryRef)someNSDict, 
           kCFPropertyListMutableContainersAndLeaves));
Tom Andersen
  • 7,132
  • 3
  • 38
  • 55
1

Thought I'd update with an answer if you're using ARC.

The solution Weva has provided works just fine. Nowadays you could do it like this:

NSMutableDictionary *mutableCopy = (NSMutableDictionary *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)originalDict, kCFPropertyListMutableContainers));
johan
  • 6,578
  • 6
  • 46
  • 68
0

Useful answers here, but CFPropertyListCreateDeepCopy doesn't handle [NSNull null] in the data, which is pretty normal with JSON decoded data, for example.

I'm using this category:

    #import <Foundation/Foundation.h>

    @interface NSObject (ATMutableDeepCopy)
    - (id)mutableDeepCopy;
    @end

Implementation (feel free to alter / extend):

    @implementation NSObject (ATMutableDeepCopy)

    - (id)mutableDeepCopy
    {
        return [self copy];
    }

    @end

    #pragma mark - NSDictionary

    @implementation NSDictionary (ATMutableDeepCopy)

    - (id)mutableDeepCopy
    {
        return [NSMutableDictionary dictionaryWithObjects:self.allValues.mutableDeepCopy
                                                  forKeys:self.allKeys.mutableDeepCopy];
    }

    @end

    #pragma mark - NSArray

    @implementation NSArray (ATMutableDeepCopy)

    - (id)mutableDeepCopy
    {
        NSMutableArray *const mutableDeepCopy = [NSMutableArray new];
        for (id object in self) {
            [mutableDeepCopy addObject:[object mutableDeepCopy]];
        }

        return mutableDeepCopy;
    }

    @end

    #pragma mark - NSNull

    @implementation NSNull (ATMutableDeepCopy)

    - (id)mutableDeepCopy
    {
        return self;
    }

    @end

Example extensions – strings are left as normal copies. You could override this if you want to be able to in place edit them. I only needed to monkey with a deep down dictionary for some testing, so I've not implemented that.

Benjohn
  • 13,228
  • 9
  • 65
  • 127