4

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

Duck
  • 34,902
  • 47
  • 248
  • 470

2 Answers2

4

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;
}
Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • This is the first time I use this copywithZone/encode/decode thing. Can you show in code what you mean by the fourth and last paragraphs? Thanks – Duck Mar 03 '15 at 04:56
  • I assume you meant the fifth paragraph, since the fourth didn't have anything to illustrate. I have edited to include sample implementations of the approaches I described. – Ken Thomases Mar 03 '15 at 05:21
  • Thanks for the explanation, but you have two codes copyWithZone, I don't get what I should use... – Duck Mar 03 '15 at 15:54
  • @SpaceDog It depends on the semantics of your program. If copying an `Object` should also copy all of its ancestors and descendants, use the first. If it should only copy `type` and `name` (and become orphaned), use the second. – Aaron Brager Mar 03 '15 at 18:49
  • @AaronBrager, thanks for clarifying, except one thing: copying an `Object` using the encode/decode approach will copy its descendants, but never it parent or ancestors. – Ken Thomases Mar 04 '15 at 04:02
1

Following your edit to the question adding the Note

@KenThomases is essentially correct, but his question

By the way, is there are a reason you're encoding the children array that way?

Should be a correction not a question. The method encodeConditionalObject:forKey: will conditionally encode in the current archive. By using a different archive:

NSData *childrenData = [NSKeyedArchiver archivedDataWithRootObject:self.children];

you have not achieved the correct sharing. You should do as Ken suggested and just:

[coder encodeObject:self.children forKey:@"children"];

so the children are encoded by the same NSKeyedArchiver.

And your other problem is the rather obvious:

_parent = nil;

presumably a left over.

CRD
  • 52,522
  • 5
  • 70
  • 86