2
\$\begingroup\$

I’m working in Objective-C, SKSpriteKit and am trying to create a class which will work in both OS X and iOS for flexibility.

I have just created a new protocol for my programs: <mutableDeepCopying>. I’ve implemented this protocol into NSArray and NSDictionary using categories. The MutableDeepCopying.h file is pasted below.

As I’m new to protocols and categories, I’m not sure if I’ve implemented it absolutely correctly (after a few tests with NSLog, it does seem to work with 2D NSArray so far, though…).

The points that I’m unsure about are the following:

  1. I’ve named the protocol <mutableDeepCopying> rather than <NSMutableDeepCopying>. Is it best to avoid the prefix NS, or would it be ok to change to <NSMutableDeepCopying> for consistency?

  2. In my protocol should I bother with the (id _Nonnull) and (nullable NSZone *) like the <NSCopying> protocol does for consistency?

    @protocol mutableDeepCopy
    - (id _Nonnull)mutableDeepCopyWithZone:(nullable NSZone *)zone;
    @end
    
  3. In the NSArray and NSDictionary categories I have implemented the mutableDeepCopyWithZone: method which uses an enumeration block to cycle through all of the objects. Is this the most efficient way to copy them?

    Also, for the NSArray, I’ve had to first create a dummy array which consists of NSNulls. Do I really need to do this, or is there a way to get around this if I am to stick with an enumeration block?

  4. With the standard <NSCopying> protocol (and <NSMutableCopying> as well), there are 2 related methods which are:

    - (id)copy;
    - (id)copyWithZone:(NSZone *)zone;
    

    I have implemented their parallels:

    - (id)mutableDeepCopy;
    - (id)mutableDeepCopyWithZone:(NSZone *)zone;
    

    The mutableDeepCopy: method should call the mutableDeepCopyWithZone: method if I’m not mistaken… but I had no idea what to put in the zone parameter. So as you can see in my code, I just created a new NSZone and put it there.

    It seems to have worked in my program, but I’m not sure if the program (or any others I create using this method) will break down at some point because I haven’t created an NSZone large enough, or something like that. Is this what I’m supposed to do? And if it is, why do Apple bother with 2 methods when you can just simply type all of the code in copyWithZone: into copy:?

  5. For the mutableDeepCopyWithZone: method, the block uses the following checklist for each object to see what it should do to it. It checks if the object can perform the selector, and then performs it on the object if it can:

    a) mutableDeepCopyWithZone:

    b) mutableCopyWithZone:

    c) copyWithZone:

    d) If none of the above work, the object is encoded and then decoded (archived then unarchived).

    The question is: Do all objects (Foundation or SKSpriteKit objects) fall into one of the bottom 2 categories — do they all follow the <NSCopying> or <NSCoding> protocols? If not all of them do… then what should I do to ensure everything is copied properly?

I plan to create a <deepCopying> protocol next, so I’d appreciate it if someone could correct any mistakes I made with my first attempt at a protocol, as I will be following the same format…

  1. Also, would it be wise to implement the <mutableDeepCopying> (and in future, <deepCopying>) into other container classes too, like NSSet, or is there no point bothering with classes I don’t use? (the differentiation between shallow/deep copying only occurs for container classes, right?)

  2. If I hadn’t implemented this protocol, there wouldn’t have been any other general/generic way to implement mutable deep copying, right? (Actually, now that I think about it, encoding/decoding then making a mutable copy would’ve worked too…)

The MutableDeepCopying.h file:

#import <Foundation/NSObject.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>

@protocol mutableDeepCopy
- (id)mutableDeepCopyWithZone:(NSZone *)zone;
@end


@interface NSArray (mutableDeepCopy) <mutableDeepCopy>

- (id)mutableDeepCopy;

@end
@implementation NSArray (mutableDeepCopying)

- (id)mutableDeepCopy {

    NSZone *zone;

    return [self mutableDeepCopyWithZone:zone];
}
- (id)mutableDeepCopyWithZone:(NSZone *)zone {

    NSMutableArray *arrayMutableDeepCopy = [NSMutableArray arrayWithCapacity:self.count];

    for (int i = 0; i < self.count; i++) {
        [arrayMutableDeepCopy addObject:[NSNull null]];
    }

    [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        if ([obj respondsToSelector:@selector(mutableDeepCopyWithZone:)]) {
            arrayMutableDeepCopy[idx] = [obj mutableDeepCopyWithZone:zone];
        }
        else if ([obj respondsToSelector:@selector(mutableCopyWithZone:)])
        {
            arrayMutableDeepCopy[idx] = [obj mutableCopyWithZone:zone];
        }
        else if ([obj respondsToSelector:@selector(copyWithZone:)]) {
            arrayMutableDeepCopy[idx] = [obj copyWithZone:zone];
        }
        else {
            arrayMutableDeepCopy[idx] = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:obj]];
        }
    }
     ];
    return arrayMutableDeepCopy;
}
@end // NSArray


@interface NSDictionary (mutableDeepCopy) <mutableDeepCopy>

- (id)mutableDeepCopy;

@end
@implementation NSDictionary (mutableDeepCopy)

- (id)mutableDeepCopy {

    NSZone *zone;

    return [self mutableDeepCopyWithZone:zone];
}
- (id)mutableDeepCopyWithZone:(NSZone *)zone {

    NSMutableDictionary *dictionaryMutableDeepCopy = [NSMutableDictionary dictionaryWithCapacity:self.count];

    [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        if ([obj respondsToSelector:@selector(mutableDeepCopyWithZone:)]) {
            [dictionaryMutableDeepCopy setObject:[obj mutableDeepCopyWithZone:zone] forKey:key];
        }
        else if ([obj respondsToSelector:@selector(mutableCopyWithZone:)])
        {
            [dictionaryMutableDeepCopy setObject:[obj mutableCopyWithZone:zone] forKey:key];
        }
        else if ([obj respondsToSelector:@selector(copyWithZone:)]) {
            [dictionaryMutableDeepCopy setObject:[obj copyWithZone:zone] forKey:key];
        }
        else {
            [dictionaryMutableDeepCopy setObject:[NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:obj]] forKey:key];
        }
    }
     ];
    return dictionaryMutableDeepCopy;
}
@end // NSDictionary
\$\endgroup\$
3
\$\begingroup\$

Is it best to avoid the prefix NS,

Yes.

or would it be ok to change to <NSMutableDeepCopying> for consistency?

No. You misunderstand what prefixes are for. Objective-C does not have namespaces. Everything that's not file scoped or a member of a class is global. Every class and protocol is a global name, available anywhere in the program. Prefixes are used as pseudo-namespaces, to prevent name collisions.

You should use a prefix on names you create, and you must not use someone else's prefix. NS is Apple's prefix; they also reserve all two-letter prefixes for any program using Cocoa (Touch). You must choose your own three-letter prefix and apply it to any name that has any chance of conflicting with code written by someone else: if you were writing a framework, for example, to be included in other programs. You can get away with not prefixing names that are only used inside your one program, but it is not good Cocoa/ObjC practice.

Even more importantly for your code, any method you add to a framework class via a category must must have your prefix. If you choose the same name as an already-existing method on a class, even a method you don't know about, you will replace that method for every instance of the class in your program, including instances you don't create. Something between hilarity and pandemonium will result. The method is clobbered: you can't even call the original implementation. (Try implementing addSubview: to print to the log and do nothing else in a category on UIView in an iOS app, and see what happens.)

Also, for the NSArray, I’ve had to first create a dummy array which consists of NSNulls. Do I really need to do this, or is there a way to get around this if I am to stick with an enumeration block?

You should almost never need to pre-fill an NSMutableArray. It will expand upon demand. (arrayWithCapacity: does not -- last I checked -- even actually reserve memory. It's a hint to the reader, nothing more.) If for some reason you needed to assign to it out-of-order, you could do what you have done here.

It's not necessary at all in your case, however: moving sequentially through the original array, you are simply adding on to the end of the new array. Both addObject: and setObject:atIndexedSubscript: handle this just fine. The latter is the method that is called when you use the subscripting syntactic sugar like you've done. The method's documentation says "If the index is equal to count, the element is added to the end of the array, growing the array."

The mutableDeepCopy: method should call the mutableDeepCopyWithZone: method if I’m not mistaken… but I had no idea what to put in the zone parameter. So as you can see in my code, I just created a new NSZone and put it there.

This is unnecessary. NSZone is obsolete. Its existence in the NSCopying protocol is a historical artifact, and you do not need to emulate it. Simply declare -mutableDeepCopy. (Note that, under ARC, the NSZone that you created and did not initialize is nil. Under MRR, it is garbage, which, since it isn't used at all, isn't significant, but it's still not a great idea.)

The question is: Do all objects (Foundation or SKSpriteKit objects) fall into one of the bottom 2 categories — do they all follow the or protocols?

Not quite all, no. Here's two that don't, for example: NSMetadataQuery and SKPhysicsContact.

If not all of them do… then what should I do to ensure everything is copied properly?

I'm not sure that you can. The best you can do is trust any object that holds onto one of those non-copyable objects to know what to do when it is itself copied.

Also, would it be wise to implement the <mutableDeepCopying> (and in future, <deepCopying>) into other container classes too, like NSSet, or is there no point bothering with classes I don’t use? (the differentiation between shallow/deep copying only occurs for container classes, right?)

This only affects your one program that contains this code, so do what you need. If you don't need to deep copy NSSet, then don't bother. (If you do, remember my warning above about prefixing.)

If I hadn’t implemented this protocol, there wouldn’t have been any other general/generic way to implement mutable deep copying, right? (Actually, now that I think about it, encoding/decoding then making a mutable copy would’ve worked too…)

Encoding and then decoding is the standard Cocoa way to copy an object graph, whether that graph is contained in an collection class or not.

There's some further reading you can do on SO about deep copying that you might find fruitful.

\$\endgroup\$
  • \$\begingroup\$ Thank you for the great answer! There are a few things I would like to just clarify: The reason I used a block to loop through the collection of objects was that I thought the block would loop through the collection simultaneously (so by the time object 2 is copied, object 1 might not be done yet) rather than 1 by 1 - which was why I thought I had to pre-fill the mutable collection. Is this not the case? \$\endgroup\$ – Shuri2060 Oct 28 '15 at 9:40
  • \$\begingroup\$ So for the copying, is: [X copyWithZone:nil] equivalent to [X copy]? \$\endgroup\$ – Shuri2060 Oct 28 '15 at 9:41
  • \$\begingroup\$ "This only affects your one program that contains this code" - I was really thinking of implementing this protocol into the majority of my programs, since I use deep (mutable) copying quite a lot. I wanted to get this right, since this code is going to be reused a lot. \$\endgroup\$ – Shuri2060 Oct 28 '15 at 9:43
  • \$\begingroup\$ "I thought the block would loop through the collection simultaneously" Only if use enumerateObjectsWithOptions:usingBlock: and pass NSEnumerationConcurrent. "So for the copying, is: [X copyWithZone:nil] equivalent to [X copy]" Yes; copy just calls through to copyWithZone:. "I was really thinking of implementing this protocol into the majority of my programs" Fine, but what you don't use won't be used. Implementing deep copy for NSSet doesn't have any effect if you never create an NSSet. Sets created by something other than your code will have the method, but won't know it exists. \$\endgroup\$ – Josh Caswell Oct 28 '15 at 18:55
  • \$\begingroup\$ Thank you! One last little problem: I tried implementing the category for NSMutableArray which would print to NSLog for addObject: (I couldn’t resist the temptation of seeing what would happen). Unfortunately, it didn’t seem to work — objects were added to arrays as they normally would be, and nothing came out in the NSLog (although in the category, I did indeed receive a yellow warning sign: “Category is implementing a method which will also be implemented by its primary class”). \$\endgroup\$ – Shuri2060 Oct 28 '15 at 20:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.