How to use NSUserDefaults to store a container class - ios

I'm trying to store a set of values under an NSUserDefaults key. I use a custom class to access an RSS feed and set the class's variables to match the info found in the feed. I then use another class to set the values under a NSUserDefaults key:
[infoStorageClass dataIsNew:self];
[infoStorageClass storeData:self];
The problem is that whenever I store my class I get this warning:
[NSUserDefaults setObject:forKey:]: Attempt to insert non-property value '(
"TARSSInfo: 0x80eb6a0"
)' of class '__NSArrayM'. Note that dictionaries and arrays in property lists must also contain only property values.
What's going on here? Thanks in advance for you help.

From documentation:
A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary.
In order to store an object of another type you first need to implement NSCoding protocol in the class of the object you want to store. Which means implement these methods and do decoding and encoding like this(a snippet of my own code of custom class BMDifficultyLevel):
- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
_difficultyLevel = [decoder decodeObjectForKey:#"difficulty"];
_difficultyLevelType = [decoder decodeIntegerForKey:#"type"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:_difficultyLevel forKey:#"difficulty"];
[encoder encodeInteger:_difficultyLevelType forKey:#"type"];
}
then before storing your object you need to archive and then store like this:
NSUserDefaults *defaults = [[NSUserDefaults alloc] init];
_defaultsDataWithLevelObject = [NSKeyedArchiver archivedDataWithRootObject:_difficultyLevel];
[defaults setObject:_defaultsDataWithLevelObject forKey:BMDifficultyLevelDefaultsKey];
where _defaultsDataWithLevelObject is an object of type NSData, which means eventually you store NSData object.
To retrieve your defaults you'll need to unarchive the object something like this:
_defaultsDataWithLevelObject = [[NSUserDefaults standardUserDefaults] objectForKey:BMDifficultyLevelDefaultsKey];
_difficultyLevel = [NSKeyedUnarchiver unarchiveObjectWithData:_defaultsDataWithLevelObject];

You should make your custom class implement the NSCoding protocol and then archive your array of instances. This will give you an NSData instance that you can store into user defaults.

Related

"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.

Save NSArray to NSUserDefaults issues (using custom object in array)

I try to save my object to NSUserDefaults. But when I call this method again it is not have any info about previous operation.
There is my method below:
- (void)addToCart {
if([[NSUserDefaults standardUserDefaults] objectForKey:kCart]) {
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
NSMutableArray *products = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:kCart]];
[products addObject:self.product];
[prefs setObject:products forKey:kCart];
[prefs synchronize];
[products release];
}
else {
//Saving...
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setObject:[NSArray arrayWithObjects:self.product, nil] forKey:kCart];
[prefs synchronize];
}
}
I need to save a collection with a products to NSUserDefault. I wrap my object to NSArray and save it but it doesn't work.
Everything put into NSUserDefaults must be a valid property list object (NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary). All collection elements must themselves also be property list objects.
In order to save non-PL objects into NSUserDefaults, you must first convert the object into a PL object. The most generic way to do this is by serializing it to NSData.
Serializing to NSData is handled with NSKeyedArchiver. See Storing NSColor in User Defaults for the canonical example of this. (That document is very old and still references NSArchiver which will work fine for this problem, but NSKeyedArchiver is now the preferred serializer.)
In order to archive using NSKeyedArchiver, your object must conform to NSCoding as noted by #harakiri.
You need to conform to the <NSCoding> protocol and implement -initWithCoder: and -encodeWithCoder: in your custom object.
See: https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Protocols/NSCoding_Protocol/Reference/Reference.html

How do I store and retrieve an NSMutableDictionary to and from NSUserDefaults?

I tried to do this to store an empty dictionary in NSUserDefaults.
NSMutableDictionary* fruits = [NSMutableDictionary alloc];
[defaults setObject:fruits forKey:#"fruits"];
and then later this to retrieve it.
NSMutableDictionary* fruits = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:#"fruits"]];
However, retrieving the dictionary crashes my application. Why? How do I store a dictionary in NSUserDefaults?
You get a immutable dictionary back. You do not need to "capsulate" it in another dictionary. If you want to make it mutable write:
NSMutableDictionary* animals = [[defaults objectForKey:#"animals"] mutableCopy];
The NSUserDefaults class provides convenience methods for accessing common types such as floats, doubles, integers, Booleans, and URLs. A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData.
Values returned from NSUserDefaults are immutable, even if you set a
mutable object as the value. For example, if you set a mutable string
as the value for "MyStringDefault", the string you later retrieve
using stringForKey: will be immutable.
Note: The user defaults system, which you programmatically access through the NSUserDefaults class, uses property lists to store objects representing user preferences. This limitation would seem to exclude many kinds of objects, such as NSColor and NSFont objects, from the user default system. But if objects conform to the NSCoding protocol they can be archived to NSData objects, which are property list–compatible objects. For information on how to do this, see ““Storing NSColor in User Defaults”“; although this article focuses on NSColor objects, the procedure can be applied to any object that can be archived.
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/nsuserdefaults_Class/Reference/Reference.html
You can use:
Save:
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:mutableArray];
[[NSUserDefaults standardUserDefaults] setObject:stack forKey:#"Your Key"];
Retrieve:
NSMutableArray *mutableArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];
You need to init your dictionary and set is as object later. This way works, it's the same as your example but just with properly initialization.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:#"someValue", #"someKey", nil];
[defaults setObject:dict forKey:#"slovnik"];
[dict release];
NSLog(#"READ: %#", [defaults objectForKey:#"slovnik"]);
NSMutableDictionary *newDict = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:#"slovnik"]];
NSLog(#"READ2: %#", newDict);
Now I get to log console and app do not crash:
2012-04-12 08:47:55.030 Test[12179:f803] READ: {
someKey = someValue;
}
2012-04-12 08:47:55.031 Test[12179:f803] READ2: {
someKey = someValue;
}
NSMutableDictionary* fruits = [NSMutableDictionary alloc];
should be
NSMutableDictionary* fruits = [[NSMutableDictionary alloc] init];
You need to always initialize objects after allocating them.

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.

Why NSUserDefaults failed to save NSMutableDictionary in iOS?

I'd like to save an NSMutableDictionary object in NSUserDefaults. The key type in NSMutableDictionary is NSString, the value type is NSArray, which contains a list of object which implements NSCoding. Per document, NSString and NSArray both are conform to NSCoding.
I am getting this error:
[NSUserDefaults setObject: forKey:]: Attempt to insert non-property value.... of class NSCFDictionary.
I found out one alternative, before save, I encode the root object (NSArray object) using NSKeyedArchiver, which ends with NSData. Then use UserDefaults save the NSData.
When I need the data, I read out the NSData, and use NSKeyedUnarchiver to convert NSData back to the object.
It is a little cumbersome, because i need to convert to/from NSData everytime, but it just works.
Here is one example per request:
Save:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *arr = ... ; // set value
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:arr];
[defaults setObject:data forKey:#"theKey"];
[defaults synchronize];
Load:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults objectForKey:#"theKey"];
NSArray *arr = [NSKeyedUnarchiver unarchiveObjectWithData:data];
The element in the array implements
#interface CommentItem : NSObject<NSCoding> {
NSString *value;
}
Then in the implementation of CommentItem, provides two methods:
-(void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:value forKey:#"Value"];
}
-(id)initWithCoder:(NSCoder *)decoder
{
self.value = [decoder decodeObjectForKey:#"Value"];
return self;
}
Anyone has better solution?
Thanks everyone.
If you're saving an object in user defaults, all objects, recursively, all the way down, must be property list objects. Conforming to NSCoding doesn't mean anything here-- NSUserDefaults won't automatically encode them into NSData, you have to do that yourself. If your "list of object which implements NSCoding" means objects that are not property list objects, then you'll have to do something with them before saving to user defaults.
FYI the property list classes are NSDictionary, NSArray, NSString, NSDate, NSData, and NSNumber. You can write mutable subclasses (like NSMutableDictionary) to user preferences but the objects you read out will always be immutable.
Are all of your keys in the dictionary NSStrings? I think they have to be in order to save the dictionary to a property list.
Simplest Answer :
NSDictionary is only a plist object , if the keys are NSStrings.
So, Store the "Key" as NSString with stringWithFormat.
Solution :
NSString *key = [NSString stringWithFormat:#"%#",[dictionary valueForKey:#"Key"]];
Benefits :
It will add String-Value.
It will add Empty-Value when your Value of Variable is NULL.
Have you considered looking at implementing the NSCoding Protocol? This will allow you encode and decode on the iPhone with two simple methods that are implemented with the NSCoding. First you would need to adding the NSCoding to your Class.
Here is an example:
This is in the .h file
#interface GameContent : NSObject <NSCoding>
Then you will need to implement two methods of the NSCoding Protocol.
- (id) initWithCoder: (NSCoder *)coder
{
if (self = [super init])
{
[self setFoundHotSpots:[coder decodeObjectForKey:#"foundHotSpots"]];
}
return self;
}
- (void) encodeWithCoder: (NSCoder *)coder
{
[coder encodeObject:foundHotSpots forKey:#"foundHotSpots"];
}
Check out the documentation on NSCoder for more information. That has come in really handy for my projects where I need to save the state of the application on the iPhone if the application is closed and restore it back to it's state when its back on.
The key is to add the protocol to the interface and then implement the two methods that are part of NSCoding.
I hope this helps!
There is no better solution. Another option would be to just save the coded object to disk - but that is doing the same thing. They both end up with NSData that gets decoded when you want it back.

Resources