Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deal with field type changes when using NSCoding

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.

like image 277
DanielGibbs Avatar asked Apr 22 '15 21:04

DanielGibbs


People also ask

What is NSCoding in ios?

NSCoding is a protocol that you can implement on your data classes to support the encoding and decoding of your data into a data buffer, which can then persist on disk. Implementing NSCoding is actually ridiculously easy — that's why you may find it helpful to use.

What is NSCoding in Swift?

The NSCoding protocol declares the two methods that a class must implement so that instances of that class can be encoded and decoded. This capability provides the basis for archiving (where objects and other structures are stored on disk) and distribution (where objects are copied to different address spaces).

What is NSCode?

Overview. NSCoder declares the interface used by concrete subclasses to transfer objects and other values between memory and some other format. This capability provides the basis for archiving (storing objects and data on disk) and distribution (copying objects and data items between different processes or threads).


2 Answers

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).

like image 160
Todd Ditchendorf Avatar answered Sep 21 '22 19:09

Todd Ditchendorf


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
}
like image 23
rmaddy Avatar answered Sep 20 '22 19:09

rmaddy