How to convert NSValue to NSData and back?
Asked Answered
C

3

11

A have a number of NSValue (obtained via KVC valueForKey) that I need to append to an NSData object in order to send it over the network using Game Center. Obviously I will also need to convert the NSValue back from NSData for more KVC (setValue:forKey:).

I don't know the exact type of each NSValue, so I'm limited to using the interfaces provided by NSValue and NSData. I should mention that the NSValue are never pointer types.

I'm surprised that there's no [NSData dataWithValue:value] and neither something like [value dataWithEncoding:] or similar. But maybe I'm blind and not seeing the obvious choice. I thought about using getValue: to copy the value into a void* buffer, but how am I supposed to determine the buffer length other than by using objCType and comparing that with all possible types?

How should I go about this?

NOTE: NSKeyedArchiver is out of the question because it is terribly inefficient. A 4 Byte float is archived to a 142 Bytes NSData, a 8 Byte CGPoint uses 260 Bytes when archived to NSData. Keeping the amount of data sent to a minimum is crucial to what I'm doing.

Crowded answered 9/12, 2011 at 15:1 Comment(2)
NSValue supports NSCoding, so you can create an NSData representation using NSKeyedArchiver or NSArchiver.Nosepiece
You could always add a category that converts it to data, based on the objcType variable..Hereld
C
17

Martin Gordon's answer is getting close, but fortunately you don't have to manually parse the objCType string. There's a function that does that: NSGetSizeAndAlignment. From that you get the size/length of the value obtained from getValue:. This question lead me to the solution.

I ended up creating a category for NSData that allows me to create NSData from NSNumber or NSValue objects:

@interface NSData (GameKitCategory)
+(NSData*) dataWithValue:(NSValue*)value;
+(NSData*) dataWithNumber:(NSNumber*)number;
@end

@implementation NSData (GameKitCategory)
+(NSData*) dataWithValue:(NSValue*)value
{
    NSUInteger size;
    const char* encoding = [value objCType];
    NSGetSizeAndAlignment(encoding, &size, NULL);

    void* ptr = malloc(size);
    [value getValue:ptr];
    NSData* data = [NSData dataWithBytes:ptr length:size];
    free(ptr);

    return data;
}

+(NSData*) dataWithNumber:(NSNumber*)number
{
    return [NSData dataWithValue:(NSValue*)number];
}
@end

I also add a small header before this NSValue/NSNumber data that allows me to decode which property the data is for and how many bytes are in the data section. With that I can restore the value to the remote property.

Crowded answered 9/12, 2011 at 20:28 Comment(4)
I can't upvote you enough, this was a pain to find out! You think this would be more of an Objective-C runtime thing (like a ivar_sizeOfObjcType or something).Histopathology
Steffen, I like to acknowledge when I use an idea from someone else. I took the above code and massaged it to meet my sensibilities. It is included in my DDGPreferences project on GitHub: <github.com/adonoho/DDGPreferences>. Thank you for exploring this idea publicly. AndrewSela
And what does the reverse look like, from NSData back into NSNumber?Rochelle
You shouldn't need that because you would normally encode the value itself into NSData, not the NSNumber that wraps the value. See for instance: #9225212Crowded
M
4

Since NSValue adopts the NSCoding protocol, you can use the NSCoder subclasses, NSKeyedArchiver and NSKeyedUnarchiver:

- (NSData *)dataWithValue:(NSValue *)value {
  return [NSKeyedArchiver archivedDataWithRootObject:value];
}

- (NSValue *)valueWithData:(NSData *)data {
  return (NSValue *)[NSKeyedUnarchiver unarchiveObjectWithData:data];
}

If NSKeyedArchiver and NSKeyedUnarchiver can't be used (for resulting data size issues), then you would have to find the size of the contained value yourself:

- (NSData *)dataWithValue:(NSValue *)value {
  // Can use NSGetSizeAndAlignment() instead. See LearnCocos2D's answer.
  if (type[0] == '{') {
    // Use the various NSValue struct value methods to detect the type.
  } else if (type[0] == 'c') {
    size = sizeof(char);
  } else if (type[0] == 'i') {
    size = sizeof(int);     
  } // etc for all/most of the values in the table linked below [1];

  void *bytes = malloc(size);
  [value getValue:bytes];
  return [NSData dataWithBytes:bytes length:size];
}

[1]: Objective-C Type Encodings

Malocclusion answered 9/12, 2011 at 15:12 Comment(2)
I'm afraid NSKeyedArchiver is terribly inefficient for sending data over the network. A simple float (4 Bytes) value gets blown up to 142 Bytes, a CGPoint (8 Bytes) is 260 Bytes when key-archived to NSData. Isn't there an alternative?Crowded
Thanks for the update. I was worried that it would have to come to this, meaning comparing the string values of the objCType. Fortunately that isn't necessary thanks to NSGetSizeAndAlignment(). I'll post an answer with my solution.Crowded
R
0

You can use NSCoder (like NSKeyedArchiver) to archive the data, then send the resulting NSData. On the other side you can unarchive it. There may be some caveats with this (for example http://www.cocoabuilder.com/archive/cocoa/82344-nskeyedarchiver-and-nsvalue.html) esepcially if you are wrapping structs and other non-archiving stuff in NSValue.

Reset answered 9/12, 2011 at 15:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.