How to store custom objects in NSUserDefaults

141,279

Solution 1

On your Player class, implement the following two methods (substituting calls to encodeObject with something relevant to your own object):

- (void)encodeWithCoder:(NSCoder *)encoder {
    //Encode properties, other class variables, etc
    [encoder encodeObject:self.question forKey:@"question"];
    [encoder encodeObject:self.categoryName forKey:@"category"];
    [encoder encodeObject:self.subCategoryName forKey:@"subcategory"];
}

- (id)initWithCoder:(NSCoder *)decoder {
    if((self = [super init])) {
        //decode properties, other class vars
        self.question = [decoder decodeObjectForKey:@"question"];
        self.categoryName = [decoder decodeObjectForKey:@"category"];
        self.subCategoryName = [decoder decodeObjectForKey:@"subcategory"];
    }
    return self;
}

Reading and writing from NSUserDefaults:

- (void)saveCustomObject:(MyObject *)object key:(NSString *)key {
    NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:encodedObject forKey:key];
    [defaults synchronize];

}

- (MyObject *)loadCustomObjectWithKey:(NSString *)key {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSData *encodedObject = [defaults objectForKey:key];
    MyObject *object = [NSKeyedUnarchiver unarchiveObjectWithData:encodedObject];
    return object;
}

Code shamelessly borrowed from: saving class in nsuserdefaults

Solution 2

Swift 4 introduced the Codable protocol which does all the magic for these kinds of tasks. Just conform your custom struct/class to it:

struct Player: Codable {
  let name: String
  let life: Double
}

And for storing in the Defaults you can use the PropertyListEncoder/Decoder:

let player = Player(name: "Jim", life: 3.14)
UserDefaults.standard.set(try! PropertyListEncoder().encode(player), forKey: kPlayerDefaultsKey)

let storedObject: Data = UserDefaults.standard.object(forKey: kPlayerDefaultsKey) as! Data
let storedPlayer: Player = try! PropertyListDecoder().decode(Player.self, from: storedObject)

It will work like that for arrays and other container classes of such objects too:

try! PropertyListDecoder().decode([Player].self, from: storedArray)

Solution 3

I create a library RMMapper (https://github.com/roomorama/RMMapper) to help save custom object into NSUserDefaults easier and more convenient, because implementing encodeWithCoder and initWithCoder is super boring!

To mark a class as archivable, just use: #import "NSObject+RMArchivable.h"

To save a custom object into NSUserDefaults:

#import "NSUserDefaults+RMSaveCustomObject.h"
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults rm_setCustomObject:user forKey:@"SAVED_DATA"];

To get custom obj from NSUserDefaults:

user = [defaults rm_customObjectForKey:@"SAVED_DATA"]; 

Solution 4

If anybody is looking for a swift version:

1) Create a custom class for your data

class customData: NSObject, NSCoding {
let name : String
let url : String
let desc : String

init(tuple : (String,String,String)){
    self.name = tuple.0
    self.url = tuple.1
    self.desc = tuple.2
}
func getName() -> String {
    return name
}
func getURL() -> String{
    return url
}
func getDescription() -> String {
    return desc
}
func getTuple() -> (String,String,String) {
    return (self.name,self.url,self.desc)
}

required init(coder aDecoder: NSCoder) {
    self.name = aDecoder.decodeObjectForKey("name") as! String
    self.url = aDecoder.decodeObjectForKey("url") as! String
    self.desc = aDecoder.decodeObjectForKey("desc") as! String
}

func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(self.name, forKey: "name")
    aCoder.encodeObject(self.url, forKey: "url")
    aCoder.encodeObject(self.desc, forKey: "desc")
} 
}

2) To save data use following function:

func saveData()
    {
        let data  = NSKeyedArchiver.archivedDataWithRootObject(custom)
        let defaults = NSUserDefaults.standardUserDefaults()
        defaults.setObject(data, forKey:"customArray" )
    }

3) To retrieve:

if let data = NSUserDefaults.standardUserDefaults().objectForKey("customArray") as? NSData
        {
             custom = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [customData]
        }

Note: Here I am saving and retrieving an array of the custom class objects.

Solution 5

Taking @chrissr's answer and running with it, this code can be implemented into a nice category on NSUserDefaults to save and retrieve custom objects:

@interface NSUserDefaults (NSUserDefaultsExtensions)

- (void)saveCustomObject:(id<NSCoding>)object
                     key:(NSString *)key;
- (id<NSCoding>)loadCustomObjectWithKey:(NSString *)key;

@end


@implementation NSUserDefaults (NSUserDefaultsExtensions)


- (void)saveCustomObject:(id<NSCoding>)object
                     key:(NSString *)key {
    NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];
    [self setObject:encodedObject forKey:key];
    [self synchronize];

}

- (id<NSCoding>)loadCustomObjectWithKey:(NSString *)key {
    NSData *encodedObject = [self objectForKey:key];
    id<NSCoding> object = [NSKeyedUnarchiver unarchiveObjectWithData:encodedObject];
    return object;
}

@end

Usage:

[[NSUserDefaults standardUserDefaults] saveCustomObject:myObject key:@"myKey"];
Share:
141,279

Related videos on Youtube

Ethan Mick
Author by

Ethan Mick

I am a software engineer and entrepreneur. Hopefully I'll do something cool, so keep in touch.

Updated on July 09, 2020

Comments

  • Ethan Mick
    Ethan Mick almost 4 years

    Alright, so I've been doing some poking around, and I realize my problem, but I don't know how to fix it. I have made a custom class to hold some data. I make objects for this class, and I need to them to last between sessions. Before I was putting all my information in NSUserDefaults, but this isn't working.

    -[NSUserDefaults setObject:forKey:]: Attempt to insert non-property value '<Player: 0x3b0cc90>' of class 'Player'.
    

    That is the error message I get when I put my custom class, "Player", in the NSUserDefaults. Now, I've read up that apparently NSUserDefaults only stores some types of information. So how an I get my objects into NSUSerDefaults?

    I read that there should be a way to to "encode" my custom object and then put it in, but I'm not sure how to implement it, help would be appreciated! Thank you!

    ****EDIT****

    Alright, so I worked with the code given below (Thank you!), but I'm still having some issues. Basically, the code crashes now and I'm not sure why, because it doesn't give any errors. Perhaps I'm missing something basic and I'm just too tired, but we'll see. Here is the implementation of my Custom class, "Player":

    @interface Player : NSObject {
        NSString *name;
        NSNumber *life;
        //Log of player's life
    }
    //Getting functions, return the info
    - (NSString *)name;
    - (int)life;
    
    
    - (id)init;
    
    //These are the setters
    - (void)setName:(NSString *)input; //string
    - (void)setLife:(NSNumber *)input; //number    
    
    @end
    

    Implementation File:

    #import "Player.h"
    @implementation Player
    - (id)init {
        if (self = [super init]) {
            [self setName:@"Player Name"];
            [self setLife:[NSNumber numberWithInt:20]];
            [self setPsnCounters:[NSNumber numberWithInt:0]];
        }
        return self;
    }
    
    - (NSString *)name {return name;}
    - (int)life {return [life intValue];}
    - (void)setName:(NSString *)input {
        [input retain];
        if (name != nil) {
            [name release];
        }
        name = input;
    }
    - (void)setLife:(NSNumber *)input {
        [input retain];
        if (life != nil) {
            [life release];
        }
        life = input;
    }
    /* This code has been added to support encoding and decoding my objecst */
    
    -(void)encodeWithCoder:(NSCoder *)encoder
    {
        //Encode the properties of the object
        [encoder encodeObject:self.name forKey:@"name"];
        [encoder encodeObject:self.life forKey:@"life"];
    }
    
    -(id)initWithCoder:(NSCoder *)decoder
    {
        self = [super init];
        if ( self != nil )
        {
            //decode the properties
            self.name = [decoder decodeObjectForKey:@"name"];
            self.life = [decoder decodeObjectForKey:@"life"];
        }
        return self;
    }
    -(void)dealloc {
        [name release];
        [life release];
        [super dealloc];
    }
    @end
    

    So that's my class, pretty straight forward, I know it works in making my objects. So here is the relevant parts of the AppDelegate file (where I call the encryption and decrypt functions):

    @class MainViewController;
    
    @interface MagicApp201AppDelegate : NSObject <UIApplicationDelegate> {
        UIWindow *window;
        MainViewController *mainViewController;
    }
    
    @property (nonatomic, retain) IBOutlet UIWindow *window;
    @property (nonatomic, retain) MainViewController *mainViewController;
    
    -(void)saveCustomObject:(Player *)obj;
    -(Player *)loadCustomObjectWithKey:(NSString*)key;
    
    
    @end
    

    And then the important parts of the implementation file:

        #import "MagicApp201AppDelegate.h"
        #import "MainViewController.h"
        #import "Player.h"
    
        @implementation MagicApp201AppDelegate
    
    
        @synthesize window;
        @synthesize mainViewController;
    
    
        - (void)applicationDidFinishLaunching:(UIApplication *)application {
        NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
            //First check to see if some things exist
            int startup = [prefs integerForKey:@"appHasLaunched"];
            if (startup == nil) {
    //Make the single player 
            Player *singlePlayer = [[Player alloc] init];
            NSLog([[NSString alloc] initWithFormat:@"%@\n%d\n%d",[singlePlayer name], [singlePlayer life], [singlePlayer psnCounters]]); //  test
            //Encode the single player so it can be stored in UserDefaults
            id test = [MagicApp201AppDelegate new];
            [test saveCustomObject:singlePlayer];
            [test release];
    }
    [prefs synchronize];
    }
    
    -(void)saveCustomObject:(Player *)object
    { 
        NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
        NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];
        [prefs setObject:myEncodedObject forKey:@"testing"];
    }
    
    -(Player *)loadCustomObjectWithKey:(NSString*)key
    {
        NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
        NSData *myEncodedObject = [prefs objectForKey:key ];
        Player *obj = (Player *)[NSKeyedUnarchiver unarchiveObjectWithData: myEncodedObject];
        return obj;
    }
    

    Eeee, sorry about all the code. Just trying to help. Basically, the app will launch and then crash immediatly. I've narrowed it down to the encryption part of the app, that's where it crashes, so I'm doing something wrong but I'm not sure what. Help would be appreciated again, thank you!

    (I haven't gotten around to decrypting yet, as I haven't gotten encrypting working yet.)

    • chrissr
      chrissr about 14 years
      Do you have a stack trace or more information about the crash, such as which line number is causing the crash? I'm not immediately seeing anything wrong with the code, so a starting point would be helpful.
    • broot
      broot over 13 years
      In above example you have used encodeObject to store self.life which is an int. You should use encodeInt instead.
  • Ethan Mick
    Ethan Mick about 14 years
    Edited my post above to reflect my changes.
  • Maggie
    Maggie over 12 years
    @chrissr you have an error in NSUserDefaults defaults = [NSUserDefaults standardUserDefaults]; ... should be NSUserDefaults *defaults.
  • karim
    karim over 12 years
    Hey guys/girls, don't forget to add a [defaults synchronize] as the last line in the saveCustomObject method.
  • matzino
    matzino over 12 years
    @karim synchronize is automatically invoked at periodic intervals, use this method only if you cannot wait for the automatic synchronization! From the apple-doc developer.apple.com/library/mac/#documentation/Cocoa/Referen‌​ce/…
  • BadPirate
    BadPirate about 12 years
    NSKeyedArchiver rocks... seems it even automatically descends into NSArray or NSDictionary objects and encodes any encodable custom objects within.
  • Rubycon
    Rubycon over 11 years
    I can use NSKeyedArchiver without implementing NSCoding. So, why i must use NSCoding, for what reason?
  • theDuncs
    theDuncs over 11 years
    So I have to code those methods for every property I have on every custom class! Surely there's a simpler way to do this?!
  • Samhan Salahuddin
    Samhan Salahuddin over 11 years
    I'm not being pedantic , just a genuine question , isnt it against apples guidelines to use synthesized setters ie self.property in init methods ?
  • Erik B
    Erik B over 10 years
    @eddardstark developer.apple.com/library/mac/documentation/Cocoa/Conceptu‌​al/… So yes, you shouldn't do it, but it's generally safe. I don't think I ever encountered a bug that was caused by an accessor method being called in init/dealloc and I've worked in several projects where it was done quite frequently.
  • Samhan Salahuddin
    Samhan Salahuddin over 10 years
    @Erik B :- It has to do with KVO i think. If theres no KVO involved its safe.
  • Erik B
    Erik B over 10 years
    Your library assumes that you have properties for everything you want to persist and that you want to persist everything you have properties for. If this is true, your library could certainly be helpful, but I think you will find that in many cases it isn't.
  • thomasdao
    thomasdao over 10 years
    It is useful to persist plain object only, I would say its usage is pretty similar to Gson in Java. What cases you are looking at?
  • oopology
    oopology over 10 years
    would this work for CCSprites also or do they have to be handled differently?
  • Albara
    Albara over 10 years
    That was really helpful, glad I kept reading on the answers ;D
  • GoRoS
    GoRoS almost 10 years
    @chrissr Please change NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:object]; for NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];. Variables don't match.
  • Shial
    Shial over 9 years
    what about when my custom object have in property other custom object ?
  • thomasdao
    thomasdao over 9 years
    @Shial: if you make other custom object archivable, it will be saved too
  • Shial
    Shial over 9 years
    @thomasdao thanks for your answear. But i recive some problems cus I'm using ReactiveCocoa. I have no idea from where he took didSubscribe property name.
  • RomanKousta
    RomanKousta over 9 years
    Don't you have to implement NSCoding protocol?
  • Stone
    Stone almost 9 years
    If you use the CustomClass in another Target, don't forget these steps: stackoverflow.com/questions/27212360/…
  • Nishant
    Nishant almost 9 years
    unable to save an array of core data objects by this class.
  • Arpit B Parekh
    Arpit B Parekh almost 9 years
    I can save my custom objects by this valuable class.
  • Lean-Juc Dicarp
    Lean-Juc Dicarp over 8 years
    As I see, your library RMMapper adds categories to NSObject, so that every NSObject implements the Methods "encodeWithCoder" and "initWithCoder". And your copy category adds "copyWithZone" to every instance of NSObject. Be aware, that existing classes (also from Apple) who already implements these methods could get into trouble. You should not "override" existing methods in categories. Correct me if I'm wrong.
  • thomasdao
    thomasdao over 8 years
    @Lumpy yes you are correct. I am aware of this may interfere with other classes. My solution is short and most useful for plain model. Some developers who want to archive their model to NSUserDefaults, but cannot do so easily, will look for a quick way to implement it, and this utility is just the quickest way to get things done. It's possible to refactor it to a static method, but developer will need to have to create their own encodeWithCoder and call this static method. I'm not sure which is better, and open for discussion.
  • Hedylove
    Hedylove over 7 years
    Note people from the future that as of Swift 3, "synchronize" should not be called by you at all.
  • code4latte
    code4latte almost 7 years
    @JozemiteApps why? or can you post a link with explanation for this topic?
  • Hedylove
    Hedylove almost 7 years
    @code4latte I don't know if it was changed but Apple's documentation states that "The synchronize() method, which is automatically invoked at periodic intervals, keeps the in-memory cache in sync with a user’s defaults database." Before, it said that you should only call it IF you are sure you need the data saved instantly. I've read around a couple of time that the user should not call it anymore.
  • Tamás Sengel
    Tamás Sengel over 6 years
    Just a reminder: self is not mandatory before the variables in init and encode.
  • Nathan Barreto
    Nathan Barreto over 6 years
    Works like a charm
  • Abhishek Thapliyal
    Abhishek Thapliyal about 6 years
    Can you show how you are intializing object with NSCoder
  • Vyacheslav
    Vyacheslav about 6 years
    @AbhishekThapliyal, I do not understand you. init(coder aDecoder: NSCoder) initializes it
  • BallpointBen
    BallpointBen almost 6 years
    Note that you only get the Codable behavior for free when all instance members are themselves Codable -- otherwise, you have to write some encoding code yourself,
  • Sergiu Todirascu
    Sergiu Todirascu almost 6 years
    Or rather simply conform those member types to Codable too. And so on, recursively. Only in very custom cases you would need to write encoding code.
  • alstr
    alstr over 5 years
    The easiest way with Swift 4.