Issue with initWithCoder: and NSKeyedUnarchiver - ios

I'm currently attempting to add a game-saving feature to my app. The end goal is to get ~300 custom objects saved to a .plist file and extract them again later. I've made some headway, but I'm having some issues with initWithCoder: and I'm also unsure about my technique.
The following code is being used by a UIViewController to save the objects (I only added 2 objects to the dictionary as an example):
//Saves the contents to a file using an NSMutableDictionary
-(IBAction)saveContents {
//Create the dictionary
NSMutableDictionary *dataToSave = [NSMutableDictionary new];
//Add objects to the dictionary
[dataToSave setObject:label.text forKey:#"label.text"];
[dataToSave setObject:territory forKey:#"territory"];
[dataToSave setObject:territory2 forKey:#"territory2"];
//Archive the dictionary and its contents and set a BOOL to indicate if it succeeds
BOOL success = [NSKeyedArchiver archiveRootObject:dataToSave toFile:[self gameSaveFilePath]];
//Handle success/failure here...
//Remove and free the dictionary
[dataToSave removeAllObjects]; dataToSave = nil;
}
This successfully calls the encodeWithCoder: function in the Territory class twice:
-(void)encodeWithCoder:(NSCoder *)aCoder {
NSLog(#"ENCODING");
[aCoder encodeObject:self.data forKey:#"data"];
[aCoder encodeInt:self.MMHValue forKey:#"MMHValue"];
[aCoder encodeObject:self.territoryName forKey:#"territoryName"];
//Continue with other objects
}
When I look in the directory, sure enough, the file exists. Now, here's the issue I'm having: When the following function is run, the initWithCoder: function is successfully called twice as it should be. The logs inside the initWithCoder: function output what they should, but the logs in the UIViewController's loadContents function return 0, null, etc. However, the label's text is set correctly.
//Loads the contents from a file using an NSDictionary
-(IBAction)loadContents {
//Create a dictionary to load saved data into then load the saved data into the dictionary
NSDictionary *savedData = [NSKeyedUnarchiver unarchiveObjectWithFile:[self gameSaveFilePath]];
//Load objects using the dictionary's data
label.text = [savedData objectForKey:#"label.text"];
NSLog(#"%i, %i", [territory intAtKey:#"Key4"], [territory2 intAtKey:#"Key4"]);
NSLog(#"MMH: %i, %i", territory.MMHValue, territory2.MMHValue);
NSLog(#"NAME: %#, %#", territory.territoryName, territory2.territoryName);
//Free the dictionary
savedData = nil;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
NSLog(#"DECODING TERRITORY");
self.data = [aDecoder decodeObjectForKey:#"data"];
self.MMHValue = [aDecoder decodeIntForKey:#"MMHValue"];
self.territoryName = [[aDecoder decodeObjectForKey:#"territoryName"] copy];
//Continue with other objects
}
NSLog(#"DECODED INT: %i", self.MMHValue);
NSLog(#"DECODED NAME: %#", self.territoryName);
return self;
}
I've been trying to get this to work for hours, but to no avail. If anyone has any insights to this, please help me out. Also, I'm not entirely sure if my technique for saving is good or not (using an NSMutableDictionary to store references to the objects so it can output to one file)? Thanks!

I read through Apple's documentation on NSKeyedArchiver and NSKeyedUnarchiver, along with a bunch of blog posts on the subject, and I realized what I was doing wrong. After encoding the objects to a file (by adding them to a dictionary using keys and then saving them to a .plist), when I tried decoding them, I never actually extracted the values from the new dictionary (the dictionary holding the contents of the file that was just decoded). This also explains why the label's text was loaded, but territory and territory2 weren't. I changed my loadContents function to the following, and now the code works:
-(IBAction)loadContents {
//Create a dictionary to load saved data into then load the saved data into the dictionary
NSLog(#"***** STARTING DECODE *****");
NSDictionary *savedData = [NSKeyedUnarchiver unarchiveObjectWithFile:[self gameSaveFilePath]];
NSLog(#"***** CALLING initWithCoder: ON OBJECTS *****");
NSLog(#"***** FINISHED DECODE *****");
//Load objects using the dictionary's data
label.text = [savedData objectForKey:#"label.text"];
territory = [savedData objectForKey:#"territory"];
territory2 = [savedData objectForKey:#"territory2"];
//Free the dictionary
savedData = nil;
}

Related

Objective C - How To Keep Reference To Multiple Objects With Keys Just Like How NSMutableDictionary Works

I just learned how to make use of KVO, but only the basics. What I need to achieve is something like this:
I have a delegate call that passes a Speaker object.
- (void)onSpeakerFound:(Speaker *)speaker
Once I receive this Speaker in the UI part, from there I will assign observers for this object.
But, this is just for one speaker. What if I have multiple speakers to keep track of. I need to assign observers separately for those speakers and then at the same time I wish to keep their references for further updates to the values.
Each speaker could be updated from time to time. So when I notice that there is a change that happened on a speaker, I wish to access the reference to that speaker and update the values just like how NSMutableDictionary works.
NSMutableDictionary makes a copy of an object set to it so it will be a difference object if I get it again from the dictionary.
So, is there a class that allows me to keep track of an object by just keeping a reference only to that object without making a copy of it?
EDIT: A Test Made To Verify That When An Instantiated Object is Set in an NSMutableDictionary, The Instantiated Object is not referenced with the one set inside NSMutableDictionary.
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
NSString *obj = #"initial value";
NSString *key = #"key";
[dict setObject:obj forKey:key];
NSLog(#"Object is now %#", [dict objectForKey:key]);
obj = #"changed value";
NSLog(#"Object is now %#", [dict objectForKey:key]);
}
Log:
2016-07-26 21:04:58.759 AutoLayoutTest[49723:2144268] Object is now initial value
2016-07-26 21:04:58.761 AutoLayoutTest[49723:2144268] Object is now initial value
NSMutableDictionary makes a copy of an object set to it...
That is not correct; it will add a reference to the object. It will be the same object referenced inside and outside the Objective-C collection.
So, is there a class that allows me to keep track of an object...?
Probably NSMutableSet if you just want a list of the objects. That will take care that you have a unique reference to each object, however you need to implement the methods hash and isEqual on those objects so they behave correctly. Otherwise NSMutableDictionary if you want fast look-up by key.
-try this one
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
NSString *obj = #"initial value";
NSString *key = #"key";
[dict setObject:obj forKey:key];
NSLog(#"Object is now %#", [dict objectForKey:key]);
obj = #"changed value";
[dict setObject:obj forKey:Key];
NSLog(#"Object is now %#", [dict objectForKey:key]);
}

Saving non-property list items to NSUserDefaults

I have an array of custom objects which I want to store in array that updates the UITableView. The array is quite small ~10 objects at most. However, I discovered that I am unable to save an array of custom objects because:
Property list invalid for format: 200 (property lists cannot contain objects of type 'CFType')
2015-01-04 17:56:33.414 fanSyncDemo[13985:1264828] Attempt to set a non-property-list object (
It looks like I am going to have to turn the objects into NSData objects using NSKeyArchiver. However, I am having trouble understanding how this will look. Could someone help me with an example? My code for the saving looks like this:
- (IBAction)savePressed:(id)sender {
if (_NotificationsSegmentedControl.selectedSegmentIndex == 0){
_notificationBool = #"Yes";
}
else{
_notificationBool = #"No";
}
MyFavorite *favorite = [[MyFavorite alloc] initWithName:_nameFavoriteTextField.text withLocation:#"FANLOCATION" withIsOnNotificationsScreen:_notificationBool];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *favoritesList = [[NSMutableArray alloc] initWithArray: [defaults objectForKey:#"favoritesList"]];
[favoritesList addObject:favorite];
[defaults setObject:favoritesList forKey:#"favoritesList"];
[defaults synchronize];
}
This is not a duplicate of this question because that example doesn't explain how to retrieve the data, nor can I figure out how to apply it to my example which normally I don't have a problem with but I have just been struggling with this.
From what I understand your going to want to do something like this in your MyFavorite class
- (id)initWithCoder:(NSCoder *)aDecoder
{
_location = [aDecoder decodeObjectForKey:#"location"];
_isOnNotificationsScreen = [aDecoder decodeObjectForKey:#"notificationScreen"];
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:self.location forKey:#"location"];
[aCoder encodeObject:self.isOnNotificationsScreen forKey:#"notificationScreen"];
}

Detecting duplicate custom object contained in a NSMutable Array

I've read every similar question, but have determined either I'm doing something stupid (possible) or I fail to grasp the NSArray method containsObject:
I'm trying to setup a UITableView that contains saved "favorites"; locations that are kept as a custom class called "MapAnnotations." This contains stuff like coordinates, title, an info field, and a couple of other parameters. I'm successfully saving/retrieving it from a NSUserDefaults instance, but can't seem to successfully detect duplicate objects held in my NSMutableArray.
Here's the relevant code:
-(void)doSetUp
{
//load up saved locations, if it exists
NSUserDefaults *myDefaults = [NSUserDefaults standardUserDefaults];
//if there are saved locations
if ([myDefaults objectForKey:#"savedLocations"]) {
NSLog(#"file exists!");
//get saved data and put in a temporary array
NSData *theData = [myDefaults dataForKey:#"savedLocations"];
//my custom object uses NSCode protocol
NSArray *temp = (NSArray *)[NSKeyedUnarchiver unarchiveObjectWithData:theData];
NSLog(#"temp contains:%#",temp);
//_myFavs currently exists as a NSMutableArray property
_myFavs = [temp mutableCopy];
}else{
NSLog(#"File doesn't exist");
_myFavs = [[NSMutableArray alloc]init];
}
//_currLoc is an instance of my Mapnnotations custom class
// which contains coordinates, title, info, etc.
if (_currLoc != nil) {
//if this is a duplicate of a saved location
if ([_myFavs containsObject:_currLoc]) {
//pop an alert
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"Sorry..." message:#"That location has already been saved." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alert show];
}else{
//add location to end of myFavs array
[_myFavs addObject:_currLoc];
NSLog(#"myFavs now contains:%#",_myFavs);
//write defaults
NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:_myFavs];
[myDefaults setObject:encodedObject forKey:#"savedLocations"];
[myDefaults synchronize];
}
}
}
I've tried enumerating through the _myFavs array, checking for matches on specific fields (get errors for enumerating through something mutable), tried to copy to a straight array... tried to use indexOfObject:..
You can use containsObject: method with custom objects that implement isEqual: method. Adding an implementation of this method to your Mapnnotations class will fix the problem:
// In the .h file:
#interface Mapnnotations : NSObject
-(BOOL)isEqual:(id)otherObj;
...
#end
// In the .m file:
#implementation Mapnnotations
-(BOOL)isEqual:(id)otherObj {
... // Check if other is Mapnnotations, and compare the other instance
// to this instance
Mapnnotations *other = (Mapnnotations*)otherObj;
// Instead of comparing unique identifiers, you could compare
// a combination of other custom properties of your objects:
return self.uniqueIdentifier == other.uniqueIdentifier;
}
#end
Note: when you implement your own isEqual: method, it is a good idea to implement the hash method as well. This would let you use the custom objects in hash sets and as NSDictionary keys.
Or you could use an NSOrderedSet (or mutable if needed) which is designed to do all your set membership functions, as well as having all your index type functions you'd expect from an NSArray.
You can transform it to array with - array when you need actual an NSArray version where an enumerable won't work.

"Attempt to set a non-property list object...." NSMutableDictionary with custom class [duplicate]

This question already has answers here:
Write custom object to .plist in Cocoa
(3 answers)
Closed 9 years ago.
I have a custom class called ServerModule which is a subclass of NSObject. I'm basically storing all of these ServerModules with a key-value pair in an NSMutableDictionary. The dictionary is then stored in NSUserDefaults. I learned that NSUserDefaults only returns an immutable version of the object when it is accessed, so I changed my dictionary initialization to this:
_AllModules = [[NSMutableDictionary alloc]initWithDictionary:[_editServerModules objectForKey:#"AllModules"]]; //initialize a copy of AllModules dictionary
Now, I am simply trying to store a custom ServerModule object in this dictionary, and sync it. The following code attempts to do this:
//Create new ServerModule
ServerModule* newServer = [[ServerModule alloc]initWithUUID];
newServer.name = self.tf_name.text;
newServer.ip = self.tf_ip.text;
newServer.port = self.tf_port.text;
newServer.username = self.tf_user.text;
newServer.password = self.tf_pass.text;
//Add the ServerModule to AllModules dictionary with the key of its identifier
[_AllModules setObject:newServer forKey:newServer.identifier];
[self updateData];
[_editServerModules synchronize];
The identifier is a string which is set in the constructor of ServerModule. Here is the code for updateData.
[_editServerModules setObject:_AllModules forKey:#"AllModules"];
In case you are wondering, the object at #"AllModules" is initialized in the AppDelegate as follows:
NSMutableDictionary* AllModules = [[NSMutableDictionary alloc]init];
Once again, here is the error I am getting when I try to save something:
Attempt to set a non-property-list object {
"42E9EEA0-9051-4E2A-81EA-DC8FC5639C26" = "<ServerModule: 0x8ac4e50>";
} as an NSUserDefaults value for key AllModules
Thanks for any help!
~Carpetfizz
You can only store property list types (array, data, string, number, date, dictionary) or urls in NSUserDefaults. This means that everything, including any nested dictionary values, must be property list types. You'll want to implement the NSCoding protocol on your ServerModule object and then use NSKeyedArchiver to serialize your data before storing it and and NSKeyedUnarchiver to deflate your data after reading it back out of NSUserDefaults.
For example, given the properties you've shown exist on ServerModule objects, I'd add the following NSCoding protocol methods to your ServerModule implementation:
#pragma mark - NSCoding support
-(void)encodeWithCoder:(NSCoder*)encoder {
[encoder encodeObject:self.name forKey:#"name"];
[encoder encodeObject:self.ip forKey:#"ip"];
[encoder encodeObject:self.port forKey:#"port"];
[encoder encodeObject:self.username forKey:#"username"];
[encoder encodeObject:self.password forKey:#"password"];
}
-(id)initWithCoder:(NSCoder*)decoder {
self.name = [decoder decodeObjectForKey:#"name"];
self.ip = [decoder decodeObjectForKey:#"ip"];
self.port = [decoder decodeObjectForKey:#"port"];
self.username = [decoder decodeObjectForKey:#"username"];
self.password = [decoder decodeObjectForKey:#"password"];
return self;
}
And then of course you'll need to serialize:
NSData* archivedServerModules = [NSKeyedArchiver archivedDataWithRootObject:_AllModules];
[_editServerModules setObject:archivedServerModules forKey:#"AllModules"];
and deflate appropriately:
NSData* archivedServerModules = [_editServerModules objectForKey:#"AllModules"];
NSDictionary* serverModules = [NSKeyedUnarchiver unarchiveObjectWithData:archivedServerModules];
Hopefully that gives you an idea of what I'm talking about.

NSCoding of NSMutableDictionaries containing custom objects

I was trying to serialize a SearchEntity object(custom object) containing an NSMutableDictionary containing a set of type CategoryEntity(custom object).
1 SearchEntity<NSCoding> containing:
1 NSMutableDictionary (parameters)
parameters containing
X CategoryEntities<NSCoding> containing just strings and numbers.
At this line [encoder encodeObject:parameters forKey:kPreviousSearchEntityKey]; in the SearchEntity encodeWithCoder" I get GDB:Interrupted every time, no error message, exception etc. just GDB:Interrupted.
This is the implementation in SearchEntity and parameters is the NSMutableDictionary
#pragma mark -
#pragma mark NSCoding delegate methods
- (void) encodeWithCoder:(NSCoder*)encoder
{
//encode all the values so they can be persisted in NSUserdefaults
if (parameters)
[encoder encodeObject:parameters forKey:kPreviousSearchEntityKey]; //GDB:Interrupted!
}
- (id) initWithCoder:(NSCoder*)decoder
{
if (self = [super init])
{
//decode all values to return an object from NSUserdefaults in the same state as when saved
[self setParameters:[decoder decodeObjectForKey:kPreviousSearchEntityKey]];
}
return self;
}
The CategoryEntity also implements the NSCoding protocol and looks like this:
- (void) encodeWithCoder:(NSCoder*)encoder
{
//encode all the values so they can be persisted in NSUserdefaults
[encoder encodeObject:ID forKey:kIDKey];
[encoder encodeObject:text forKey:kTextKey];
[encoder encodeObject:category forKey:kCategoryKey];
[encoder encodeObject:categoryIdentifierKey forKey:kCategoryIdentifierKey];
}
- (id) initWithCoder:(NSCoder*)decoder
{
if (self = [super init]) {
//decode all values to return an object from NSUserdefaults in the same state as when saved
[self setID:[decoder decodeObjectForKey:kIDKey]];
[self setText:[decoder decodeObjectForKey:kTextKey]];
[self setCategory:[decoder decodeObjectForKey:kCategoryKey]];
[self setCategoryIdentifierKey:[decoder decodeObjectForKey:kCategoryIdentifierKey]];
}
return self;
}
I try to encode it from a wrapper for NSUserDefaults, like this:
+ (void) setPreviousSearchParameters:(SearchParameterEntity*) entity
{
if (entity)
{
//first encode the entity (implements the NSCoding protocol) then save it
NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:entity];
[[self defaults] setObject:encodedObject forKey:kPreviousSearchKey];
[[self defaults] synchronize];
}
}
+ (SearchParameterEntity*) getPreviousSearchParameters
{
//retrieve the encoded NSData object that was saved, decode and return it
SearchParameterEntity *entity = nil;
NSData *encodedObject = [[self defaults] objectForKey:kPreviousSearchKey];
if (encodedObject)
entity = [NSKeyedUnarchiver unarchiveObjectWithData:encodedObject];
return entity;
}
I was thinking that when I ask to Serialize the SearchEntity, it would start to serialize the 'parameters' mutableDictionary object, NSCoder will call "encode" on the CategoryEntities contained in the dictionary and they will all respond with their correct encoded objects.
However I just get GDB:Interrupted in the bottom of the console.
How can I debug this?
And is my approach wrong, should I wrap all levels of encoding in NSData?
Ps. I do the exact same thing with a ResultEntity containing NSArrays of CategoryEntities, it encodes with no problems, so I guess the NSMutableDictionary is the only thing sticking out.
The code you have posted does not appear to be incorrect. I've made a best guess at some details you've left out and I get a successful result from a test program containing your code with enough boilerplate to show that it encodes/decodes correctly.
(You can compile it from the command line using: gcc -framework foundation test.m -o test and run with: ./test.)
With regard to your question, how can I debug this, I would suggest an approach as follows:
(Temporarily) modify your code to be as simple as possible. For example, you could change the parameters property to a plain NSString and verify that works correctly first.
Slowly add in complexity, introducing one new property at a time, until the error starts occurring again. Eventually you will narrow down where the troublesome data is coming from.
Alas, if this is occurring due to some mis-managed memory elsewhere in your app, debugging this code itself may not get you anywhere. Try (manually) verifying that memory is managed correctly for each piece of data you are receiving for encoding.
If you are already using Core Data you could consider persisting just the object ID in the user defaults and restore your object graph based on that. (See: Archiving NSManagedObject with NSCoding).
I suggest you to bypass the NSMutableArray first. Let SearchEntity contains only one CategoryEntity and see if it works.
The code you posted looks good, you may want to give us more detailed context.
For object encoding, this file may help: DateDetailEntry
The problem with archiving objects with NSKeyedArchiver is that you cannot encode mutable objects. Only instances of NSArray, NSDictionary, NSString, NSDate, NSNumber, and NSData (and some of their subclasses) can be serialized
So, in your SearchEntity method encodeWithCoder: you should try creating NSDictionary from NSMutableDictionary and then encoding the immutable one:
if (parameters) {
NSDictionary *dict = [NSDictionary dictionaryWithDictionary:parameters];
[encoder encodeObject:dict forKey:kPreviousSearchEntityKey];
}
Also in the initWithCoder: method try creating NSMutableDictionary from the encoded immutable one:
NSDictionary *dict = [decoder decodeObjectForKey:kPreviousSearchEntityKey];
[self setParameters:[NSMutableDictionary dictionaryWithDictionary:dict]];
Also check for all obejct within parameters dictionary to conform to NSCoding protocol and ensure that all of them encode only immutable objects in their encodeWithCoder: methods.
Hope it solves the problem.

Resources