2

Basically I am using some open source code called OrderedDictionary that is derived from NSMutableDictionary. Then I want to save the ordered dictionary data to NSUserDefaults by adding encode and decode method to the OrderedDictionary class. However, I realized the encode and decode methods are not being called, as a result, the decoded dictionary is no longer ordered. Below is my code:

@interface OrderedDictionary : NSMutableDictionary <NSCopying, NSCoding>
{
    NSMutableDictionary *dictionary;
    NSMutableArray *array;
}

In the implementation file:

/**
 * encode the object
 **/
- (void)encodeWithCoder:(NSCoder *)coder
{
    [super encodeWithCoder:coder];
    [coder encodeObject:dictionary forKey:@"dictionary"];
    [coder encodeObject:array forKey:@"array"];
}

/**
 * decode the object
 */
- (id)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self != nil)
    {
        dictionary = [coder decodeObjectForKey:@"dictionary"];
        array = [coder decodeObjectForKey:@"array"];
    }   
    return self;
}

Quick example code for using this:

dictionary = [[OrderedDictionary alloc] init];
[dictionary setObject:@"one" forKey:@"two"];
[dictionary setObject:@"what" forKey:@"what"];
[dictionary setObject:@"7" forKey:@"7"];
NSLog(@"Final contents of the dictionary: %@", dictionary);

if ([[NSUserDefaults standardUserDefaults] objectForKey:@"myDictionary"] == nil)
{
    [[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:dictionary] 
                                          forKey:@"myDictionary"];
}
else
{
    NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];

    NSData *savedDictionary = [currentDefaults objectForKey:@"myDictionary"];

    if (savedDictionary != nil)
    {
        OrderedDictionary *oldData = [NSKeyedUnarchiver unarchiveObjectWithData:savedDictionary];
        if (oldData != nil) 
        {
            NSLog(@"Final contents of the retrieved: %@", oldData);

        } 
    }
}

The thing is, the final retrievedDictionary does NOT have the original data order and the encode and decode methods are not called at all.

Thanks for any help in advance! :)

trillions
  • 3,669
  • 10
  • 40
  • 59
  • I suspect there is no way to fix it, i.e. we may not be able to call child class's encode and decode methods of a NSMutableDictionary.... – trillions Apr 09 '12 at 17:38

2 Answers2

4

There's an NSObject method called -classForCoder that you need to override in OrderedDictionary. From the docs:

classForCoder
Overridden by subclasses to substitute a class other than its own during coding.

-(Class)classForCoder
Return Value
The class to substitute for the receiver's own class during coding.

Discussion
This method is invoked by NSCoder. NSObject’s implementation returns the receiver’s class. The private subclasses of a class cluster substitute the name of their public superclass when being archived.

That last line is the key. So, in OrderedDictionary.m:

- (Class)classForCoder 
{  
    return [self class]; // Instead of NSMutableDictionary
} 

Also, if you're not using ARC, make sure you retain the objects coming back from -decodeObjectForKey. As rob mayoff mentions below, you shouldn't call [super initWithCoder:] (or [super encodeWithCoder:'). I also changed the key strings to avoid collisions.

- (id)initWithCoder:(NSCoder *)coder
{
    if (self != nil)
    {
        dictionary = [[coder decodeObjectForKey:@"OrderedDictionary_dictionary"] retain];
        array = [[coder decodeObjectForKey:@"OrderedDictionary_array"] retain];
    }   
    return self;
}
Andrew Madsen
  • 21,309
  • 5
  • 56
  • 97
  • He also needs to avoid calling `[super initWithCoder:]` unless he's also implemented `initWithObjects:forKeys:count:`, or he will crash. And if he doesn't call `[super initWithCoder:]` then he shouldn't call `[super encodeWithCoder:]` either. – rob mayoff Apr 09 '12 at 18:14
  • Thanks for catching that rob. Edited my answer to include it. – Andrew Madsen Apr 09 '12 at 18:17
  • @AndrewMadsen I've tested the changes you made, worked great! Thank you so much! Shouldn't we add this answer to the original OrderedDictionary code? :) – trillions Apr 10 '12 at 00:14
0

You are possibly creating a new OrderedDictionary with the wrong initializer, initWithDictionary:. Try this instead:

OrderedDictionary *oldData = [NSKeyedUnarchiver unarchiveObjectWithData: savedDictionary];
if (oldData != nil) 
{
    NSLog(@"Final contents of the retrieved: %@", oldData);
}

Make sure initWithDictionary: expects OrderedDictionary as an argument. My guess is that it expects an NSDictionary.

Edit: the code to save the defaults should include something like this:

OrderedDictionary *myDict = ...;
NSData* data = [NSKeyedArchiver archivedDataWithRootObject: myDict];
[[NSUserDefaults standardDefaults] setObject: data forKey: @"myDictionary"];
Costique
  • 23,712
  • 4
  • 76
  • 79
  • thank you for your help. I just tried your way. The output is still not right, data order is no longer reserved. I've updated my code in this post to reflect the change you made. I think the problem is my encodeWithCoder was not called at all, so that the encode and decode methods in parent class (NSMutableDictionary) was called. However, i dont know how to fix this. – trillions Apr 04 '12 at 17:16
  • Make sure you use `NSKeyedArchiver` when saving defaults. Put `NSLog()` in your `initWithCoder:` and `encodeWithCoder:` to see if they get called and work correctly. – Costique Apr 05 '12 at 05:04
  • Thanks a lot for helping me. I think the archiving code I did is the same as yours, just I combined two lines of code into one. It's still not calling the encode and decode methods. [[NSUserDefaults standardUserDefaults] setObject: [NSKeyedArchiver archivedDataWithRootObject:dictionary] forKey:@"myDictionary"]; – trillions Apr 06 '12 at 00:25