I need to save many NSMutableArray with my custom objects and I want to know what is the best way to do this.
Maybe NSUserDefaults is not the best way to do it.
What should I use?
If your array contains non-plist objects, then you cannot use NSUserDefaults without first encoding the array.
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
You'll want to encode it using NSKeyedArchiver. This will give you an NSData object that you can then store in NSUserDefaults, or write it to file through NSKeyedArchiver itself.
All you have to do is conform to NSCoding in your custom object, and override initWithCoder: to initialise your object when it's loaded and encodeWithCoder: to encode your variables when it gets encoded. For example, your custom object will look something like this:
#interface customArrayObject : NSObject <NSCoding>
#property (nonatomic) NSString* foo;
#property (nonatomic) NSInteger bar;
#end
#implementation customArrayObject
-(instancetype) initWithCoder:(NSCoder *)aDecoder { // decode variables
if (self = [super init]) {
_foo = [aDecoder decodeObjectForKey:#"foo"];
_bar = [aDecoder decodeIntegerForKey:#"bar"];
}
return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder { // encode variables
[aCoder encodeObject:_foo forKey:#"foo"];
[aCoder encodeInteger:_bar forKey:#"bar"];
}
#end
It's also worth noting that NSUserDefaults is used to store user preferences, and therefore if your array contains data that isn't in any way to do with a user preference, you shouldn't be using NSUserDefaults - you should be writing it to disk yourself.
Writing your array to disk is actually a lot more trivial than it sounds, you can use the archiveRootObject:toFile: method on NSKeyedArchiver. For example, this will write your custom array to the documents directory:
// Gets the documents directory path
NSString* documentsPath = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES)[0];
// Archive and save the file to foo.dat in the documents directory. Returns whether the operation was successful.
BOOL success = [NSKeyedArchiver archiveRootObject:customArray toFile:[NSString stringWithFormat:#"%#/%#", documentsPath, #"foo.dat"]]
However, it is also worth noting that this flat (yes/no) as to whether the operation was successful isn't that great when it comes to error handling. If you want to implement custom error handling, then you'll want to first encode the object using NSKeyedArchiver's archivedDataWithRootObject: method, and then NSData's writeToFile:options:error: method.
Related
I would like to serialize excerpts from a collection of objects that do not themselves conform to NSCoding to a file. What's the best way to achieve this without transformation the collected objects an intermediary representation (one that would conform to NSCoding)? The (apparent) problem arises, because NSKeyedArchiver requires unique keys, and there is no NSArchiver on iOS.
For instance, the following will not work, because values are overwritten inside the loop as keys are reused. Calculating unique key strings from some loop index would be possible but quite a nuisance:
for (MyObject *object in myObjects) {
NSString *someString = myObject.someString; // excerpted string
[archiver encodeObject: someString withKey: #"someString"]
}
One way would be to encode the extracted excepts as an array:
NSMutableArray *extractedStrings = [NSMutableArray array];
for (MyObject *object in myObjects) {
[extractedStrings addObject:object.someString];
}
[archiver encodeObject:extractedStrings forKey:#"extractedStrings"];
Adding a category that conforms to NSCoding has proved to be the best solution in this case:
#interface MyClass (Coding) <NSCoding>
// ...
How do I convert my NSManagedObject to NSData object?
I'm new to Core Data and the Multipeer Connectivity Framework.
I need to transfer data between 2 devices via the Multipeer Connectivity Framework. I understand that I cannot simply transfer via MPC since it requires an NSData object.
Are there any third-party libraries that provides such function?
I think NSCoding is not such a good idea here. The reason is that the objects will not be the same on two different devices due to their internal managed object IDs as well as a myriad of other possible problems that can occur in unexpected syncing scenarios.
I would strongly recommend to take the trouble and convert your object into a NSDictionary type and then use the standard NSData APIs on the dictionary (or an array of dictionaries).
try like this
You should use NSCoding protocol, then you can encode your object to NSData. Again If you want decode your original object use NSKeyedUnarchiver.
in .h
#interface Testting : NSManagedObject<NSCoding>
and this in .m
NSData *data=[NSKeyedArchiver archivedDataWithRootObject:hereyourObject];
//get your original object
Testting *Obj = [NSKeyedUnarchiver unarchiveObjectWithData:data];
You should use NSCoding protocol. Using NSKeyedAchiever you can encode your object to NSData. Again If you want decode your original object use NSKeyedUnarchiver.
#interface Test : NSManagedObject <NSCoding>
#property (nonatomic, retain) NSString *title;
#end
#implementation Test
#dynamic title;
- (id)initWithCoder:(NSCoder *)coder {
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;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.title forKey:#"title"];
}
#end
//converting to NSData
NSData *archivedObject = [NSKeyedArchiver archivedDataWithRootObject:testObj];
//get your original object
Test *testObj = [NSKeyedUnarchiver unarchiveObjectWithData:archivedObject];
I created a custom class to display in a TableView, I read a custom class object's array in TableView. Now, when I close the app I lose my date. I want to save that data permanently. But when I try to do so, I get the following error:
Attempt to set a non-property-list object as an NSUserDefaults
My code is:
#implementation CAChallangeListViewController
- (void)loadInitialData{
self.challangeItems = [[NSUserDefaults standardUserDefaults] objectForKey:#"challanges"];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[[NSUserDefaults standardUserDefaults] setObject:self.challangeItems forKey:#"challanges"];
}
Here, the self. Challange items is a NSMutable array which contains objects of type CAChallange which is a custom class with following interface.
#interface CAChallange : NSObject
#property NSString *itemName;
#property BOOL *completed;
#property (readonly) NSDate *creationDate;
#end
You can easily accomplish what you are doing by converting your object into a standard dictionary (and back when you need to read it).
NSMutableArray *itemsToSave = [NSMutableArray array];
for (CAChallange *ch in myTableItems) {
[itemsToSave addObject:#{ #"itemName" : ch.itemName,
#"completed" : #(ch.completed),
#"creationDate" : ch.creationDate }];
}
You can use NSKeyedArchiver to create an NSData representation of your object. Your class will need to conform to the NSCoding protocol, which can be a bit tedious, but the AutoCoding category can do the work for you. After adding that to your project, you can easily serialize your objects like this:
id customObject = // Your object to persist
NSData *customObjectData = [NSKeyedArchiver archivedDataWithRootObject:customObject];
[[NSUserDefaults standardUserDefaults] setObject:customObjectData forKey:#"PersistenDataKey"];
And deserialize it like this:
NSData *customObjectData = [[NSUserDefaults standardUserDefaults] objectForKey:#"PersistenDataKey"];
id customObject = [NSKeyedUnarchiver unarchiveObjectWithData:customObjectData];
Your CAChallange object has a pointer to a BOOL which is not a suitable type for a plist:-
#property BOOL *completed
This is probably causing the error. You cannot store raw pointers in a plist.
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.
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.