101

I've recently been trying to store the search results of my iPhone app in the NSUserDefaults collection. I also use this to save user registration info successfully, but for some reason trying to store my NSMutableArray of custom Location classes always comes back empty.

I tried converting the NSMutableArray to a NSData element as of this post but I get the same result (Possible to save an integer array using NSUserDefaults on iPhone?)

The code samples I have tried are:

Save:

[prefs setObject:results forKey:@"lastResults"];
[prefs synchronize];

or

NSData *data = [NSData dataWithBytes:&results length:sizeof(results)];
[prefs setObject:data forKey:@"lastResults"];

or

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:results];
[prefs setObject:data forKey:@"lastResults"];

Load:

lastResults = (NSMutableArray *)[prefs objectForKey:@"lastResults"];

or

NSData *data = [prefs objectForKey:@"lastResults"];
memcpy(&lastResults, data.bytes, data.length);  

or

NSData *data = [prefs objectForKey:@"lastResults"];
lastResults = [NSKeyedUnarchiver unarchiveObjectWithData:data];

After following advice below I have also implemented NSCoder in my object (ignore the overuse of NSString its temporary):

#import "Location.h"


@implementation Location

@synthesize locationId;
@synthesize companyName;
@synthesize addressLine1;
@synthesize addressLine2;
@synthesize city;
@synthesize postcode;
@synthesize telephoneNumber;
@synthesize description;
@synthesize rating;
@synthesize priceGuide;
@synthesize latitude;
@synthesize longitude;
@synthesize userLatitude;
@synthesize userLongitude;
@synthesize searchType;
@synthesize searchId;
@synthesize distance;
@synthesize applicationProviderId;
@synthesize contentProviderId;

- (id) initWithCoder: (NSCoder *)coder
{
    if (self = [super init])
    {
        self.locationId = [coder decodeObjectForKey:@"locationId"];
        self.companyName = [coder decodeObjectForKey:@"companyName"];
        self.addressLine1 = [coder decodeObjectForKey:@"addressLine1"];
        self.addressLine2 = [coder decodeObjectForKey:@"addressLine2"];
        self.city = [coder decodeObjectForKey:@"city"];
        self.postcode = [coder decodeObjectForKey:@"postcode"];
        self.telephoneNumber = [coder decodeObjectForKey:@"telephoneNumber"];
        self.description = [coder decodeObjectForKey:@"description"];
        self.rating = [coder decodeObjectForKey:@"rating"];
        self.priceGuide = [coder decodeObjectForKey:@"priceGuide"];
        self.latitude = [coder decodeObjectForKey:@"latitude"];
        self.longitude = [coder decodeObjectForKey:@"longitude"];
        self.userLatitude = [coder decodeObjectForKey:@"userLatitude"];
        self.userLongitude = [coder decodeObjectForKey:@"userLongitude"];
        self.searchType = [coder decodeObjectForKey:@"searchType"];
        self.searchId = [coder decodeObjectForKey:@"searchId"];
        self.distance = [coder decodeObjectForKey:@"distance"];
        self.applicationProviderId = [coder decodeObjectForKey:@"applicationProviderId"];
        self.contentProviderId = [coder decodeObjectForKey:@"contentProviderId"];
    }
}

- (void) encodeWithCoder: (NSCoder *)coder
{
    [coder encodeObject:locationId forKey:@"locationId"];
    [coder encodeObject:companyName forKey:@"companyName"];
    [coder encodeObject:addressLine1 forKey:@"addressLine1"];
    [coder encodeObject:addressLine2 forKey:@"addressLine2"];
    [coder encodeObject:city forKey:@"city"];
    [coder encodeObject:postcode forKey:@"postcode"];
    [coder encodeObject:telephoneNumber forKey:@"telephoneNumber"];
    [coder encodeObject:description forKey:@"description"];
    [coder encodeObject:rating forKey:@"rating"];
    [coder encodeObject:priceGuide forKey:@"priceGuide"];
    [coder encodeObject:latitude forKey:@"latitude"];
    [coder encodeObject:longitude forKey:@"longitude"];
    [coder encodeObject:userLatitude forKey:@"userLatitude"];
    [coder encodeObject:userLongitude forKey:@"userLongitude"];
    [coder encodeObject:searchType forKey:@"searchType"];
    [coder encodeObject:searchId forKey:@"searchId"];
    [coder encodeObject:distance forKey:@"distance"];
    [coder encodeObject:applicationProviderId forKey:@"applicationProviderId"];
    [coder encodeObject:contentProviderId forKey:@"contentProviderId"];

}
Community
  • 1
  • 1
Anthony Main
  • 6,039
  • 12
  • 64
  • 89

5 Answers5

177

For loading custom objects in an array, this is what I've used to grab the array:

NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *dataRepresentingSavedArray = [currentDefaults objectForKey:@"savedArray"];
if (dataRepresentingSavedArray != nil)
{
    NSArray *oldSavedArray = [NSKeyedUnarchiver unarchiveObjectWithData:dataRepresentingSavedArray];
    if (oldSavedArray != nil)
        objectArray = [[NSMutableArray alloc] initWithArray:oldSavedArray];
    else
        objectArray = [[NSMutableArray alloc] init];
}

You should check that the data returned from the user defaults is not nil, because I believe unarchiving from nil causes a crash.

Archiving is simple, using the following code:

[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:objectArray] forKey:@"savedArray"];

As f3lix pointed out, you need to make your custom object comply to the NSCoding protocol. Adding methods like the following should do the trick:

- (void)encodeWithCoder:(NSCoder *)coder;
{
    [coder encodeObject:label forKey:@"label"];
    [coder encodeInteger:numberID forKey:@"numberID"];
}

- (id)initWithCoder:(NSCoder *)coder;
{
    self = [super init];
    if (self != nil)
    {
        label = [[coder decodeObjectForKey:@"label"] retain];
        numberID = [[coder decodeIntegerForKey:@"numberID"] retain];
    }   
    return self;
}
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • I've copied your code and my object which is implementing NSCoder into a prototype but despite the NSData that comes out of the UserDefaults has data the final array still has a count of 0. Any ideas? I will try and upload my prototype somewhere – Anthony Main Feb 13 '09 at 16:31
  • 3
    You were missing a "return self" at the end of your initWithCoder: method. Adding that seems to allow the unarchiving to take place. – Brad Larson Feb 13 '09 at 20:07
  • I cant believe it was something that simple! Stupid coding blindness! – Anthony Main Feb 16 '09 at 10:03
  • I had same problem, and it was solved thank you very much Brad – fyasar Nov 30 '09 at 17:46
  • @shawnwall - Good catch. I had pulled this from a garbage collected Mac application, so that was missing here. I've updated the code. – Brad Larson Oct 19 '11 at 16:21
  • If the object in the array is of class "Foo" that inherits from "Bar", should both Foo and Bar implement NSCoding? Also in Apple's examples, they don't do [[Class alloc] init]; within initWithCoder:. – Jonny Oct 29 '11 at 14:21
  • @Johny - If a superclass is NSCoding-compliant, the only reason you'd have to deal with `-encodeWithCoder:` and `-initWithCoder:` would be if you had added other instance variables or properties on top of the ones in the superclass. You'd need to make sure these got serialized, as well. As for your latter point, from the Archives and Serialization Guide: "If the superclass of your class does not support NSCoding, you should invoke the superclass’s designated initializer instead of initWithCoder:." In this case, `-init` was the designated initializer for the superclass. – Brad Larson Oct 29 '11 at 20:04
  • @BradLarson When using designated initializer `[[MyObject alloc] init]` instead of `[super init]` there are leaks reported when profiling the app in Instruments. @fyasar reported the same issue in http://stackoverflow.com/questions/1933285/nskeyedunarchiver-memory-leak-problem/1933399#1933399 Any Thoughts on that? – Palimondo Nov 04 '11 at 10:08
  • 2
    You should not store arrays into nsuserdefaults as the arrays could grow too big for nsuserdefaults. It should be instead stored into a file using + (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path. Why is this answer voted up so much? – mskw Dec 24 '12 at 17:38
  • 1
    @mskw - You're right in that NSUserDefaults shouldn't be used for storage of large arrays (Core Data or raw SQLite should be used for that instead), but it's perfectly fine to stash small arrays of encoded objects there. In any case, the question was asking how to do this, which is what the above code provides. As far as the votes, your guess is as good as mine. Lots of people must have wanted to do this over the last four years. – Brad Larson Dec 24 '12 at 18:55
  • i have NSArray which have dictionaries as a objects will this work ... i can not write initWithCoder for that – Mihir Mehta Feb 12 '13 at 15:54
  • Why are you `alloc`ing in initWithCoder? – Jonny Jul 29 '13 at 09:47
  • @Jonny - You're right, that shouldn't have been in there. Thanks for pointing it out, I've changed the code. – Brad Larson Jul 29 '13 at 14:48
  • I can't say I've checked this, but surely objectArray = [[NSMutableArray alloc] initWithArray:oldSavedArray]; won't return a MutableArray, you will need to call objectArray = [objectArray mutableCopy] on it as the initWithArray function will return a non-mutable array right? – aronspring Jan 27 '14 at 11:07
  • 2
    @Goodsum - Nope, that works just fine. Try it, it will return an NSMutableArray that is indeed mutable. It merely starts the new array by adding that array of items to it. – Brad Larson Jan 27 '14 at 19:27
  • 1
    @AlbertRenshaw - Where did the `andSync` you added to the code above come from? That's not an actual method signature for NSUserDefaults. Also, `synchronize` isn't as useful as it once was: https://twitter.com/Catfish_Man/status/647274106056904704 – Brad Larson Dec 11 '15 at 16:24
  • 1
    @BradLarson I must have been drunk xD I meant to type synchronize ahahahahaha I think it was like 5am when I did that hahahahaha. I was going to add `[[NSUserDefaults standardUserDefaults] synchronize];` – Albert Renshaw Dec 11 '15 at 22:40
  • @AlbertRenshaw - No problem. David has some more thoughts on synchronize that are worth reading in the conversation here: https://twitter.com/Catfish_Man/status/674727133017587712 . Used to be needed in some situations, but doesn't do much at present. – Brad Larson Dec 11 '15 at 22:48
  • @BradLarson Interesting, thanks for the link, so it just prevents the kind of issues that would arise from calling this across multiple parallel threads? – Albert Renshaw Dec 12 '15 at 01:24
13

I think you've gotten an error in your initWithCoder method, at least in the provided code you don't return the 'self' object.

- (id) initWithCoder: (NSCoder *)coder
{
    if (self = [super init])
    {
        self.locationId = [coder decodeObjectForKey:@"locationId"];
        self.companyName = [coder decodeObjectForKey:@"companyName"];
        self.addressLine1 = [coder decodeObjectForKey:@"addressLine1"];
        self.addressLine2 = [coder decodeObjectForKey:@"addressLine2"];
        self.city = [coder decodeObjectForKey:@"city"];
        self.postcode = [coder decodeObjectForKey:@"postcode"];
        self.telephoneNumber = [coder decodeObjectForKey:@"telephoneNumber"];
        self.description = [coder decodeObjectForKey:@"description"];
        self.rating = [coder decodeObjectForKey:@"rating"];
        self.priceGuide = [coder decodeObjectForKey:@"priceGuide"];
        self.latitude = [coder decodeObjectForKey:@"latitude"];
        self.longitude = [coder decodeObjectForKey:@"longitude"];
        self.userLatitude = [coder decodeObjectForKey:@"userLatitude"];
        self.userLongitude = [coder decodeObjectForKey:@"userLongitude"];
        self.searchType = [coder decodeObjectForKey:@"searchType"];
        self.searchId = [coder decodeObjectForKey:@"searchId"];
        self.distance = [coder decodeObjectForKey:@"distance"];
        self.applicationProviderId = [coder decodeObjectForKey:@"applicationProviderId"];
        self.contentProviderId = [coder decodeObjectForKey:@"contentProviderId"];
    }

    return self; // this is missing in the example above


}

I use

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:results];
[prefs setObject:data forKey:@"lastResults"];

and

NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *dataRepresentingSavedArray = [currentDefaults objectForKey:@"lastResults"];
if (dataRepresentingSavedArray != nil)
{
        NSArray *oldSavedArray = [NSKeyedUnarchiver unarchiveObjectWithData:dataRepresentingSavedArray];
        if (oldSavedArray != nil)
                objectArray = [[NSMutableArray alloc] initWithArray:oldSavedArray];
        else
                objectArray = [[NSMutableArray alloc] init];
}

and it works perfect for me.

With regards,

Stefan

  • 3
    Stephan you are the life saver, look at here; That line saved my mind "if (self = [super init])" http://stackoverflow.com/questions/1933285/nskeyedunarchiver-memory-leak-problem/1933399#1933399 – fyasar Dec 19 '09 at 17:23
  • I got ERROR_BAD_ACCESS but you saved me. Was useing property with retain only and adding the self.variable solved it. – David Sep 09 '11 at 01:10
0

See "Why NSUserDefaults failed to save NSMutableDictionary in iPhone SDK? " (Why NSUserDefaults failed to save NSMutableDictionary in iPhone SDK?)


If you want to (de)serialize custom objects, you have to provide the functions to (de)serialize the data (NSCoding protocol). The solution you refer to works with the int array because the array is not an object but a contiguous chunk of memory.

Community
  • 1
  • 1
f3lix
  • 29,500
  • 10
  • 66
  • 86
  • I think your right I'll have a look into NSCoding (unless you have a recommended linky), the objects themselves are made up on NSStrings that is all so I suppose I could just write my own serialiser and put them all into a string array – Anthony Main Feb 11 '09 at 14:56
  • Ok so I have implemented NSCode see above, but still no joy using NSArchiver – Anthony Main Feb 11 '09 at 16:29
0

I think it looks like the problem with your code is not saving the results array. Its loading the data try using

lastResults = [prefs arrayForKey:@"lastResults"];

This will return the array specified by the key.

Gcoop
  • 3,372
  • 4
  • 26
  • 35
0

I would recommend against trying to store stuff like this in the defaults db.

SQLite is fairly easy to pick up and use. I have an episode in one of my screencasts (http://pragprog.com/screencasts/v-bdiphone) about a simple wrapper that I wrote (you can get the code without buying the SC).

It's much cleaner to store app data in app space.

All that said if it still makes sense to put this data into the defaults db, then see the post f3lix posted.

Bill Dudney
  • 3,358
  • 1
  • 16
  • 13
  • 1
    Thanks for the suggestion but I dont really want to have to start writing SQL queries (as I see in your example) just for an object with a few strings – Anthony Main Feb 11 '09 at 16:58