How to handle field type changes when using NSCoding

I have the following class that implements NSCoding , and I created several instances of it and saved them in a 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 

Now I decided that I want to present numBiscuits as a float (since 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 a file, the application crashes trying to decode int and float .

Is there a good way to handle this? Ideally, I could load the existing int value and convert it to a float , but I would not mind using the default value and not fail.

I considered wrapping the applicable decode string in try-catch, but in my case there are about 50 or so properties that are encoded / decoded, and it would be nice to not have explicit processing for everyone that ever changes type.

+6
source share
2 answers

When I deal with this situation (and I have several times), I use the version . In other words, encode the version number along with the rest of your encoded data.

Even if you did not perform version control from the very beginning, this is not a problem, you can start now with version 1 and just consider the absence of version value as equivalent to version 0 .

How?

Save the version field in the encoded object. Using 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 function that does not support backward compatibility, add CURRENT_VERSION , add a new constant MIN_VERSION_WITH_FEATURE_Y with the new integer value CURRENT_VERSION and another branch in the if expression in -initWithCoder: >.

This is a bit dirty, but I think the cost is backward compatible. (On the other hand, I think this method is quite self-documenting, and therefore it is easy to work with it when you return to it after a few months or years).

+6
source

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

So, version 2 of your code (with changing the property to float ) will look something like this:

 @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:@"numBiscuits2"]; 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 } 
+2
source

Source: https://habr.com/ru/post/985720/


All Articles