iosiphonecocoanscodingnscopying

How to make a deep copy with copyWithZone to duplicate a structure?


I have a class that represents a structure.

This class called Object has the following properties

@property (nonatomic, strong) NSArray *children;
@property (nonatomic, assign) NSInteger type;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, weak) id parent;

children is an array of other Objects. parent is a weak reference to an object parent.

I am trying to do copy and paste a branch of this structure. If a root object is selected, parent is nil, obviously. If the object is not the root, it has a parent.

To be able to do so, the objects of kind Object have to conform to NSCopying and NSCoding protocols.

This is my implementation of these protocols on that class.

-(id) copyWithZone: (NSZone *) zone
{
  Object *obj = [[Object allocWithZone:zone] init];
  if (obj) {
    [obj setChildren:_children];
    [obj setType:_type];
    [obj setName:_name];
    [obj setParent:_parent];
   }

  return obj;
}

- (void)encodeWithCoder:(NSCoder *)coder {
  [coder encodeObject:@(self.type) forKey:@"type"];
  [coder encodeObject:self.name forKey:@"name"];
  NSData *childrenData = [NSKeyedArchiver archivedDataWithRootObject:self.children];
  [coder encodeObject:childrenData forKey:@"children"];
  [coder encodeConditionalObject:self.parent forKey:@"parent"]; //*
}

- (id)initWithCoder:(NSCoder *)coder {

  self = [super init];

  if (self) {

    _type = [[coder decodeObjectForKey:@"type"] integerValue];
    _name = [coder decodeObjectForKey:@"name"];
    _parent = [coder decodeObjectForKey:@"parent"]; //*
    NSData *childrenData = [coder decodeObjectForKey:@"children"];
    _children = [NSKeyedUnarchiver unarchiveObjectWithData:childrenData];
    _parent = nil;
  }

  return self;

}

You may have notice that I have no reference to retrieve or storing self.parent on initWithCoder: and encodeWithCoder: and because of that, every sub object of an object comes with parent = nil.

I simply don't know how to store that. Simply because of this. Suppose I have this structure of Object.

ObjectA > ObjectB > ObjectC

When encoderWithCoder: starts its magic encoding ObjectA, it will also encode, ObjectB and ObjectC but when it starts encoding ObjectB it finds a parent reference pointing to ObjectA and will start that again, creating a circular reference that hangs the application. I tried that.

How do I encode/restore that parent reference?

What I need is to store an object and by the time of restore, to restore a new copy, identical to what was stored. I don't want to restore the same object that was stored, but rather, a copy.

NOTE: I have added the lines marked with //* as suggested by Ken, but _parent is nil on initWithCoder: for objects that should have a parent


Solution

  • Encode the parent with [coder encodeConditionalObject:self.parent forKey:@"parent"]. Decode it with -decodeObjectForKey: as normal.

    What this does is, if the parent would be archived for some other reason — say it was a child of an object higher in the tree — the reference to the parent is restored. However, if the parent was only ever encoded as a conditional object, it won't be stored in the archive. Upon decoding the archive, the parent will be nil.

    The way you're encoding the children array is both clumsy and will prevent the proper encoding of the parent as a conditional object. Since you're creating a separate archive for the children, no archive is likely to have both an Object and its parent (unconditionally). Therefore, the link to the parent won't be restored when the archive is decoded. You should do [coder encodeObject:self.children forKey:@"children"] and _children = [coder decodeObjectForKey:@"children"], instead.

    There's a problem with your -copyWithZone: implementation. The copy has the same children as the original, but the children don't consider the copy as their parent. Likewise, the copy considers the original's parent to be its parent, but that parent object doesn't include the copy among its children. This will cause you grief.

    One option would be to leverage your NSCoding support to make the copy. You'd encode the original and then decode it to produce the copy. Like so:

    -(id) copyWithZone: (NSZone *) zone
    {
      NSData* selfArchive = [NSKeyedArchiver archivedDataWithRootObject:self];
      return [NSKeyedUnarchiver unarchiveObjectWithData:selfArchive];
    }
    

    The copy operation would copy an entire sub-tree. So, it would have its own children which are copies of the original's children, etc. It would have no parent.

    The other option is to just copy the aspects of the original which are not part of the encompassing tree data structure (i.e. type and name). That is, the copy will end up with no parent and no children, which is appropriate because it's not in the tree itself, it's just a copy of a thing which happened to be in a tree at the time.

    Finally, -copyWithZone: should use [self class] instead of Object when it allocates the new object. That way, if you ever write a subclass of Object and its -copyWithZone: calls through to super before setting the subclass's properties, this implementation in Object will allocate an instance of the right class (the subclass). For example:

    -(id) copyWithZone: (NSZone *) zone
    {
      Object *obj = [[[self class] allocWithZone:zone] init];
      if (obj) {
        [obj setType:_type];
        [obj setName:_name];
       }
    
      return obj;
    }