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.
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.
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).
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).
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).
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With