https://mar.in | Github | Mastodon | X

Storing custom objects into NSUserDefaults as NSDictionary

Using [NSUserDefaults standardUserDefaults] for storing some local data has became a standard practice. It comes with a limitation, though: you are only able to store built-in data types such as: NSData, NSString, NSNumber, NSDate , NSArray, and NSDictionary.

Here’s a way to handle serialization / deserialization a bit differently, in a way you can also use with web services.

The Apple solution

The solution proposed by Apple is implementing encodeWithCoder and initWithCoder on your types, then using NSKeyedArchiver and NSKeyedUnarchiver in order to serialize and deserialize your objects, respectively. With this approach we’re converting objects into NSData and storing them on the disk as binary.

- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeObject:self.surname forKey:@"surname"];
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
    self = [super initWithCoder:decoder];
    if (self) {
        _name = [decoder decodeObjectForKey:@"name"];
        _surname = [decoder decodeObjectForKey:@"surname"];
    }
    return self;
}

//storing
Person *person  = //get the person
NSData *encodedPerson = [NSKeyedArchiver archivedDataWithRootObject:person];
[[NSUserDefaults standardUserDefaults] setObject:myEncodedObject forKey:@"encodedPersonKey"];

//retrieving
NSData *encodedPerson = [[NSUserDefaults standardUserDefaults] objectForKey:@"encodedPersonKey"];
Person *person = (Person *)[NSKeyedUnarchiver unarchiveObjectWithData:encodedPerson];

This post is about using NSDictionary instead of NSData for doing the job. Let’s assume we have a web service and we’re receiving the Person object remotely. When we consume a JSON response, it’s usually gets converted into an array of NSDictionaries. We know better than to pass dictionaries around our app, so we map each dictionary to a Person.

// bad example: accessing dictionaries all over the place
personDict[@"name"];

// we want to map them to strongly typed objects and pass those around
Person *person
person.name
person.surname

To achieve that, we need to deserialize NSDictionary received from a JSON web service into an array of Person objects.

- (id)initWithDictionary:(NSDictionary *)personDict {
    self = [super init];
    if (self) {
        self.name = personDict[@"name"];
        self.surname = personDict[@"surname"];
    }
    return self;
}

We already have 2 deserialization methods. If we want to send the modified object back to the server, we’ll need to add a serialization method that’ll convert a Person back to NSDictionary. Then we can easily convert a dictionary of primitive types into JSON. To serialize, we need a -toDictionary method. As we already have -initWithDictionary:, this means we can serialize/deserialize to and from JSON and NSUserdefaults.

- (NSDictionary *)toDictionary {
    return @{
        @"first_name": self.name,
        @"last_name": self.surname
    };
}

To sum up, you can replace Apple’s initWithCoder, encodeWithCoder, NSKeyedArchiver and NSKeyedUnarchiver with just using dictionaries and NSUserDefaults.

//save
[[NSUserDefaults standardUserDefaults] setObject:[person toDictionary]] forKey:@"personKey"];

//load
[[NSUserDefaults standardUserDefaults] objectForKey:@"personKey"];

// receive / send the object from a web service
NSDictionary *personFromJSON;
Person *person = [[Person alloc] initWithDictionary:personFromJSON];

person.surname = @"modifiedSurname";
[person toDictionary]; // dict - you can convert it back to JSON