Why can't I encode an NSValue in an NSKeyedArchiver?
Asked Answered
O

3

8

I was trying to encode an MKMapView center and span into an NSKeyedArchiver for state preservation. I found a couple of handy new MapKit NSValue additions, valueWithMKCoordinate: and valueWithMKCoordinate:. Trying to encode these into the keyed archiver failed:

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
    NSValue *mapCenterValue = [NSValue valueWithMKCoordinate:mapView.centerCoordinate];
    NSValue *mapSpanValue = [NSValue valueWithMKCoordinateSpan:mapView.region.span];
    [coder encodeObject:mapCenterValue forKey:kMapCenter];
    [coder encodeObject:mapSpanValue forKey:kMapSpan];
}

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSKeyedArchiver encodeValueOfObjCType:at:]: this archiver cannot encode structs'

I understand that the solution to this problem is to just encode the individual doubles into four separate keys.

My question is why does this happen. An NSValue is an object, so why is it telling me "this archiver cannot encode structs"

Overlap answered 28/8, 2013 at 10:10 Comment(0)
M
4

According to the documentation of the NSKeyedArchiver class,

A keyed archive differs from a non-keyed archive in that all the objects and values encoded into the archive are given names, or keys.

In order to archive elements of a structand give keys to them NSKeyedArchiver would need metadata to know where each field of a struct is located, and what are the names of these fields. The @encode stored with NSValue gives it enough information about the layout of a struct, but the information about the names of each field is missing.

Since there is no metadata about the names of the fields in a struct, it would be impossible to archive the data in such a way as to ensure proper un-archiving. That is why NSKeyedArchiver must refuse to archive NSValues with embedded C structs.

Mythology answered 28/8, 2013 at 10:22 Comment(5)
I had been assuming that NSValue encoded its stuff like NSData, as a stream of bytes which could be unpacked at the other end if you knew the format. I guess this is wrong?Overlap
It's not wrong, but NSValue doesn't know enough information about the data it is storing to be able to pack and unpack it to/from bytes.Atheist
@nevanking That is absolutely true when you use NSArchiver. However, NSKeyedArchiver places an additional requirement on the data items to have names. In return the keyed archiver lets you unarchive into objects that are not 100% identical.Mythology
So NSKeyedArchiver can "see into" the value, and even though I gave it a name in encodeValue:forKey: it would require further names for the things inside the struct?Overlap
@nevanking Correct. Since keyed archiver knows that the data behind the value has further structure behind it, the archiver designers decided that it is better to "fail early" and not to archive the data at all than to archive as a sequence of bytes and risk not knowing how to un-archiving into an incompatible struct. You should be able to work around this by archiving your structs as sequences of unstructured bytes (though I haven't tried this myself).Mythology
A
2

NSValue is used to encapsulate non-object (e.g. C structs, ints, etc.) into an objective-c object, but it doesn't provide a way to define custom archiving/serialization routines for these wrapped types. This just isn't part of its interface.

If you wanted to archive an NSValue containing a struct, you would have to take things like endianness into consideration and handle things like nested pointers or other types which can't be trivially written out as bytes. There's no way to do this automatically with NSValue.

Atheist answered 28/8, 2013 at 10:19 Comment(1)
I guess this is why you can use values internally (like in an NSArray) but not externally (like sharing an archive).Overlap
B
2

I ran into the same problem and decided the easiest solution would just be to be boring and encode all the values individually:

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
    MKCoordinateRegion region = [self.mapView region];
    [coder encodeDouble:region.center.latitude forKey:RWSMapCenterLatitude];
    [coder encodeDouble:region.center.longitude forKey:RWSMapCenterLongitude];
    [coder encodeDouble:region.span.latitudeDelta forKey:RWSMapCenterLatitudeDelta];
    [coder encodeDouble:region.span.longitudeDelta forKey:RWSMapCenterLongitudeDelta];

    [super encodeRestorableStateWithCoder:coder];
}

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
    MKCoordinateRegion region;
    CLLocationCoordinate2D center;
    center.latitude = [coder decodeDoubleForKey:RWSMapCenterLatitude];
    center.longitude = [coder decodeDoubleForKey:RWSMapCenterLongitude];
    region.center = center;
    MKCoordinateSpan span;
    span.latitudeDelta = [coder decodeDoubleForKey:RWSMapCenterLatitudeDelta];
    span.longitudeDelta = [coder decodeDoubleForKey:RWSMapCenterLongitudeDelta];
    region.span = span;

    self.mapView.region = region;

    [super decodeRestorableStateWithCoder:coder];
}
Blastopore answered 27/2, 2014 at 12:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.