NSValue: Boxing Arbitrary C Structs

Understand NSValue as the general-purpose boxing type for arbitrary C data, learn its generic byte-boxing method alongside the convenience constructors for common geometry and range structs, and see how NSValue relates to NSNumber as its more specialized subclass.

NSNumber solves boxing for numeric and Boolean scalars, but Foundation and the wider Objective-C/Cocoa world are full of other C struct types that also need to live inside object-only collections: a CGPoint holding an x/y coordinate, a CGSize holding a width/height pair, a CGRect combining both, or an NSRange describing a location-and-length span. None of these are single scalar numbers, so NSNumber cannot box them. NSValue is Foundation's more general boxing container: it can wrap literally any fixed-size chunk of C data — not just the common geometry and range types it has convenience constructors for, but any struct at all — inside a genuine Objective-C object, using the same underlying idea as NSNumber but generalized far beyond numbers.

NSValue: Boxing Beyond Numbers

Where NSNumber's entire job is boxing scalar numeric and Boolean values, NSValue's job is boxing arbitrary blocks of C data of any shape: structs, C arrays, pointers, even primitive scalars (NSNumber is, in fact, implemented as a specialized subclass of NSValue, which is covered in detail later in this lesson). The core problem NSValue solves is exactly the same one NSNumber solves for numbers, generalized: Foundation collections can only hold objects, and a CGPoint or CGRect is a plain C struct with no object-ness to it at all, so it cannot go into an NSArray or be used as an NSDictionary value without first being wrapped inside some kind of object container.

NSValue instances, like NSNumber instances, are immutable once created — a given NSValue object always represents the exact same boxed bytes it was created with, and there is no mutable counterpart the way NSMutableString complements NSString. If you need to change a boxed value, you create a new NSValue rather than mutating the existing one.

The Fully Generic Path: valueWithBytes:objCType:

NSValue's most general constructor, +valueWithBytes:objCType:, can box absolutely any C struct, given a pointer to its bytes and a type-encoding string describing its layout. MyCustomStruct s = { .x = 10, .y = 20 }; NSValue *boxed = [NSValue valueWithBytes:&s objCType:@encode(MyCustomStruct)]; boxes an arbitrary programmer-defined struct that has no dedicated NSValue convenience constructor at all. The @encode() compiler directive is what makes this possible: given any C type, @encode(SomeType) expands at compile time to a string literal describing that type's exact memory layout (its fields, their types, and their sizes), which is exactly what NSValue needs internally to know how many bytes to copy out of the pointer you hand it and how to format them back out later.

To get the struct back out, you call -getValue:, handing it a pointer to a local variable of the same struct type: MyCustomStruct extracted; [boxed getValue:&extracted]; copies the boxed bytes back into extracted. This generic path is what you reach for whenever you need to box a struct that isn't one of the common geometry or range types Cocoa already provides a convenience constructor for — it works for literally any fixed-size C struct, at the cost of being slightly more verbose to write than the convenience constructors covered next.

Convenience Constructors: Points, Sizes, Rects, and Ranges

Because certain struct types come up constantly — coordinates, dimensions, rectangles, and ranges — NSValue provides dedicated, more convenient constructors for each of them rather than requiring every caller to write out valueWithBytes:objCType: by hand every time. +valueWithCGPoint: boxes a CGPoint (an x/y pair); +valueWithCGSize: boxes a CGSize (a width/height pair); +valueWithCGRect: boxes a CGRect (an origin CGPoint plus a size CGSize); and +valueWithRange: boxes an NSRange (a location plus a length, exactly the same struct you've already used with substringWithRange: and the NSMutableString mutation methods from the previous lesson).

Each of these has a matching accessor for unboxing: -CGPointValue, -CGSizeValue, -CGRectValue, and -rangeValue respectively, each returning the plain C struct with its original values intact. These convenience methods exist purely for ergonomics — under the hood, valueWithCGPoint: still does the same job as calling valueWithBytes:objCType:@encode(CGPoint) would, but it's shorter to write, harder to get the type encoding wrong on, and immediately communicates to a reader exactly what kind of struct is boxed inside.

The reason these particular types need boxing so often in real Cocoa/UIKit-adjacent code is that geometry structs are everywhere in layout and drawing code, but they cannot be stored directly in an NSArray or as an NSDictionary value — an array of button frames, for instance, has to be an array of NSValue-boxed CGRects, not raw CGRect values, because CGRect itself is not an object.

• valueWithCGPoint: / CGPointValue — box/unbox a CGPoint (x, y). • valueWithCGSize: / CGSizeValue — box/unbox a CGSize (width, height). • valueWithCGRect: / CGRectValue — box/unbox a CGRect (origin + size). • valueWithRange: / rangeValue — box/unbox an NSRange (location, length).

NSValue in Collections: Why It's Needed At All

The entire motivation for NSValue mirrors NSNumber's motivation exactly, just for non-scalar C data: suppose you're tracking the tap locations a user made on screen, each one a CGPoint, and you want to store the whole sequence in an NSArray so you can replay or analyze it later. You cannot write NSArray *taps = @[point1, point2, point3]; directly if point1 is a raw CGPoint variable — the array literal syntax requires each element to already be an object, and a CGPoint is a plain C struct, not an object. The fix is to box each point first: NSArray *taps = @[[NSValue valueWithCGPoint:point1], [NSValue valueWithCGPoint:point2], [NSValue valueWithCGPoint:point3]];, giving you an array of genuine NSValue objects that happily satisfies the array literal's object-only requirement.

The same pattern applies identically to NSDictionary: a dictionary mapping button names to their on-screen frames needs NSValue-boxed CGRect values, since CGRect cannot be a dictionary value directly any more than it can be an array element. Whenever you find yourself wanting to put a struct — geometry or otherwise — into any Foundation collection, boxing it in an NSValue first is the standard, idiomatic way to do it.

How NSValue Relates to NSNumber

It's worth closing the loop on how these two classes fit together, since the spec for the NSNumber lesson and this one cover closely related ground. NSNumber is, architecturally, a specialized subclass of NSValue built specifically for scalar numeric and Boolean C types (int, float, double, BOOL, and their siblings), with its own dedicated boxing/unboxing methods (numberWithInt:/intValue, and so on) tuned for that narrower job. NSValue is the more general parent concept: it can box anything with a fixed byte layout, scalars included, but its convenience constructors beyond the numeric ones focus on the struct types — points, sizes, rects, ranges — that come up constantly in UI and layout code but aren't themselves single numbers.

In practice this means you should reach for NSNumber specifically when boxing a single scalar value (an int, a double, a BOOL) because its API is purpose-built and more ergonomic for that case — numberWithInt:/intValue reads more clearly at the call site than the fully generic valueWithBytes:objCType: path would for a plain int — and reach for NSValue (either its convenience constructors or the fully generic valueWithBytes:objCType:) for everything else with a fixed C struct layout: points, sizes, rects, ranges, or your own custom structs that have no dedicated boxing type of their own.

NSValue Boxing and Unboxing

The convenience constructors (valueWithCGPoint: etc.) are shorthand over the same mechanism as valueWithBytes:objCType:. NSValue instances are immutable; there is no NSMutableValue. getValue: copies the boxed bytes into the pointer you provide -- the destination variable must be the correct struct type.

NSValue *pointBox = [NSValue valueWithCGPoint:CGPointMake(10, 20)];
NSValue *sizeBox  = [NSValue valueWithCGSize:CGSizeMake(100, 50)];
NSValue *rectBox  = [NSValue valueWithCGRect:CGRectMake(0, 0, 100, 50)];
NSValue *rangeBox = [NSValue valueWithRange:NSMakeRange(0, 5)];

CGPoint p = pointBox.CGPointValue;
CGSize  s = sizeBox.CGSizeValue;
CGRect  r = rectBox.CGRectValue;
NSRange rg = rangeBox.rangeValue;

// Fully generic path for any struct:
NSValue *custom = [NSValue valueWithBytes:&myStruct objCType:@encode(MyStruct)];
[custom getValue:&myStruct];

Code Examples

CGPoint is a plain C struct and cannot be an array element directly. Each point is boxed with valueWithCGPoint: before being placed in the array literal, and unboxed with.CGPointValue when read back out during enumeration. Expected result: Tap at (10, 20) Tap at (30, 40) [NSValue valueWithCGPoint:point1],

NSRange, like CGPoint, is a plain C struct and cannot be a dictionary value directly. valueWithRange: boxes it into an NSValue that satisfies NSDictionary's object-only requirement, and rangeValue unboxes it back into a usable NSRange. Expected result: location=6 length=5 [NSValue valueWithRange:highlightRange]

WeatherReading has no dedicated NSValue convenience constructor, so the fully generic valueWithBytes:objCType: path boxes it using @encode to describe its layout, and getValue: copies the bytes back out into a local struct of the matching type. Expected result: 21.5C, 60%

[NSValue valueWithBytes:&reading objCType:@encode(WeatherReading)];

CGPoint point1 = CGPointMake(10, 20);
CGPoint point2 = CGPointMake(30, 40);

NSArray<NSValue *> *tapLocations = @[
    [NSValue valueWithCGPoint:point1],
    [NSValue valueWithCGPoint:point2]
];

for (NSValue *boxed in tapLocations) {
    CGPoint p = boxed.CGPointValue;
    NSLog(@"Tap at (%.0f, %.0f)", p.x, p.y);
}
NSRange highlightRange = NSMakeRange(6, 5); // "World" in "Hello World"

NSDictionary<NSString *, NSValue *> *highlights = @{
    @"greeting": [NSValue valueWithRange:highlightRange]
};

NSValue *boxedRange = highlights[@"greeting"];
NSRange range = boxedRange.rangeValue;
NSLog(@"location=%lu length=%lu", (unsigned long)range.location, (unsigned long)range.length);
typedef struct {
    double celsius;
    int humidity;
} WeatherReading;

WeatherReading reading = { .celsius = 21.5, .humidity = 60 };
NSValue *boxed = [NSValue valueWithBytes:&reading objCType:@encode(WeatherReading)];

WeatherReading extracted;
[boxed getValue:&extracted];
NSLog(@"%.1fC, %d%%", extracted.celsius, extracted.humidity);

Important Rules

NSValue boxes any fixed-size C data -- structs, scalars, even pointers -- into a genuine Objective-C object.

valueWithBytes:objCType: (paired with @encode) is the fully generic boxing path for any struct, including custom, project-defined ones.

valueWithCGPoint:/valueWithCGSize:/valueWithCGRect:/valueWithRange: are convenience constructors for the most common geometry and range structs, with matching *Value accessors to unbox.

NSValue is immutable -- there is no mutable counterpart; create a new NSValue rather than modifying an existing one.

NSNumber is a specialized NSValue subclass for scalar numeric/Boolean types; prefer NSNumber's dedicated API for scalars and NSValue for structs.

Animation Keyframes and Layout Data

NSValue shows up constantly in real UIKit/AppKit-adjacent code wherever a sequence or collection of geometry values needs to be stored: an array of CGRect keyframes driving a custom animation, a dictionary mapping view identifiers to their CGSize constraints, or a cache of previously computed NSRange results from text layout. Anywhere Cocoa's collection classes meet Core Graphics' struct types, NSValue is the object doing the bridging.

NSArray *points = @[point1, point2]; // point1/point2 are raw CGPoint variables

NSArray *points = @[[NSValue valueWithCGPoint:point1], [NSValue valueWithCGPoint:point2]];

A CGPoint is a plain C struct, not an object, so it cannot be used directly as an array literal element. It must be boxed in an NSValue first.

CGRect r = boxedValue.CGPointValue; // boxed as a CGRect, unboxed as a CGPoint CGRect r = boxedValue.CGRectValue;

Unboxing with the accessor for the wrong struct type reads garbage or misinterpreted bytes -- the accessor you call must match the type the NSValue was actually boxed with.

int score = [NSValue valueWithBytes:&scoreInt objCType:@encode(int)]; // then used as if score were the NSValue itself

NSNumber *score = @(scoreInt); // prefer NSNumber's dedicated, more ergonomic API for scalars

While NSValue's generic path can technically box a scalar int, NSNumber exists specifically for that job with a more ergonomic, purpose-built API (@(), intValue) -- reach for NSValue for structs, not plain scalars.

Why can't a raw CGPoint be stored directly as an element in an NSArray literal? A. CGPoint values are always too large to fit in an array slot B. NSArray literals only accept NSString elements C. CGPoint is a plain C struct, not an object, and Foundation collections can only hold objects D. CGPoint must first be converted to an NSNumber

C. CGPoint is a plain C struct, not an object, and Foundation collections can only hold objects — NSArray, like all Foundation collections, can only store objects. A CGPoint is a C struct with no object-ness, so it must be boxed into an NSValue (via valueWithCGPoint:) before it can go into an array.

Which NSValue method can box literally any fixed-size C struct, including a custom, project-defined type with no dedicated convenience constructor? A. valueWithCGRect: B. valueWithBytes:objCType:, using @encode to describe the struct's layout C. numberWithInt: D. valueWithRange:

B. valueWithBytes:objCType:, using @encode to describe the struct's layout — valueWithBytes:objCType: is NSValue's fully generic boxing path. Paired with @encode(SomeStructType), it can box any struct, not just the ones with dedicated convenience constructors like CGPoint or NSRange.

How does NSNumber relate architecturally to NSValue? A. They are unrelated classes that happen to have similar names B. NSValue is a subclass of NSNumber, specialized for structs C. NSNumber is a specialized subclass of NSValue, purpose-built for scalar numeric and Boolean C types D. NSNumber replaced NSValue in recent Foundation versions

C. NSNumber is a specialized subclass of NSValue, purpose-built for scalar numeric and Boolean C types — NSNumber is architecturally a subclass of NSValue focused specifically on scalar numeric/Boolean values, with its own more ergonomic API (@(), intValue, etc.), while NSValue itself remains the general-purpose boxing type for arbitrary structs.

What does calling .CGRectValue on an NSValue that was actually boxed with valueWithCGPoint: produce? A. A compile-time error preventing the mismatch B. An automatic conversion from CGPoint to CGRect with zero size C. Misinterpreted or garbage data, since the accessor must match the type the value was actually boxed with D. nil

C. Misinterpreted or garbage data, since the accessor must match the type the value was actually boxed with — NSValue does not track a semantic type-check between boxing and unboxing calls the way the compiler would catch a plain type mismatch; unboxing with the wrong accessor for the boxed struct type produces misinterpreted bytes rather than a safe error.

For boxing a single scalar int value so it can be stored in an NSDictionary, which is the more idiomatic choice? A. NSValue's fully generic valueWithBytes:objCType:@encode(int) path B. NSNumber, via @(intValue), since it is purpose-built with a more ergonomic API for scalar types C. Neither works for scalar values D. NSMutableString

B. NSNumber, via @(intValue), since it is purpose-built with a more ergonomic API for scalar types — While NSValue's generic path can technically box a scalar, NSNumber is the specialized subclass built exactly for that job, with a shorter, clearer API (@(), intValue) -- NSValue is best reserved for structs without their own dedicated boxing type.

Given three CGSize values representing icon dimensions at different scales, box all three into an NSArray of NSValue objects, then write a loop that unboxes each one and logs its width and height.

NSArray<NSValue *> *sizes = @[ [NSValue valueWithCGSize:CGSizeMake(16, 16)], [NSValue valueWithCGSize:CGSizeMake(32, 32)], [NSValue valueWithCGSize:CGSizeMake(64, 64)] ]; for (NSValue *boxed in sizes) { CGSize s = boxed.CGSizeValue; NSLog(@"%.0f x %.0f", s.width, s.height); }

Use [NSValue valueWithCGSize:size] for each element when building the array literal, and .CGSizeValue inside the loop to unbox.