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
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;
}
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.
Related
I have many "model" objects whose properties are defined as "readonly" and shared among various components.
In some cases I need to create local mutable copies of the objects (using them for local mutable state)
I rather not implement NSMutableCopy protocol as the object should be immutable after it is created. The modified object could be "passed" around after copy+mutate operations.
Is there a suggested mechanism , or should I just implement a constructor receiving the "changed" parameters?
For example an object which parses a JSON to native types :
#interface ImmutableObject : NSObject
// various "readonly" properties
...
-(instancetype)initWithJSON:(NSDictionary *)jsonDictionary;
#property (nonatomic, readonly) MyClass1 *prop1;
#property (nonatomic, readonly) MyClass2 *prop2;
...
#property (nonatomic, readonly) NSArray<MyClass100 *> *prop100;
#end
#implementation
-(instancetype)initWithJSON:(NSDictionary *)jsonDictionary {
self = [super init];
[self parseDictionaryToNative:jsonDictionary];
return self;
}
#end
Somewhere in code:
ImmutableObject *mutated = [immutableObject mutableCopy]; // best way to accomplish this?
// change some values...
mutated.prop1 = ... // change the value to something new
self.state = [mutated copy]; // save the new object
#spinalwrap is correct, but in this case there is no reason to create the extra copy before storing it. NSMutableArray is a subclass of NSArray, so can be used anywhere an NSArray can be used (and this is very common). Same for yours. In your particular case, you'd probably do it this way:
MutableObject *mutated = [immutableObject mutableCopy]; // create an instance of MutableObject
mutated.prop1 = ... // change the value to something new
self.state = mutated; // Since `state` is an immutable type,
// attempts to mutate this later will be compiler errors
This is safe because you know that this block of code is the only block that has a reference to the mutable version of the object (because you created it here).
That said, once you've created a mutable subclass, you now need to consider the possibility that any ImmutableObject you are passed might actually be a MutableObject, and so make defensive copies (just as is done with NSArray, NSString, etc.) For example:
- (void)cacheObject:(ImmutableObject *)object {
// Need to copy here because object might really be a MutableObject
[self.cache addObject:[object copy]];
}
This is made fairly efficient by implementing copy on ImmutableObject and return self, and implementing copy on MutableObject as an actual copy, usually like this:
ImmutableObject.m
- (ImmutableObject *)copy {
return self;
}
MutableObject.m
// as in spinalwrap's example
- (MutableObject *)mutableCopy {
MutableObject *instance = [MutableObject new];
instance.prop1 = [self.prop1 copy]; // depends what you want here and what kind of class the properties are... do you need a deep copy? that might be a bit more work.
// etc...
return instance;
}
// No need to duplicate code here. Just declare it immutable;
// no one else has a pointer to it
- (ImmutableObject *)copy {
return (ImmutableObject *)[self mutableCopy];
}
So the copy is almost free if the object was immutable already. I say "fairly efficient" because it still causes some unnecessary copies of mutable objects that are never mutated. Swift's copy-on-write system for value types was specifically created to deal with this problem in ObjC. But the above is the common pattern in ObjC.
note that NSMutableArray, NSMutableData etc. are different classes than their immutable counterparts. So in this case, you maybe should define a MutableObject class with the same interface as the ImmutableObject class (but with mutable properties) and use that if you want to have a mutable object.
MutableObject *mutated = [immutableObject mutableCopy]; // create an instance of MutableObject
mutated.prop1 = ... // change the value to something new
self.state = [mutated copy]; // creates an ImmutableObject
the implementation of ImmutableObject's mutableCopy could be something like:
- (MutableObject *) mutableCopy
{
MutableObject *instance = [MutableObject new];
instance.prop1 = [self.prop1 copy]; // depends what you want here and what kind of class the properties are... do you need a deep copy? that might be a bit more work.
// etc...
return instance;
}
and MutableObject's copy method could look like this:
- (ImmutableObject *) copy
{
ImmutableObject *instance = [ImmutableObject new];
instance.prop1 = [self.prop1 copy];
// etc...
return instance;
}
You're not forced to use the NSMutableCopy protocol formally, but you can.
Say I create a RLMObject that has a relationship, which I proceed to save in my realm database. After that is complete, I decide to call initWithValues on the newly created object and return this copied object to the front end for use. I noticed that the object it has a relationship with is still considered instantiated.
Is there a way to make it such that when I call initWithValues to create an uninstantiated copy of my object, to ensure all my properties are uninstantiated as well?
There is no builtin way to achieve that. You would need to create a standalone copy of the object yourself. Relationships could be cyclic, so that a generic solution for that would be non-trivial.
If I understand you correctly, for example, you can implement the NSCopying protocol in each managed class:
- (instancetype)copyWithZone:(NSZone *)zone {
//SomeClass *object = (SomeClass *)[super.class allocWithZone:zone];
SomeClass *object = [SomeClass new];
object->_isClone = YES;
object->_name = self.name.copy;
object->_age = self.age;
return object;
}
Then, after removing the original, the copy remains available.
Can also be implement NSCopying protocol in category for RLMResults:
- (instancetype)copyWithZone:(NSZone *)zone {
NSMutableArray *array = [NSMutableArray arrayWithCapacity:self.count];
for (RLMObject *object in self) {
[array addObject:object.copy];
}
return array.copy;
}
Use case:
RLMResults *objects = [SomeObject objectsWhere:#"ANY nested.age < 99"];
NSArray<SomeObject*> *clones = objects.copy;
In my child view controller, I have a property defined as:
#property (nonatomic, copy) NSString *name;
In view controller A, the Parent, I have the following:
NSString *temp = currency.name; //This is because currency is a Core Data Managed Object.
//I wanted to make sure it wasn't a confounding factor.
childViewController.name = temp;
if(childViewController.name == temp)
NSLog(#"I am surprised");
The problem is that if statement finds equivalency and the "I am surprised" is printed. I thought that == should be checking if they're the same object, and that the use of copy in the property declaration should ensure the setter is making a copy. I checked in the debugger and they are both pointing to the same string. (Which I believe is immutable, which may be why this is happening?)
The same thing happens even if I write childViewController.name = [temp copy];, which I find shocking!
Can anyone explain what is going on here?
Edit: I removed a bit here on worrying about a circular reference which I realized wasn't a concern.
This is an optimization.
For immutable objects, it's superfluous to create an actual copy, so - copy is often implemented as a simple retain, i. e.
- (id)copy
{
[self retain];
return self;
}
Try assigning a mutable object (e. g. NSMutableString) to the property, and you will get the "expected" behavior.
I need to transfer a single object across device. Right now I am converting my NSManagedObject to a dictionary , archiving it and sending as NSData. Upon receiving I am unarchiving it. But I would really like to transfer the NSManagedObject itself by archiving and unarchiving instead of creating an intermediate data object.
#interface Test : NSManagedObject<NSCoding>
#property (nonatomic, retain) NSString * title;
#end
#implementation Test
#dynamic title;
- (id)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
self.title = [coder decodeObjectForKey:#"title"]; //<CRASH
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.title forKey:#"title"];
}
#end
NSData *archivedObjects = [NSKeyedArchiver archivedDataWithRootObject:testObj];
NSData *objectsData = archivedObjects;
if ([objectsData length] > 0) {
NSArray *objects = [NSKeyedUnarchiver unarchiveObjectWithData:objectsData];
}
The problem with the above code is. It crashes at self.title in initWithCoder saying unrecognized selector sent to instance.
Why is title not being recognized as a selector.
Should unarchive use a nil managed object context somehow before creating the object in initWithCoder?
Do i need to override copyWithZone?
This snippet below should do the trick. The main difference is to call super initWithEntity:insertIntoManagedObjectContext:
- (id)initWithCoder:(NSCoder *)aDecoder {
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Test" inManagedObjectContext:<YourContext>];
self = [super initWithEntity:entity insertIntoManagedObjectContext:nil];
NSArray * attributeNameArray = [[NSArray alloc] initWithArray:self.entity.attributesByName.allKeys];
for (NSString * attributeName in attributeNameArray) {
[self setValue:[aDecoder decodeObjectForKey:attributeName] forKey:attributeName];
}
return self;
}
Above snippet will handle only the attributes, no relationships. Dealing with relationships as NSManagedObjectID using NSCoding is horrible. If you do need to bring relationships across consider introducing an extra attribute to match the two (or many) entities when decoding.
how to obtain <YourContext>
(based on a now unavailable post by Sam Soffes, code taken from https://gist.github.com/soffes/317794#file-ssmanagedobject-m)
+ (NSManagedObjectContext *)mainContext {
AppDelegate *appDelegate = [AppDelegate sharedAppDelegate];
return [appDelegate managedObjectContext];
}
Note: replace <YourContext> in the first snippet with mainContext
Obviously NSManagedObject does not conform to NSCoding. You could try to make a custom managed object subclass conform, but it would be a dicey proposition at best. An NSManagedObject must have a related NSManagedObjectID. And, you don't get to assign the object ID-- that happens automatically when the object is created. Even if you made your subclass conform to NSCoding, you'd have to find a way to unarchive the object while also allowing the local managed object context to assign an object ID.
And even that ignores the question of how you'd handle relationships on your managed objects.
Converting to/from an NSDictionary is really a much better approach. But you can't just unarchive the data and be finished. On the receiving end, you need to create a new managed object instance and set its attribute values from the dictionary. It might be possible to get your approach to work, but by the time you're done it will be more work and more code than if you just used an NSDictionary.
Seriously: NSCoding, initWithCoder:, copyWithZone:, etc, are a really bad idea for the problem you're trying to solve. NSCoding is nice for many situations but it's not appropriate here.
The problem is obviously the unarchiver. In the end there is no way to use both initWithEntity: and initWithCoder: in the same object. However, I suspect that with some trickery you may be able to make this work. For instance, implement initWithCoder: as you have done, and in that create another managed object with initWithEntity: (this means you will need unmanaged ivars that can hold such a reference. Implement forwardingTargetForSelector:, and if the object is the one being created using initWithCoder:, forward it to the shadow object you created with initWithEntity: (otherwise, forward that selector to super). When the object is decoded fully, then ask it for the real managed object, and you're done.
NOTE: I have not done this but have had great success with forwardingTargetForSelector:.
I have a Singleton object that manages all my lists. We'll call it ListStore.
ListStore has a mutable array, which stores Lists.
#interface ListStore : NSObject
#property (nonatomic, copy) NSMutableArray *lists; // an array of List objects
end
Lists has a mutable array, which stores Things.
#interface Wanderlist : NSObject <NSCoding, NSCopying>
#property (nonatomic, copy) NSMutableArray *things; // an array of Thing objects
#end
At any time, a background process might go through ListStore and loop through and process all Lists, while a user might be interacting with a List.
To guard against "object was mutated while being enumerated" type errors, I do this:
// all of this is in a background thread
NSArray *newLists = [[ListStore sharedStore] lists] copy];
for (List *list in newLists) {
// yay, no more crashes, because I'm enumerating over a copied object, so the user
// can do whatever they want while I'm here
for(Thing *thing in list.things) {
// oh crap, my copy and the original object both reference the same list.things,
// which is why i'm seeing the 'mutation while enumerating" errors still
...
}
}
I originally thought that because I made a copy into newLists that all of its members would be properly copied. I now understand that not to be the case: I'm still seeing the "object was mutated while enumerated" errors, but this time it's happening on list.things.
Can I use NSCopying with my setup so that when I say:
[[ListStore sharedStore] copy];
It calls copyWithZone: on Lists, so I can then copyWithZone: on things?
I tried to set it up like this but copyWithZone: wasn't getting called.
I know I could simply say NSArray *newList = [list.things copy] but I'd like to get a better understanding of NSCopying at the very least.
Right before submitting this question I clicked on a question in SO's list of related questions, and found my solution.
Figured it doesn't hurt to post my solution.
Instead of this:
NSArray *newLists = [[ListStore sharedStore] lists] copy];
I had to do:
NSArray *newLists = [[NSArray alloc] initWithArray:[[ListStore sharedStore] lists] copyItems:true];
From the NSArray docs:
- (id)initWithArray:(NSArray *)array copyItems:(BOOL)flag
flag:
If YES, each object in array receives a copyWithZone: message to create a copy of the object—objects must conform to the NSCopying protocol. In a managed memory environment, this is instead of the retain message the object would otherwise receive. The object copy is then added to the returned array.
Once I used initWithArray:copyItems:, it automatically sent copyWithZone to all my List objects, and I was able to then manually perform a copyWithZone on list.things.