86

I've read the NSCopying docs but I am still very unsure about how to implement what is required.

My class Vendor:

@interface Vendor : NSObject 
{
    NSString        *vendorID;
    NSMutableArray  *availableCars;
    BOOL            atAirport;
}

@property (nonatomic, copy) NSString *vendorID;
@property (nonatomic, retain) NSMutableArray *availableCars;
@property (nonatomic, assign) BOOL atAirport;

- (id)initFromVehVendorAvailsDictionary:(NSDictionary *)vehVendorAvails;

@end

The Vendor class has an array of objects called Car.

My Car object:

@interface Car : NSObject 
{
    BOOL            isAvailable;
    NSString        *transmissionType;
    NSMutableArray  *vehicleCharges; 
    NSMutableArray  *fees; 
}

@property (nonatomic, assign) BOOL isAvailable;
@property (nonatomic, copy) NSString *transmissionType;
@property (nonatomic, retain) NSMutableArray *vehicleCharges;
@property (nonatomic, retain) NSMutableArray *fees;

- (id) initFromVehicleDictionary:(NSDictionary *)vehicleDictionary;

@end

So, Vendor holds an array of Car objects. Car holds 2 arrays of other custom objects.

Both Vendor and Car are init from a dictionary. I'll add one of these methods, they may or may not be relevant.

-(id)initFromVehVendorAvailsDictionary:(NSDictionary *)vehVendorAvails {

    self.vendorCode      = [[vehVendorAvails objectForKey:@"Vendor"] 
                           objectForKey:@"@Code"];

    self.vendorName      = [[vehVendorAvails objectForKey:@"Vendor"] 
                           objectForKey:@"@CompanyShortName"];

    self.vendorDivision  = [[vehVendorAvails objectForKey:@"Vendor"]   
                           objectForKey:@"@Division"];

    self.locationCode    = [[[vehVendorAvails objectForKey:@"Info"] 
                           objectForKey:@"LocationDetails"] 
                           objectForKey:@"@Code"];

    self.atAirport       = [[[[vehVendorAvails objectForKey:@"Info"] 
                           objectForKey:@"LocationDetails"] 
                           objectForKey:@"@AtAirport"] boolValue];

    self.venLocationName = [[[vehVendorAvails objectForKey:@"Info"] 
                           objectForKey:@"LocationDetails"] 
                           objectForKey:@"@Name"];

    self.venAddress      = [[[[vehVendorAvails objectForKey:@"Info"] 
                           objectForKey:@"LocationDetails"] 
                           objectForKey:@"Address"] 
                           objectForKey:@"AddressLine"];

    self.venCountryCode  = [[[[[vehVendorAvails objectForKey:@"Info"]  
                           objectForKey:@"LocationDetails"] 
                           objectForKey:@"Address"] 
                           objectForKey:@"CountryName"]
                           objectForKey:@"@Code"];

    self.venPhone        = [[[[vehVendorAvails objectForKey:@"Info"]  
                           objectForKey:@"LocationDetails"]        
                           objectForKey:@"Telephone"] 
                           objectForKey:@"@PhoneNumber"];

    availableCars        = [[NSMutableArray alloc] init];

    NSMutableArray *cars = (NSMutableArray *)[vehVendorAvails objectForKey:@"VehAvails"];

    for (int i = 0; i < [cars count]; i++) {

        Car *car = [[Car alloc] initFromVehicleDictionary:[cars objectAtIndex:i]];
        [availableCars addObject:car];
        [car release];
    }

    self.venLogo = [[[vehVendorAvails objectForKey:@"Info"] 
                   objectForKey:@"TPA_Extensions"] 
                   objectForKey:@"VendorPictureURL"];

    return self;
}

So to summarize the scary problem.

I need to copy an array of Vendor objects. I believe I need to implement the NSCopying protocol on Vendor, which may mean I need to implement it also on Car since Vendor holds an array of Cars. That means I also need to implement it on the classes that are held in the 2 arrays belonging to the Car object.

I'd really appreciate it if I could get some guidance on implementing NSCopying protocol on Vendor, I can't find any tutorials on this anywhere.

  • Have you read the documentation of NSCopying? I found it quite clear when needed. – jv42 Nov 03 '10 at 16:39
  • 5
    Yes, read and it re-read it. I rarely find apple docs easy to learn from, though they are great for finding methods etc while programming. Thanks -Code –  Nov 03 '10 at 16:57

3 Answers3

187

To implement NSCopying, your object must respond to the -copyWithZone: selector. Here’s how you declare that you conform to it:

@interface MyObject : NSObject <NSCopying> {

Then, in your object’s implementation (your .m file):

- (id)copyWithZone:(NSZone *)zone
{
    // Copying code here.
}

What should your code do? First, create a new instance of the object—you can call [[[self class] alloc] init] to get an initialized obejct of the current class, which works well for subclassing. Then, for any instance variables that are a subclass of NSObject that supports copying, you can call [thatObject copyWithZone:zone] for the new object. For primitive types (int, char, BOOL and friends) just set the variables to be equal. So, for your object Vendor, it’d look like this:

- (id)copyWithZone:(NSZone *)zone
{
    id copy = [[[self class] alloc] init];

    if (copy) {
        // Copy NSObject subclasses
        [copy setVendorID:[[self.vendorID copyWithZone:zone] autorelease]];
        [copy setAvailableCars:[[self.availableCars copyWithZone:zone] autorelease]];

        // Set primitives
        [copy setAtAirport:self.atAirport];
    }

    return copy;
}
Jeff Kelley
  • 19,021
  • 6
  • 70
  • 80
  • Hi Jeff, Is it really as simple as that? Also do I need to apply the same things to the Car class and all custom objects that it contains arrays of? Basically step down the whole way. Or would it only need to be applied to Vendor? Many Thanks, –  Nov 03 '10 at 16:59
  • 2
    @Code: `copy` is typically implemented as a shallow copy like Jeff showed. It's unusual — though not inconceivable — that you'd want a full-on deep copy (where everything all the way down is copied). Deep copies are a lot more trouble, too, so you generally want to be sure that's really what you want. – Chuck Nov 03 '10 at 17:15
  • @Code: You’ll want to implement this for anything you plan on copying with `-copy` or `-copyWithZone:`. Note that as @Chuck says, this will be a “shallow copy,” so the arrays may point to the same objects in the original and the copy. If those objects change, you may need to copy them as well. – Jeff Kelley Nov 03 '10 at 17:50
  • 3
    There is a problem in your code where you copy your subclasses, as `copyWithZone:` returns a object with reference count of 1 and no autorelease this will cause a leak. You need to add at least an autorelease. – Marius May 26 '11 at 19:53
  • Do you mean instance variables instead of subclasses? You’re right about those, I’ll edit the answer. – Jeff Kelley May 27 '11 at 19:34
  • 23
    Shouldn't `[[self class] alloc]` use `allocWithZone` instead? Sorry for bringing this up. – jweyrich Oct 02 '12 at 19:34
  • 1
    @jweyrich You’re probably correct, but in typical use this won’t matter much. – Jeff Kelley Oct 03 '12 at 16:14
  • 1
    Folks, I suppose by using ARC(since the minimum supported IOS for any app is 4.3), you need not worry about release & auto-release. – rishabh Dec 05 '12 at 17:05
  • Let's say I do need a deep copy. How would the code for that be different from the code here? – GeneralMike Feb 27 '14 at 15:44
  • 1
    @GeneralMike: This should probably be a separate question, but in general (see what I did there?), you want to make sure to copy every object from the original during a deep copy—and make sure that their `-copy` methods also do deep copies. – Jeff Kelley Feb 28 '14 at 04:36
  • @JeffKelley: Yeah that's what I figured, which is why I didn't post another Q. I was just wondering if there was a better way than a loop-and-copy type thing, but I guess not. Thx for the help. – GeneralMike Feb 28 '14 at 13:38
  • And if the class is immutable, just return object itself without creating a new object. – Phineas Lue Oct 29 '15 at 22:35
  • @jweyrich you are correct. I added a new answer with this correction. – Justin Meiners Jan 03 '17 at 17:20
  • I need more explaining here. In both NSObjcet.h and the documentation, copyWithZone: is always a Class method! +(id) copyWithZone: etc. That really confused me, because how can a class method copy a specific instance it doesn't know about? Furthermore the copyWithZone: method definition in NSObject.h - is not available when using ARC!!!! so I can't even implement it. Please advise/explain – Motti Shneor Jan 24 '22 at 09:26
7

This answer is similar to the accepted, but uses allocWithZone: and is updated for ARC. NSZone is foundation class for allocating memory. While ignoring NSZone might work for most cases, it is still incorrect.

To correctly implement NSCopying you must implement a protocol method which allocates a new copy of the object, with properties that match the values of the original.

In the interface declaration in the header, specify that your class implements the NSCopying protocol:

@interface Car : NSObject<NSCopying>
{
 ...
}

In the .m implementation add a -(id)copyWithZone method which looks something like the following:

- (id)copyWithZone:(NSZone*)zone
{
    Car* carCopy = [[[self class] allocWithZone:zone] init];

    if (carCopy)
    {
        carCopy.isAvailable = _isAvailable;
        carCopy.transmissionType = _transmissionType;
        ... // assign all other properties.
    }

    return carCopy;
}
Justin Meiners
  • 10,754
  • 6
  • 50
  • 92
3

Swift Version

Just call object.copy() to create the copy.

I didn't use copy() for value types since those are copied "automatically." But I had to use copy() for class types.

I ignored the NSZone parameter because docs say it is deprecated:

This parameter is ignored. Memory zones are no longer used by Objective-C.

Also, please note that this is a simplified implementation. If you have subclasses it gets a bit tricker and you should use dynamic type: type(of: self).init(transmissionType: transmissionType).

class Vendor {
    let vendorId: String
    var availableCars: [Car] = []

    init(vendorId: String) {
        self.vendorId = vendorId
    }
}

extension Vendor: NSCopying {
    func copy(with zone: NSZone? = nil) -> Any {
        let copy = Vendor(vendorId: vendorId)
        if let availableCarsCopy = availableCars.map({$0.copy()}) as? [Car] {
            copy.availableCars = availableCarsCopy
        }
        return copy
    }
}

class Car {
    let transmissionType: String
    var isAvailable: Bool = false
    var fees: [Double] = []

    init(transmissionType: String) {
        self.transmissionType = transmissionType
    }
}

extension Car: NSCopying {
    func copy(with zone: NSZone? = nil) -> Any {
        let copy = Car(transmissionType: transmissionType)
        copy.isAvailable = isAvailable
        copy.fees = fees
        return copy
    }
}
kgaidis
  • 14,259
  • 4
  • 79
  • 93