How to deal with field type changes when using NSCoding
Asked Answered
D

2

7

I have the following class that implements NSCoding and I have created several instances of it and persisted them to file.

@interface BiscuitTin ()
@property NSString *biscuitType;
@property int numBiscuits;
@end

@implementation BiscuitTin

- (id)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        self.biscuitType = [coder decodeObjectForKey:@"biscuitType"];
        self.numBiscuits = [coder decodeIntForKey:@"numBiscuits"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *coder) {
    [coder encodeObject:self.biscuitType forKey:@"biscuitType"];
    [coder encodeInt:self.numBiscuits forKey:@"numBiscuits"];
}

@end

I have now decided that I wish to represent numBiscuits as a float (as there may be a partially eaten biscuit). Updating the property type and encodeWithCoder works fine but when I try to load an existing instance from file the app crashes as it's trying to decode an int and a float.

Is there a nice way to handle this? Ideally I would be able to load the existing int value and convert it to a float, but I wouldn't mind just using a default value and not crashing.

I've considered wrapping the applicable decode line in a try-catch but in my actually use case there are about 50 or so properties that are being encoded/decoded and it would be nice not to have to have explicit handling for each one that ever changes type.

Dagnah answered 22/4, 2015 at 21:41 Comment(0)
B
9

When dealing with this situation myself (and I have a few times), I use versioning. In other words, encode a version number along with the rest of your encoded data.

Even if you haven't been versioning from the beginning, that's not really problem, you can start now with version 1, and just treat the absence of a version value as equivalent to version 0.

How?

Store a version field in the object you are encoding. Using an int or NSInteger for this field is probably best.

#define CURRENT_VERSION 1
#define MIN_VERSION_WITH_FEATURE_X 1

@implementation BiscuitTin

- (id)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        ...
        int vers = [coder decodeIntForKey:@"version"];
        if (vers >= MIN_VERSION_WITH_FEATURE_X) {
            // handle feature X. In your case, decoding a `float`
        } else {
            // handle prior version.
            // In your case, decoding an `int` and converting to `float`
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *coder) {
    ...
    [coder encodeInt:CURRENT_VERSION forKey:@"version"];
}

@end

Whenever you add a new feature that is not backwards-compatible, increment the CURRENT_VERSION, add a new MIN_VERSION_WITH_FEATURE_Y constant with the new CURRENT_VERSION integer value, and another branch in the if statement within -initWithCoder:.

It's a bit messy, but I guess that's the cost of backwards-compatibility. (On the plus side, I think this technique is fairly self-documenting, and therefore it is easy to work with when you return to it months or years later).

Beekeeping answered 22/4, 2015 at 21:55 Comment(0)
A
5

In similar cases I've updated the code to save the changed property under a new name. Then the initWithCoder: code looks for the new name. If not there, it looks for the old value under old name.

So version 2 of your code (with the property changed to a float) would be something like the following:

@interface BiscuitTin ()
@property NSString *biscuitType;
@property float numBiscuits;
@end

- (id)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        self.biscuitType = [coder decodeObjectForKey:@"biscuitType"];
        if ([coder containsValueForKey:@"numBiscuits2"]) {
            // Process a version 2 archive
            self.numBiscuits = [coder decodeFloatForKey:@"numBiscuits2"];
        } else if ([coder containsValueForKey:@"numBiscuits"]) {
            // Process a version 1 archive
            int oldIntVal = [coder decodeIntForKey:@"numBiscuits"];
            self.numBiscuits = oldIntVal;
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *coder) {
    [coder encodeObject:self.biscuitType forKey:@"biscuitType"];
    // [coder encodeInt:self.numBiscuits forKey:@"numBiscuits"]; // obsolete version
    [coder encodeFloat:self.numBiscuits forKey:@"numBiscuits2"]; // new key name
}
Ashleeashleigh answered 22/4, 2015 at 21:55 Comment(1)
Looks like typo in else if for key @"numBiscuits2"Showalter

© 2022 - 2024 — McMap. All rights reserved.