Apple provides extensions for NSValue to store most CoreAnimation-related structs in there: CGAffineTransform, UIEdgeInsets, CGPoint, CGSize, CGRect, UIOffset. And of course the inverse methods for getting the structs back out.
This is quite useful if you need to store these structs in an Objective-C container object, like a dictionary. On Mac I found these methods missing, even though I needed to put an affine transform into a object property.
Fortunately NSValue can easily be extended to handle any kind of custom struct.
Internally NSValue instances consist of a blob of bytes paired with a C-String describing the name and structure of the bytes. On iOS this format looks like this for an affine transform:
{CGAffineTransform=ffffff}
The Fs (exactly 6) are the 6 floats that make up the structure (a,b,c,d,tx,ty) and they are single precision because on iOS CGFloat is typedef-ef as such. On Mac those would be Ds because there CGFloat is defined as double.
This difference means we have to use such NSValues with caution if we mean to cross platform boundaries. But as long as these NSValue-s done leave the platform we are coding on we are safe.
We don’t actually have to know the correct encoding for NSValue because the handy @encode does that for us automatically. So our implementation of the two methods for affine transforms on Mac looks like this:
NSValue+DTConversion.m
+ (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform { return [NSValue valueWithBytes:&transform objCType:@encode(CGAffineTransform)]; } - (CGAffineTransform)CGAffineTransformValue { CGAffineTransform transform; [self getValue:&transform]; return transform; } |
You provide the struct type as parameter of @encode and this creates a char * string with the correct contents. To decode we create a local variable and call getValue on the NSValue, which in the above example is self because this is a category on NSValue.
What’s even cooler is that NSValue already knows how to properly describe even such custom structs. For this example the output is:
CGAffineTransform: {{18.16, 0, 0, 22.700000000000003}, {295.57560000000001, 470.31100000000004}}
It shows the correct struct type name and even groups together the first 4 values and the last 2. Why is that? Of course because that is how CGAffineTransform is defined:
struct CGAffineTransform { CGFloat a, b, c, d; CGFloat tx, ty; }; |
So it’s totally correct. Awesome automatic stuff!
Conclusion
At first I had tried to specify the format manually taking the one I had gleaned from iOS…. which failed of course because of the difference in floating point precision.
But as soon as I googled for the reason I found out about the @encode method. With that in place it started to work.
Even with ARC making many uses of C-structs obsolete (because ARC cannot memory-manage objc objects in structs) they still have many good uses, especially for grouping together multiple scalar values. It is good to know how to extend NSValue to store yours in memory-managed objects.
Categories: Recipes
An alternative is in found in “UIGeometry.h”:
NSString *NSStringFromCGPoint(CGPoint point);
NSString *NSStringFromCGSize(CGSize size);
NSString *NSStringFromCGRect(CGRect rect);
NSString *NSStringFromCGAffineTransform(CGAffineTransform transform);
NSString *NSStringFromUIEdgeInsets(UIEdgeInsets insets);
NSString *NSStringFromUIOffset(UIOffset offset);
This only exists on iOS. My post is about re-creating it for use on Mac.