I know there are a lot of posts out there concerning the problem of how to archive custom objects in an NSArray or NSMutableArray and save them in NSUserDefaults. Conforming to the NSCoding Protocol and saving to NSUserDefaults isn't problematic and I use NSUserDefaults quite a lot to store the user-submitted data in my app - it mostly contains objects representing a Person (let's call the NSObject subclass "Person") which can have multiple objects of the NSObject subclass "Property" stored in an NSMutableArray. Therefore, the data structure looks like this:
NSMutableArray "persons":
Person "aPerson":
NSMutableArray "properties":
Property "aProperty"
Property "anotherProperty"
Person "anotherPerson:
...
Archiving and restoring the information was not problematic at first, because both Person and Property conform to the NSCoding Protocol - but now a problem occured which I was not able to solve yet despite those thousands of google requests in the last couple days ;)
Some of the Property objects contain references to other Persons ("Participants", which are linked to the same property and are contained in an NSMutableArray).
When I store the whole data to NSUserDefaults using NSKeyedArchiver, I use
[aCoder encodeObject:participants forKey:#"participants"];
in the Property's "encodeWithCoder" method to archive the NSMutableArray "participants" which stores the references to other Person objects. But when I decode those Person objects, they are created new and separated from the Person objects that already exist somewhere else. The NSMutableArray "participants" only contains references, weak links to the Person objects and should therefore conditional encode its content, as one can do with other objects manually in "encodeWithCoder":
[aCoder encodeConditionalObject:anObject forKey:aKey];
When the NSMutableArray gets decoded, it should represent a list of references to already existing Person objects - not completely new ones! The test "aPerson==[[aDecoder decodeObjectForKey:#"participants"] objectAtIndex:0]" is currently returning NO although it had returned YES before the encoding/decoding process has taken place.
I hope my explanation is somehow understandable and you can help me with my problem :) In simple words: How can I conditional encode custom objects contained in an NSMutableArray?
Thank You !
If NSMutableArray would use encodeConditionalObject:forKey: for the objects it contains, it would just mean that those objects aren't encoded at all, if they're not encoded unconditionally somewhere else in your object graph. This wouldn't help you in this case (the array would just be empty).
The problem is that you cannot really encode references to objects in memory. An object reference is basically just a pointer to an address in memory. When you start your app the next time and create the very same object (whether by unarchiving or otherwise), it will almost definitely have a different address in memory. There is no way the unarchiver can 'magically' know, which existing object corresponds to the reference it has archived, because the memory address (the object's 'identity') loses all its meaning when you quit your app.
You have to use other means of identifying your objects, such as database row IDs, dictionary keys, etc. and establish the connection of the archived key and the existing object corresponding to that key manually.
I had an issue with this too. I have objects that have an array of weak links to other objects. I know all the objects linked to will be encoded, so I just want to make sure I can rebuild the links.
With a single weak link is it possible to use:
aCoder.encodeConditionalObject(thing, forKey: "Thing")
...and if that item has already been encoded from elsewhere, then a reference to that encoded item will be used.
But, what to do if you have an array full of 'conditional' items, where the array needs to be encoded unconditionally?
I ended up wrapping the items I want to link to.
class thingLink: NSObject, NSCoding
{
weak var thing: Thing?
init(_ thing: Thing) {
self.thing = thing
}
required init?(coder aDecoder: NSCoder) {
thing = aDecoder.decodeObject(forKey: "Thing") as? Thing
}
func encode(with aCoder: NSCoder) {
// We encode these conditionally as they must be used elsewhere
aCoder.encodeConditionalObject(thing, forKey: "Thing")
}
}
...then I store these in my array which I encode as usual.
aCoder.encode(things, forKey: "Things")
If I move to a database to store things, I think this will help there too, because I will need a separate table to store the links and maintain priority etc.
Related
I have a Class for handling my data in my project, and now I need to store the data.
I'd like to use a Plist but I'm a bit unsure of how to start.
My class is pretty simple - 6 pieces of data, flat (no hierarchy).
I want my app to start with no data, so can I assume that I should create the PList programmatically once the User creates their first piece of data? (That is, don't create a .plist file in 'Supporting Files' prior to distribution?)
Then, when the app starts the next time, read the data and create an NSMUtableArray array of Class instances?
To create a property list, all you need to do is use appropriate types (i.e. those that support the property list format: NSData, NSString, NSDictionary, NSNumber, NSDate, NSArray), store them in a single container, and tell the containing object to write itself to a file. To read the data, you can initialize that same type using a path. For example:
// writing some data to a property list
NSString *somePath = ... // replace ... with the path where you want to store the plist file
NSMutableDictionary myDict = [NSMutableDictionary dictionary];
[myDict setObject:#"Caleb" forKey:#"name"];
[myDict setObject:[NSNumber numberWithInt:240] forKey:#"cholesterolOrIQ"];
[myDict writeToFile:somePath atomically:YES];
// reading the file again
NSDictionary *readDict = [NSDictionary dictionaryWithContentsOfFile:somePath];
The simplest way is to simple save an NSArray or NSDictionary to disk. Caleb's answer goes into detail there so I won't repeat it, other than to say you might have to convert a non-compatible object like NSColor to an property list object like NSData. It's up to you to do this each time you save or load your data.
NSKeyedArchiver and NSKeyedUnarchiver give you a little more control over the process, but work pretty much the same way. You provide (or get back) a plist compatible root object (usually an NSDictionary) that contains your data. I recommend creating a dictionary that includes your data structure as well as an arbitrary number (your app's build number is a good choice) to use as a version indicator. This way if you ever update your data model you can easily determine if you need to do anything to convert it to the new version.
If you're putting your own objects into the data file, look into NSCoding. The protocol gives you two methods using NSKeyedArchiver and NSKeyedUnarchiver to save and restore your data. This is by far the most straightforward approach if your data model consists of anything more than a few simple strings and numbers, since you're dealing with your own native objects. In your case, you would have your data class implement NSCoding and use the NSKeyedArchiver and NSKeyedUnarchiver methods to encode your six instance variables. When it's time to save or load, pack the instance of your class into an NSDictionary (along with a versioning number as I mentioned above) and call NSKeyedArchiver's archiveRootObject:toFile:. Your save an load methods deal only with your own data object, which makes things easy for you. The common pitfall to watch out for here is if your custom data object contains other custom object. This is fine, but you have to make sure every object that's going to be saved has its own NSCoding implementation.
Two things you can do:
Use NSUserDefaults:
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/Reference/Reference.html
The objectForKey method is the one you want to use to store your class. But, as pointed out in the comments, this shouldn't really be used for storing lots of user data; it's best for saving preferences.
For storing more data, you might want to look at Core Data. It's more complex, but should be better suited to your needs. Here's a tutorial on it:
http://mobile.tutsplus.com/tutorials/iphone/iphone-core-data/
Neither of these seems best for your simple application, but I leave this answer up since it gives alternatives for saving data to the iPhone.
I have three classes, A, B and C. A is the main class.
When the user wants to see the list of all objects that were purchased, Class B is called from A and shows the list of objects in a core data entity.
Inside class B, the user can buy new objects (in-app purchase). When the user wants to buy another object, class C is called.
When class C is called, a new object is created on the core data entity using
anObject = [NSEntityDescription insertNewObjectForEntityForName:#"Objects" inManagedObjectContext:context];
this object is then assigned to a local reference on Class C, using something like
self.object = anObject;
this object variable was declared like this:
.h
MyObjects *object;
#property (nonatomic, retain) MyObjects *object;
and #synthesized on .m
MyObjects is a core data class representing the entity.
In theory, object will retain anything assigned to it, so the line self.object = anObject I typed previously will retain anObject reference on self.object, right?
The problem is that when I try to access self.object in the same class after buying the new object, I receive an error "CoreData could not fulfill a fault for XXX", where XXX is exactly self.object.
At no point in the code there's any object removal from the database. The only operation to the database I could identify was a saving operation done by another class moments before the crash. The save is done by something like
if (![self.managedObjectContext save:&error]) ...
Is there any relation? what may be causing that?
CoreData manages the lifetime of managed objects and you should not retain and release them. If you want to keep a reference to the object so that it can be retrieved later then you have to store the object's id (obtained using -[NSManagedObject objectID]). Then use that to retrieve the object later using -[NSManagedObjectContext objectWithID:].
Make sure you understand about CoreData faulting. Read the documentation.
I had a similar issue a few days ago (using NSFetchedResultsController) where I was placing my fetchedObjects into an array and gathering attributes to populate tables from the array objects. It seems that if the objects in the array are faulted, you cannot unfault it unless you are acting on the direct object. In my case, I solved the issue by taking the lines of code in question and calling [[_fetchedResultsController objectAtIndexPath:indexPath] someAttribute]. I would assume that doing something similar would fix your problem as well. It seems a bit tedious to need to fetch from the managedObjectContext to obtain a faulted value, but this was the only way I could personally get past the issue.
Core Data is responsible for managing the lifetime of managed objects in memory. It's really important to understand Managed Object Contexts - Read the documentation.
Apple also provides an entire troubleshooting section here, and it contains among other things the causes for your error. But it's really only useful if you understand how core data works.
Most likely error is that the object you are saving does not belong to the managed object context.
Say you use the same object on different threads and those different threads use different managed object context, then this will happen.
I have a class that contains multiple user objects and as such has an array of them as an instance variable:
NSMutableArray *users;
The tricky part is setting it. I am deserializing these objects from a server via Objective Resource, and for backend reasons users can only be returned as a long string of UIDs - what I have locally is a separate dictionary of users keyed to UIDs. Given the string uidString of comma separated UIDs I override the default setter and populate the actual user objects:
#dynamic users;
- (void)setUsers:(id)uidString {
users = [NSMutableArray arrayWithArray:
[[User allUsersDictionary] objectsForKeys:[(NSString*)uidString componentsSeparatedByString:#","]]];
}
The problem is this: I now serialize these to database using SQLitePO, which stores these as the array of user objects, not the original string. So when I retrieve it from database the setter mistakenly treats this array of user objects as a string! Where I actually want to adjust the setter's behavior when it gets this object from DB vs. over the network.
I can't just make the getter serialize back into a string without tearing up large code that reference this array of user objects, and I tried to detect in the setter whether I have a string or an array coming in:
if ([uidString respondsToSelector:#selector(addObject)]) {
// Already an array, so don't do anything - just assign users = uidString
but no success... so I'm kind of stuck - any suggestions? Thanks in advance!
The solution you've tried is not exactly wrong, but should look like this:
if ([uidString respondsToSelector:#selector(addObject:)]) {
// Already an array, so don't do anything - just assign users = uidString
Seems that you've forggoten about the ":" - the addObject takes one parameter after all.
However, the proper way to do is to check the class of passed object:
if ([uidString isKindOfClass[NSArray class]])
Hope this was helpful,
Cheers, Pawel
It would be much less confusing - and more in line with the usual Cocoa style - if you let the actual setter take an NSArray or an NSMutableArray:
- (void)setUsers:(NSArray*)usersArray { ... }
... and have another method, say -setUsersFromUidString:, taking an NSString parameter, for the first case:
- (void)setUsersFromUidString:(NSString*)uidString { ... }
Your original approach loses the advantage of self-documentation Objective-C has and will confuse users of the class.
This is for an iPhone App, but I don't think that really matters. I need to send a custom object (which is managed by Core Data) over bluetooth using the iPhone's GameKit. Normally, I would just use an NSKeyedArchiver to package up the object as a data object and ship it over the line, then unarchive the object and I'm done. Of course, I would need to implement the initWithCoder: and encodeWithCoder: methods in my custom object as well.
I'm not sure if this can be done with an NSManagedObject class, which is managed by Core Data or not. Will they play nice together? I'm guessing once I ship the encoded managed object over to the other device and unencode it, I would just add this received object to the other device's context. Is this correct? Am I missing any steps?
An NSManagedObject instance can't meaningfully exist outside of an NSManagedObjectContext instance, so I wouldn't bother trying to do the NSCoding dances required to directly serialize and deserialize an NSManagedObject between two contexts (you can do this; see below). Instead I would create a dictionary with the appropriate attribute key/values (you can get the attribute names via the managed object instance's attribute names via instance.entity.attributesByName.allKeys (you can use [instance dictionaryWithValuesForKeys:keys] to get the dictionary of attribute:value pairs) . I would send relationship information as NSURL-encoded NSManagedObjectIDs. Don't forget to include the instance managedObjectID (as an NSURL) in the dictionary so that you can reconnect any relationships to the object on the other end. You'll have to recursively create these dictionaries for any targets of relationships for the instance you're encoding.
Then send the dict(s) across the wire and reconstitute them on the other end as instances in a new managed object context (you can use setValuesForKeysWithDictionary:).
You may notice that this is exactly what the NSCoder system would do for you, except you would have to use the classForCoder, replacementObjectForCoder: and awakeAfterUsingCoder: along with a custom NSDictionary subclass to handle all the NSManageObject-to-NSDictionary mapping and visa versa. This code is more trouble than it's worth, in my experience, unless you have a complex/deep object graph that you're trying to serialize. For a single NSManagedObject instance with no relationships, it's definitely easier to just do the conversion to a dict and back yourself.
This sounds like a job for TPAutoArchiver.
I suggest the dictionary solution for simpler options. However, here is how I solved the issue. My model was already sizable and robust, with custom classes and a single root class above NSManagedObject.
All that I needed was for that single class to call the appropriate designated initializer of NSManagedObject: [super initWithEntity:insertIntoManagedObjectContext:]. This method, and the metadata in an NSEntityDescription is what sets up the implementations of all the dynamic accessors.
- (id)initWithCoder:(NSCoder *)aDecoder {
CoreDataStack *cds = [LibraryDiscoverer unarchivingCoreDataStack];
NSEntityDescription *entity = [cds entityDescriptionForName:[[self class] entityName]];
NSManagedObjectContext *moc = [cds managedObjectContext];
self = [super initWithEntity:entity insertIntoManagedObjectContext:moc];
self.lastEditDate = [aDecoder decodeObjectForKey:#"lastEditDate"];
return self;
}
The CoreDataStack is my abstraction around CoreData. The LibraryDiscoverer is a global access hook to get hold of the core data information. The entityName is a method defined to provide the entity name from the class name; if you follow a naming convention (i.e. class name = entity name) it can be implemented generically.
All the other initWithCoder: methods in my class hierarchy are standard NSCoder, with the note that you don't need to encode both directions of a relationship, CoreData reconnects that for you. (As it always does, including with the dictionary solution.)
I want to add an NSMutableArray of NSStrings to one of my Entities in my core data model. The problem is that this isn't a supported type in Core Data.
I tried making a tranformable attribute, but the problem is that I see no way of turning a NSMutableArray to NSData, and then going from NSData, back to an NSMutableArray. Does anyone have an idea as to how this issue can be solved?
(I know I can archive the array, but I don't want to do that, I want it to be present in my model).
You could have a binary data attribute in your modeled object, archive the array to data, and hand it off to the object.
But I think the better way would be to have a to-many relationship, instead of using an array directly.
****Edit: Here's how to archive the array into NSData so that it can be used in your managed object***
NSData *arrayData = [NSKeyedArchiver archivedDataWithRootObject:[NSArray arrayWithObjects:#"1",#"2", nil]];
Basically, any class you have which conforms to the NSCoding protocol can be archived in this way. NSArray/NSMutableArray already conform to it. They tell all of their objects to archive themselves, so they must conform too. And all of those objects' members must conform, etc. It's like a tree.
Since your array conforms, and it's an array of NSString (which also conforms), then you're golden.