Saving custom class object's array in NSUserDefaults? - ios

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.

Related

Objective-C Writing general getter and setter methods

In my project I have a settings class with properties with custom setters that access NSUserDefaults to make everything simpler. The idea is that Settings class has
#property NSString *name
which has custom getter that gets the name value from NSUserDefaults and a setter that saves the new value there. In this way throughout the whole project I interact with the Settings class only to manage user defined preferences. The thing is that it seems way too repetitive to write all the getters and setters (I have about 50 properties), and would like to create one setter and one getter that would work for all variables. My only issue is how to get hold of the name of the variable within the setter.
The final question then is: is it possible to find out within a getter or setter for which property is the function being called?
If you have some other approach I would appreciate it too but considering that I would like to keep all the NSUserDefaults stuff in one class, I can't think of an alternative that doesnt include writing 50 getters and setters.
Thanks!
Another approach could be this.
No properties, just key value subscript.
#interface DBObject : NSObject<NSCoding>
+ (instancetype)sharedObject;
#end
#interface NSObject(SubScription)
- (id)objectForKeyedSubscript:(id)key;
- (void)setObject:(id)obj forKeyedSubscript:(id <NSCopying>)key;
#end
On the implementation file:
+ (instancetype)sharedObject {
static DBObject *sharedObject = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedObject = [[DBObject alloc] init];
});
return sharedObject;
}
- (id)objectForKeyedSubscript:(id)key {
return [[NSUserDefaults standardUserDefaults] objectForKey:key];
}
- (void)setObject:(id)obj forKeyedSubscript:(id <NSCopying>)key {
[[NSUserDefaults standardUserDefaults] setObject:obj forKeyedSubscript:key];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Now, you can use it like this:
// You saved it in NSUserDefaults
[DBObject sharedObject][#"name"] = #"John";
// You retrieve it from NSUserDefaults
NSLog(#"Name is: %#", [DBObject sharedObject][#"name"]);
I this this is the best approach and is what i will use in the future.
The setter and getter in this case is simple, you can do like this:
- (void)setName:(NSString *)name {
[[NSUserDefaults standardUserDefaults] setObject:name forKey:#"name"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSString *)name {
return [[NSUserDefaults standardUserDefaults] objectForKey:#"name"];
}
If you want to use a simple approach for all properties:
- (id)objectForKey:(NSString *)key {
return [[NSUserDefaults standardUserDefaults] objectForKey:key];
}
- (void)setObject:(id)object forKey:(NSString *)key {
[[NSUserDefaults standardUserDefaults] setObject:object forKey:key];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Instead of creating many properties, create many keys, each key is something you want to save or retrieve.
Example of keys:
static NSString *const kName = #"name";
static NSString *const kLastName = #"lastName";
I found your question very interesting and I said to myself "Challenge accepted!".
I've created this project on Github.
Basically, all you have to do is subclass the VBSettings class and then declare de properties, like this:
#interface MySettings : VBSettings
#property (strong, nonatomic) NSString *hello;
#end
The value of "hello" will be saved to NSUserDefaults with the key "hello". Example of usage:
MySettings settings = [[MySettings alloc] init];
settings.hello = "World!"; //The value is saved in NSUserDefaults
NSLog(#"%#", settings.hello); //The value is restored from NSUserDefaults.
One possibility would be to use KVO to detect when your properties change.
E.g.:
#interface Settings : NSObject
#property NSString *one;
#property NSString *two;
#end
#implementation Settings
- (instancetype)init
{
self = [super init];
if (self) {
[self addObserver:self forKeyPath:#"one" options:NSKeyValueObservingOptionNew context:NULL];
[self addObserver:self forKeyPath:#"two" options:NSKeyValueObservingOptionNew context:NULL];
}
return self;
}
- (void)dealloc
{
[self removeObserver:self forKeyPath:#"one"];
[self removeObserver:self forKeyPath:#"two"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
NSLog(#"Key: %#, Change: %#", keyPath, change);
}
#end
In a different class, use the standard property access:
Settings *settings = [[Settings alloc] init];
settings.one = #"something for one";
The Settings object logs:
Key: one, Change: {
kind = 1;
new = "something for one"; }
You could try to use dynamic Getter and Setter declarations as noted in this answer.
First create generic functions that you want all the properties to use:
- (id)_getter_
{
return [[NSUserDefaults standardUserDefaults] objectForKey:NSStringFromSelector(_cmd)];
}
- (void)_setter_:(id)value
{
//This one's _cmd name has "set" in it and an upper case first character
//This could take a little work to parse out the parameter name
[[NSUserDefaults standardUserDefaults] setObject:object forKey:YourParsedOutKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Then create the dynamic method generator:
+(void)synthesizeForwarder:(NSString*)getterName
{
NSString*setterName=[NSString stringWithFormat:#"set%#%#:",
[[getterName substringToIndex:1] uppercaseString],[getterName substringFromIndex:1]];
Method getter=class_getInstanceMethod(self, #selector(_getter_));
class_addMethod(self, NSSelectorFromString(getterName),
method_getImplementation(getter), method_getTypeEncoding(getter));
Method setter=class_getInstanceMethod(self, #selector(_setter_:));
class_addMethod(self, NSSelectorFromString(setterName),
method_getImplementation(setter), method_getTypeEncoding(setter));
}
Then set what strings you want to create dynamic getters and setters for:
+(void)load
{
for(NSString*selectorName in [NSArray arrayWithObjects:#"name", #"anything", #"else", #"you", #"want",nil]){
[self synthesizeForwarder:selectorName];
}
}
That will create getters and setters for any variable name that you add to the array. I'm not sure how well this will work when other classes try to call these methods, the compiler won't see them at compile time so may throw errors when you try to use them. I just combined 2 other
StackOverflow questions into this one answer for your situation.
Dynamic Getters and Setters.
Get current Method name.
As I understand it, you don't want the mental overhead of setObject:forKey: and objectForKey: method calls by the user of this class.
Here is how to get round it. I am leaving a lot of gaps for you to fill in.
Declare the property in the header file, so that callers can use it:
#property NSString *something;
#property NSString *somethingElse;
In the class file itself, declare that you are defining the properties, so that the compiler doesn't get upset:
#dynamic something,somethingElse;
In the class file, implement the methodSignatureForSelector function, like this:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{if (SelectorIsGetter(aSelector))
return [NSMethodSignature signatureWithObjCTypes:"##:"];
if (SelectorIsSetter(aSelector))
return [NSMethodSignature signatureWithObjCTypes:"v#:#"];
return nil;
}
This will tell the system to call forwardInvocation: for these selectors, and will also tell it the shape of the call that is being made.
Your implementation of SelectorIsGetter and SelectorIsSetter is up to you. You will probably use NSStringFromSelector(aSelector) to get the selector's name, and then look it up in a table of names to see if it matches any of the names of the selectors you are implementing: in this case something and somethingElse for the getters and setSomething: and setSomethingElse: for the setters.
In the class file, implement the forwardInvocation: function, like this:
-(void)forwardInvocation:(NSInvocation *)anInvocation
{if (SelectorIsGetter(anInvocation.selector))
{NSString *s=[self objectForKey:NSStringFromSelector(anInvocation.selector)];
[anInvocation setReturnValue:&s];
return;
};
if (SelectorIsSetter(anInvocation.selector))
{NSString *s;
[anInvocation getArgument:&s atIndex:2];
[self setObjectForKey:UnmangleName(NSStringFromSelector(anInvocation.selector))];
return;
};
[super forwardInvocation:anInvocation];
}
…where UnmangleName is a thoroughly tedious function that takes a string like "setSomething:" and turns it into a string like "something".
If you want to do more than just NSStrings, the extension is reasonably straightforward.

Save NSMutableArray in iOS

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.

How can I store a string and corresponding tags?

I have a bunch of activities that are specific to age groups and genders (not trying to be agist or sexist here, just some groups are more likely to take part in these activities so I am trying to suggest them using tags)
I basically need an array that would store this information.
I want a multidimensional array that essentially looks like this:
["Playing Video Games", 10, 24, "male"],
["Putting on Makeup", 14, 50, "female"],
["Sleeping", 0, 100, "both"]
["Activity name string", age lower limit, age upper limit, "genders allowed"];
How can I create this in iOS?
I'll be storing it in the userDefaults.
Objective-C is an object-oriented programming language. So, you need to create a custom class called, maybe 'Person' and then add the required properties.
For example, the custom class's .h file you require will be something like :
#interface Person : NSObject
#property (nonatomic, assign) NSString *activity;
#property (nonatomic, assign) NSString *sex;
#property int ageLowerLimit;
#property int ageUpperLimit;
#end
Then you can manage such objects by first,
Importing this custom class in your code :
#import "Person.h"
And then creating a new Person type object :
Person *firstPerson = [[Person alloc] init];
[firstPerson setActivity : #"Sleeping"];
[firstPerson setSex : #"Male"];
[firstPerson setAgeLowerLimit : 0];
[firstPerson setAgeUpperLimit : 100];
To store them you could use CoreData or just the good old NSUserDefaults.
Here's how to go about the NSUserDefaults approach :
You first store the various Person objects in an NSMutableArray, then you synchronise the defaults :
NSMutableArray *people = [[NSMutableArray alloc] init];
[people addObject : firstPerson];
NSUserDefaults *defaults = [NSUserDefaults standardDefaults];
[defaults setObject:people forKey:#"PEOPLE"];
[defaults synchronise];
To use these objects later, do this :
NSUserDefaults *defaults = [NSUserDefaults standardDefaults];
NSMutableArray *savedPeople = [defaults objectForKey:#"PEOPLE"];
Person *person1 = [savedPeople objectAtIndex:0];
Robert is right and u should use CoreData.
There is an nice [Core Data Tutorial][1] which explains the basics.
WHile using UserDefaults you can store your Data in an Array
[[NSUserDefaults standardUserDefaults] setObject:myArray forKey:#"myArray"];

Best way to store iPhone data (custom NSObject)

I have an NSMutableArray in which I store objects called "address". An address is an NSObject with 4-5 properties (NSStrings). The NSMutableArray shall contain a maximum of 15 address object.
What is the best way to store that array on the iPhone? Core data? NSUserDefaults? Should I maybe store every address object by itself, and not all objects in one NSMutableArray? In that case what should I do on the iPhone?
as #rog said, you may use NSUserDefaults to save data
& you should make your object follow protocal NSCoding
for examplem if you object is "YouObject"
#interface YouObject: NSObject {
}
#property (nonatomic, copy) NSString *uid;
#property (nonatomic, copy) NSString *name;
#end
//implement this 2 method
- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
self.title = [decoder decodeObjectForKey:#"uid"];
self.author = [decoder decodeObjectForKey:#"name"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:title forKey:#"uid"];
[encoder encodeObject:author forKey:#"name"];
}
then archive or unarchive using NSUserDefaults
//archive
YouObject *object = [YouObject ....]
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object ];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:#"address"];
//unarchive
NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:#"address"];
YouObject *object = (YouObject *)[NSKeyedUnarchiver unarchiveObjectWithData:data];
or if you have a YouObject Array, you can save the NSArray in the same way;
//archive
NSArray *addresses;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:address ];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:#"address"];
//unarchive
NSData *addressData = [[NSUserDefaults standardUserDefaults] objectForKey:#"address"];
NSArray *addresses = (NSArray*)[NSKeyedUnarchiver unarchiveObjectWithData:address];
For what you're describing, I think NSUserDefaults will suffice. See this post: How to store custom objects in NSUserDefaults. You can learn more about the limitations of NSUserDefaults here: What are the limitations of NSUserDefaults.
However, if you're saving/loading a large amount of data, then you should consider using Core Data.
Simplest way would to use the nsmutablearray read and write methods. All the data has to be plist data type nsarray nsdictionary nsstring nsnumber nsdate. Nsuserdefault like rog suggested is also good. As long as the amount of data remains small.

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