encodeWithCoder is not called in derived class of the NSMutableDictionary

Go To StackoverFlow.com

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! :)

2012-04-04 04:07
by trillions
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 2012-04-09 17:38


3

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;
}
2012-04-09 18:04
by Andrew Madsen
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 2012-04-09 18:14
Thanks for catching that rob. Edited my answer to include it - Andrew Madsen 2012-04-09 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 2012-04-10 00:14
@robmayoff thank you for the help too : - trillions 2012-04-10 00:19


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"];
2012-04-04 04:53
by Costique
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 2012-04-04 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 2012-04-05 05:04
I edited the answer to include archiving code - Costique 2012-04-05 05:10
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 2012-04-06 00:25
Ads