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:.
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.
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.
I have a NSManagedObject subclass (SOCommand*), and I'm trying to set properties of an instance of it using the following code:
SOCommand* newCommand = [[SOCommand alloc]init];
newCommand.commandName = self.tf_commandName.text;
newCommand.sshCommand = self.tf_sshCommand.text;
However, I'm getting the error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SOCommand setCommandName:]: unrecognized selector sent to instance
I thought changing the prewritten #dynamic to #synthesize would solve it. While it did resolve this error, it led to other ones since #dynamic is required for Core Data. How am I supposed to set its properties? I read somewhere that you have to initialize NSManagedObjects in a special way, but that involved "adding" to the database, so I'm really confused.
Thanks!
~Carpetfizz
You're not showing enough code to validate this, but I suspect you are making one of several common mistakes.
Common Problem #1: Not setting up your Managed Object Model Correctly
I can't demonstrate this in code, but your managed object model must contain each property you want. Just adding a new #property declaration in the object subclass does not add it to the model. It must exist in both places.
Common Problem #2: Implementing a Getter/Setter and calling Super
- (void)setCommandName:(NSString *)commandName
{
// Do "willUpdateCommandName" logic here
//Call Super
[super setCommandName:commandName];
// Do "didUpdateCommandName" logic here
}
This is a fairly common construction outside CoreData, but within Core Data, it is a common problem. Instead, you should call the primitive setter.
- (void)setCommandName:(NSString *)commandName
{
// Do "willUpdateCommandName" logic here
//Call Super
[self setPrimitiveCommandName:commandName];
// Do "didUpdateCommandName" logic here
}
Common Problem #3: You are initializing your object with -init, rather than -initWithEntity:insertIntoManagedObjectContext:.
You are definitely making this mistake in your code sample, but possibly others too. According to the documentation, you should not use -init. Internally, I suspect that it requires the NSEntityDescription to dynamically generate the getters/setters for your object.
If you don't want the object inserted immediately, you can pass nil as the NSManagedObjectContext, but you must pass in the NSEntityDescription, acquired from your NSManagedObjectModel. Doing this correctly looks like this.
// Macro for easy reading/writing
#define SOClass(x) NSStringFromClass([x class])
// It is important to note that the entity name *CAN* be different from the class name, but generally it is not
NSString *entityName = SOClass(SOCommand);
NSDictionary *entities = self.managedObjectModel.entitiesByName;
NSEntityDescription *entityDescription = entities[entityName];
SOCommand *newCommand = [[SOCommand alloc] initWithEntity:entityDescription
insertIntoManagedObjectContext:nil];
If you know the context you want to use, you may also use this syntax:
// Macro for easy reading/writing
#define SOClass(x) NSStringFromClass([x class])
// It is important to note that the entity name *CAN* be different from the class name, but generally it is not
NSString *entityName = SOClass(SOCommand);
SOCommand *newCommand = [NSEntityDescription entityForName:entityName
inManagedObjectContext:self.managedObjectContext];
you should insert new CoreData object by using:
[NSEntityDescription insertNewObjectForEntityForName:#"SOCommand" inManagedObjectContext:self.managedObjectContext];
the way you init SOCommand cause this error .
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.
My app crashed after I called addImageToQueue. I added initWithObjects: forKeys: count: but it doesn't helped me.
Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[NSDictionary initWithObjects:forKeys:count:]:
method only defined for abstract class.
Define -[DictionaryWithTag initWithObjects:forKeys:count:]!'
my code
- (void)addImageToQueue:(NSDictionary *)dict
{
DictionaryWithTag *dictTag = [DictionaryWithTag dictionaryWithDictionary:dict];
}
#interface DictionaryWithTag : NSDictionary
#property (nonatomic, assign) int tag;
- (id)initWithObjects:(id *)objects forKeys:(id *)keys count:(NSUInteger)count;
#end
#implementation DictionaryWithTag
#synthesize tag;
- (id)initWithObjects:(id *)objects forKeys:(id *)keys count:(NSUInteger)count
{
return [super initWithObjects:objects forKeys:keys count:count];
}
#end
Are you subclassing NSDictionary? That's not a common thing to do in Cocoa-land, which might explain why you're not seeing the results you expect.
NSDictionary is a class cluster. That means that you never actually work with an instance of NSDictionary, but rather with one of its private subclasses. See Apple's description of a class cluster here. From that doc:
You create and interact with instances of the cluster just as you would any other class. Behind the scenes, though, when you create an instance of the public class, the class returns an object of the appropriate subclass based on the creation method that you invoke. (You don’t, and can’t, choose the actual class of the instance.)
What your error message is telling you is that if you want to subclass NSDictionary, you have to implement your own backend storage for it (for example by writing a hash table in C). It's not just asking you to declare that method, it's asking you to write it from scratch, handling the storage yourself. That's because subclassing a class cluster directly like that is the same as saying you want to provide a new implementation for how dictionaries work. As I'm sure you can tell, that's a significant task.
Assuming you definitely want to subclass NSDictionary, your best bet is to write your subclass to contain a normal NSMutableDictionary as a property, and use that to handle your storage. This tutorial shows you one way to do that. That's not actually that hard, you just need to pass the required methods through to your dictionary property.
You could also try using associative references, which "simulate the addition of object instance variables to an existing class". That way you could associate an NSNumber with your existing dictionary to represent the tag, and no subclassing is needed.
Of course, you could also just have tag as a key in the dictionary, and store the value inside it like any other dictionary key.
From https://stackoverflow.com/a/1191351/467588, this is what I did to make a subclass of NSDictionary works. I just declare an NSDictionary as an instance variable of my class and add some more required methods. It's called "Composite Object" - thanks #mahboudz.
#interface MyCustomNSDictionary : NSDictionary {
NSDictionary *_dict;
}
#end
#implementation MyCustomNSDictionary
- (id)initWithObjects:(const id [])objects forKeys:(const id [])keys count:(NSUInteger)cnt {
_dict = [NSDictionary dictionaryWithObjects:objects forKeys:keys count:cnt];
return self;
}
- (NSUInteger)count {
return [_dict count];
}
- (id)objectForKey:(id)aKey {
return [_dict objectForKey:aKey];
}
- (NSEnumerator *)keyEnumerator {
return [_dict keyEnumerator];
}
#end
I just did a little trick.
I'm not sure that its the best solution (or even it is good to do it).
#interface MyDictionary : NSDictionary
#end
#implementation MyDictionary
+ (id) allocMyDictionary
{
return [[self alloc] init];
}
- (id) init
{
self = (MyDictionary *)[[NSDictionary alloc] init];
return self;
}
#end
This worked fine for me.