Objective-C Type Encodings
Build a precise, runtime-level understanding of Objective-C's type encoding system -- the single-character (and compound) codes the compiler generates for every type via @encode, how those codes combine into full method type-encoding strings describing a method's return type, receiver, selector, and arguments, and why NSMethodSignature, NSInvocation, and the objc_msgSend messaging ABI all depend on those encodings being exactly correct.
Every earlier lesson in this course has treated Objective-C's dynamism -- messages resolved at runtime, methods invoked by selector, objects inspected generically -- as something that simply works. It works because underneath all of it sits a small, unglamorous piece of machinery: a compact string alphabet the compiler uses to describe C and Objective-C types so the runtime, which is written in plain C and has no access to the compiler's own type-checking, can still reason about types when a program is running. This lesson is a reference-style tour of that alphabet -- type encodings -- and into how the compiler assembles them into full method type-encoding strings. It sets up two companion addendum lessons directly: NSMethodSignature, which is built from exactly these encoded strings, and NSInvocation, which uses a method signature's encodings to know precisely how many bytes each argument and return value occupy so it can copy them into and out of a raw call frame. It also lays the groundwork for the upcoming objc_msgSend in Depth lesson, where these same type encodings resurface as the reason calling the raw messaging function directly is dangerous unless you get every type exactly right.
Why the Runtime Needs a String Description of Every Type
Objective-C compiles down to C-level constructs, and the Objective-C runtime itself is a library of plain C functions operating on plain data structures. It has no built-in notion of 'an NSInteger parameter' or 'a CGRect return value' the way the compiler does while type-checking your source code -- by the time the runtime is running, the compiler is long gone, and all that remains are bytes, function pointers, and metadata tables. Whenever the runtime needs to describe a type for something it might inspect or invoke dynamically -- a property's declared type, an instance variable's type, or an entire method's full signature -- it needs that description in a form it can store as data, compare, and walk through one character at a time: a plain, null-terminated C string built from a small, fixed encoding alphabet.
Type encodings are exactly that alphabet. A single character (or a short bracketed or braced run of characters for compound types like arrays and structs) stands in for a C or Objective-C type. The compiler emits these encodings automatically at compile time and embeds them in the binary's metadata wherever the runtime might later need them -- most visibly in method type-encoding strings, but also in property attribute strings and instance-variable metadata tables.
This lesson focuses on reading and reasoning about type encodings directly, because so much of the runtime's dynamic machinery -- covered from the outside in the sibling NSMethodSignature and NSInvocation lessons, and at the raw ABI level in the upcoming objc_msgSend in Depth -- takes an encoded type string as its literal, actual input or output. Understanding the alphabet itself is what makes those other lessons make sense rather than feel like memorized incantations.
• Type encodings are the runtime's C-string representation of a type -- generated automatically by the compiler, never written by hand in ordinary code. • They exist because the runtime, written in plain C, has no access to the compiler's own type information once a program is actually running. • They appear wherever the runtime reasons about types dynamically: method signatures, property attributes, and instance-variable metadata. • Reading them directly is the foundation for the sibling NSMethodSignature and NSInvocation lessons and for the upcoming objc_msgSend in Depth.
The Core Single-Character Encoding Table
At the center of the system is a small table of single-character codes, one per fundamental C or Objective-C type, reproduced in full in the diagram accompanying this lesson. Several of these deserve a closer look individually, because their meaning is not always obvious from the letter alone.
'c' encodes a plain C char. 'i' encodes int, 's' encodes short, 'l' encodes long, and 'q' encodes long long -- note that 'l' reflects the actual C type long, not the Objective-C typedef name NSInteger; the compiler encodes what a type actually resolves to at the C level, never the typedef name used in source. 'f' and 'd' encode float and double respectively. 'v' encodes void, most commonly seen as the leading character of a method type-encoding string for a method with no return value.
'@' encodes an Objective-C object pointer (id, or any class-typed pointer such as NSString *), '#' encodes a Class reference, and ':' encodes a SEL. '^' is a prefix meaning 'pointer to' -- it is combined with whatever encoding follows it, so '^i' means 'pointer to int' and '^v' means 'pointer to void'. Square brackets '[ ]' encode a fixed-size C array, written as the element count followed immediately by the element's own encoding (for example '[10i]' for an array of ten ints), and curly braces '{ }' encode a struct, written as the struct's tag name followed by '=' and then each member's encoding back to back, with no separators -- exactly the mechanism worked through in full below with CGRect.
The letter 'B' deserves special attention because of a platform-dependent subtlety that trips people up: it stands for a real C/C++ bool, and it is the encoding modern Apple platforms use for Objective-C's BOOL, because on 64-bit Apple platforms BOOL is typedef'd to the genuine C bool type. On legacy 32-bit-era code, however, BOOL was historically typedef'd to signed char instead, in which case the very same source-level BOOL value was encoded as plain 'c', not 'B'. The same word BOOL in your source code can therefore legitimately produce two different single-character encodings depending on the platform and SDK the code was compiled against -- exactly the kind of subtlety type encodings exist to make explicit rather than leaving implicit.
• c/i/s/l/q: char, int, short, long, long long -- the encoding reflects the actual C type, never a typedef name like NSInteger. • f/d: float and double. • v: void, most commonly seen as the return-type character for a method that returns nothing. • @ / # / : object, Class, and SEL -- the three Objective-C-specific single-character codes. • ^ prefixes any other encoding to mean 'pointer to that type'; [ ] encodes a fixed-size array; { } encodes a struct. • B encodes a real C/C++ bool -- and is what BOOL encodes to on modern 64-bit Apple platforms, while legacy 32-bit BOOL (a signed char) encoded as plain 'c'.
@encode(...): Turning a Type Into Its Encoding String at Compile Time
@encode(...) is a compiler directive, syntactically similar to sizeof, that takes any type name and expands -- entirely at compile time, with nothing executing at runtime -- into a literal C string containing that type's encoding. @encode(int) expands to the C string "i"; @encode(id) expands to "@"; @encode(SEL) expands to ":". Nothing about this involves inspecting an actual value; @encode operates purely on the type name you give it, exactly like sizeof does.
@encode is the primitive building block every other type-encoding-producing API in the runtime is ultimately built from. property_getAttributes returns an attribute string that embeds a property's type encoding produced this same way; ivar_getTypeEncoding returns an instance variable's encoding directly; and method_getTypeEncoding -- covered in depth below -- returns a full method's combined type-encoding string, assembled from exactly the same per-type encodings @encode produces for each individual piece.
Because @encode resolves entirely at compile time, it works uniformly on primitive C types, Objective-C object types, pointers, arrays, and structs alike -- there's a single directive for the entire type system, which is exactly why it's worth learning to read its output fluently rather than treating specific encoding strings as things to memorize case by case.
• @encode(SomeType) expands, entirely at compile time, into a C string literal containing SomeType's encoding -- nothing runs. • It works uniformly across primitives, object types, pointers, arrays, and structs -- one directive for the whole type system. • property_getAttributes, ivar_getTypeEncoding, and method_getTypeEncoding all return strings built from these same per-type encodings.
Struct Encodings: A Fully Worked Example With CGRect
Struct encodings are where the alphabet's compound rules come together, and CGRect is the canonical worked example because it nests one level deep: a CGRect is a struct containing a CGPoint and a CGSize, each of which is itself a struct containing two doubles.
Build it from the inside out. CGPoint has two double members (x and y), so its encoding is "{CGPoint=dd}" -- the struct's tag name, an equals sign, then each member's encoding concatenated with no separators (two 'd' characters, one per double). CGSize has two double members as well (width and height), giving the identical shape: "{CGSize=dd}". CGRect itself has two members, an origin of type CGPoint and a size of type CGSize -- so its encoding is not "{CGRect=@@}" or anything referencing the member names; it is the two member structs' own full encodings, concatenated: "{CGRect={CGPoint=dd}{CGSize=dd}}".
Notice what is deliberately absent from this string: there is no mention of the member names origin or size anywhere in it, and no separators between the two nested struct encodings beyond the braces each one already carries. Type encodings describe layout and type, not the source-level names attached to that layout -- which is exactly enough information for NSInvocation and friends to know how many bytes to copy and how they're structured, without needing to know what anyone chose to call the fields.
• CGPoint (x, y: double, double) encodes as {CGPoint=dd}. • CGSize (width, height: double, double) encodes as {CGSize=dd}, an identical shape to CGPoint. • CGRect (origin: CGPoint, size: CGSize) encodes as {CGRect={CGPoint=dd}{CGSize=dd}} -- the two member structs' full encodings, concatenated. • Struct encodings never include member names -- only the struct's own tag name and its members' types, recursively.
Full Method Type-Encoding Strings: Return Type + self + _cmd + Arguments
A single type's encoding is only half the picture; the runtime also needs to describe an entire method's signature as one string, and it does so using a well-defined layout that every Objective-C method follows. Every instance method, compiled down, secretly receives two hidden parameters before any of its declared arguments: the receiver, always passed as the first parameter and encoded '@', and the selector being invoked (_cmd), always passed second and encoded ':'. This is not a coincidence or a convention layered on top of the language -- it is the literal mechanism a message send becomes at the ABI level, the exact subject of the upcoming objc_msgSend in Depth lesson.
A full method type-encoding string, in the classic format the runtime still emits, begins with the return type's encoding, followed by the total size in bytes of the argument frame, and then, for each parameter in order (self, then _cmd, then each declared argument), that parameter's own encoding immediately followed by the byte offset at which it appears in the call frame.
Walk through a concrete string field by field: "@24@0:8@16" describes a method along the lines of - (id)doSomethingWithObject:(id)obj. Reading left to right: the leading '@' is the return type -- an object (id). '24' is the total argument-frame size in bytes. Then '@0' says the first parameter is an object (self) at byte offset 0. ':8' says the second parameter is a SEL (_cmd) at byte offset 8. '@16' says the one declared argument (obj) is an object at byte offset 16. Every piece of this string is produced from the same single-character alphabet covered above -- a method's full signature is simply that alphabet applied once per parameter, in a fixed, predictable order.
• Every instance method secretly takes self ('@') and _cmd (':') as its first two parameters, before any declared arguments -- this is what a message send compiles down to. • A full method type-encoding string is: return-type-encoding, total-frame-size, then each parameter's encoding-plus-byte-offset, in order. • "@24@0:8@16" reads as: returns an object; 24-byte argument frame; self (object) at offset 0; _cmd (SEL) at offset 8; one object argument at offset 16. • This exact layout is what NSMethodSignature parses back apart, and what the objc_msgSend ABI physically realizes at the machine level.
Why Exact Type Signatures Matter: NSMethodSignature, NSInvocation, and objc_msgSend
The sibling NSMethodSignature lesson covers how an NSMethodSignature object is built directly from a method's type-encoding string -- either by asking the runtime for a selector's real encoding, or, less commonly, by constructing one manually with signatureWithObjCTypes:. The sibling NSInvocation lesson covers how NSInvocation then uses that NSMethodSignature to know exactly how many bytes each argument and the return value occupy, and at what offsets, so it can correctly copy argument bytes into a raw call frame before invoking, and copy the result back out afterward.
This is exactly why an incorrect encoding is not a cosmetic mistake but a memory-corruption or crash-level bug. If code constructs an NSMethodSignature (or, worse, a hand-built one passed to signatureWithObjCTypes:) using an encoding string that lies about a parameter -- claiming an argument is a 4-byte int ('i') when the real method expects an 8-byte object pointer ('@'), or claiming a return type is 'v' (void) when the real method returns a struct -- NSInvocation will read and write the wrong number of bytes at the wrong offsets in the underlying call frame. Meanwhile, the actual call the runtime performs underneath still obeys the method's real, compiler-generated ABI, completely unaware that the encoding-based bookkeeping is now lying about it. The result is silently corrupted adjacent argument memory, a garbage return value, or an outright crash, and these bugs are notoriously hard to track down precisely because nothing about the mismatch is visible in ordinary source code -- it only surfaces as a wrong string passed somewhere deep in reflection-style code.
The same fragility resurfaces in message forwarding: forwardInvocation: relies on the NSInvocation it's handed carrying a completely accurate signature, obtained from methodSignatureForSelector:, and getting that wrong breaks forwarding in exactly the same way described above. It resurfaces again, at an even lower level, in the upcoming objc_msgSend in Depth lesson, where calling the raw objc_msgSend function directly requires casting it to a C function pointer type that must match a method's real encoded signature exactly -- the identical failure mode as a wrong NSMethodSignature, just one layer closer to the machine.
• NSMethodSignature is built directly from a method's encoded type string -- it is the encoding, made queryable. • NSInvocation uses that signature's per-parameter type and size information to copy argument and return-value bytes to and from a raw call frame correctly. • A wrong encoding causes NSInvocation to read/write the wrong number of bytes at the wrong offsets, while the real underlying call still follows the method's true ABI -- silent corruption or a crash. • forwardInvocation: depends on an accurate signature from methodSignatureForSelector: for exactly this reason; objc_msgSend in Depth shows the same fragility one level lower, in the raw function-pointer cast.
Code Examples
Each @encode(...) expands, at compile time, into the exact single-character (or bracketed) encoding from the table above -- nothing executes; the compiler simply substitutes the literal C string. Note that @encode(int *) prepends '^' to int's own encoding 'i', and @encode(int[10]) writes the element count directly inside the brackets followed by the element's encoding, matching the array rule from the table.
NSLog(@"%s", @encode(int)); // "i" id, Class, and SEL each get their own dedicated single-character code -- these three, together with the object-pointer code '@', are the Objective-C-specific additions to an otherwise plain-C alphabet. BOOL is the subtle one: because it's a typedef rather than a distinct built-in type, its actual encoding character tracks whatever real type it currently resolves to on the target platform ('B' for a genuine bool on modern 64-bit Apple platforms, historically 'c' for the old signed-char BOOL).
NSLog(@"%s", @encode(BOOL)); // "B" on modern 64-bit platforms
CGRect's encoding is not a flat list of its own fields -- it is the full, recursive encodings of its two member structs, concatenated with no separators beyond the braces each one already carries. Notice that neither 'origin' nor 'size' (the member names) appear anywhere in the string; only the struct tag names and the underlying primitive types do, which is exactly the information NSInvocation needs to know the byte layout without needing to know what anyone called the fields.
// "{CGRect={CGPoint=dd}{CGSize=dd}}" method_getTypeEncoding returns exactly the string described above: the return type's own @encode-style character, the total frame size, and then every parameter -- always starting with the hidden self and _cmd pair -- with its type character immediately followed by its byte offset. This is the raw material NSMethodSignature parses into a queryable object, and it is generated automatically by the compiler for every single method in the binary.
const char *encoding = method_getTypeEncoding(m); // "@24@0:8@16" wrongSig lies about the single argument's type and size, so NSInvocation, which trusts the signature completely, copies the wrong number of bytes at the wrong offset when setting or reading that argument -- while the underlying call still runs against the method's real, unforgiving ABI. This is exactly the class of bug the previous section warns about: correctness here depends entirely on the encoding string matching reality, with no compiler check to catch a mismatch.
NSMethodSignature *wrongSig = [NSMethodSignature signatureWithObjCTypes:"@20@0:8i16"];
Common mistake: Hand-constructing or otherwise obtaining a type-encoding string that doesn't exactly match a method's real signature, then trusting it with NSInvocation -- there is no runtime check that catches this; it manifests later as corrupted memory or a crash.
NSLog(@"%s", @encode(int)); // "i"
NSLog(@"%s", @encode(short)); // "s"
NSLog(@"%s", @encode(long long)); // "q"
NSLog(@"%s", @encode(float)); // "f"
NSLog(@"%s", @encode(double)); // "d"
NSLog(@"%s", @encode(void)); // "v"
NSLog(@"%s", @encode(int *)); // "^i" -- '^' prefixes 'pointer to'
NSLog(@"%s", @encode(int[10])); // "[10i]" -- array of 10 intsNSLog(@"%s", @encode(id)); // "@"
NSLog(@"%s", @encode(Class)); // "#"
NSLog(@"%s", @encode(SEL)); // ":"
// On modern 64-bit Apple platforms, BOOL is typedef'd to a real C/C++
// bool, so it encodes as 'B':
NSLog(@"%s", @encode(BOOL)); // "B"
// Under the legacy 32-bit convention, BOOL was typedef'd to signed char
// instead, so the SAME source-level BOOL would have encoded as 'c' --
// the encoding character depends on the platform's BOOL typedef, not
// just the word "BOOL" in source code.NSLog(@"%s", @encode(CGPoint)); // "{CGPoint=dd}"
NSLog(@"%s", @encode(CGSize)); // "{CGSize=dd}"
NSLog(@"%s", @encode(CGRect)); // "{CGRect={CGPoint=dd}{CGSize=dd}}"
// Built from the inside out:
// CGPoint { CGFloat x; CGFloat y; } -> two doubles -> {CGPoint=dd}
// CGSize { CGFloat width; CGFloat height;}-> two doubles -> {CGSize=dd}
// CGRect { CGPoint origin; CGSize size; } -> the two struct
// encodings above,
// concatenated in order@interface Greeter : NSObject
- (id)doSomethingWithObject:(id)obj;
@end
Method m = class_getInstanceMethod([Greeter class],
@selector(doSomethingWithObject:));
const char *encoding = method_getTypeEncoding(m);
NSLog(@"%s", encoding); // "@24@0:8@16"
// Reading left to right:
// @ -> return type is an object (id)
// 24 -> total argument-frame size, in bytes
// @0 -> parameter 1 (self) is an object, at byte offset 0
// :8 -> parameter 2 (_cmd) is a SEL, at byte offset 8
// @16 -> parameter 3 (obj) is an object, at byte offset 16// Correct: matches the real method's actual signature exactly, as
// method_getTypeEncoding would return it for -doSomethingWithObject:.
NSMethodSignature *correctSig =
[NSMethodSignature signatureWithObjCTypes:"@24@0:8@16"];
// WRONG: claims the one declared argument is an int ('i', laid out and
// sized differently) instead of an object ('@'), while the REAL method
// still expects and will receive an object pointer at the ABI level.
NSMethodSignature *wrongSig =
[NSMethodSignature signatureWithObjCTypes:"@20@0:8i16"];
NSInvocation *invocation =
[NSInvocation invocationWithMethodSignature:wrongSig];
// Any argument bytes set or read through 'invocation' from here on are
// interpreted using the WRONG layout that 'wrongSig' describes -- but
// the actual call the runtime performs underneath still obeys the
// method's true, compiler-generated ABI, completely unaware anything
// is wrong. The mismatch typically shows up as corrupted adjacent
// memory or an outright crash, often far from this code.Objective-C Type Encoding Table
The core single-character (and bracket/brace) alphabet the compiler uses to describe every C and Objective-C type to the runtime -- the building blocks every method type-encoding string, property attribute string, and NSMethodSignature is assembled from.
c char
i int
s short
l long
q long long
f float
d double
B C++ bool / Objective-C BOOL-related encoding context
v void
@ object
# Class
: SEL
^ pointer
[ ] array
{ } structWhere This Actually Shows Up in Practice
Most Objective-C code never touches a type-encoding string directly, and that's by design -- the compiler emits them, and NSMethodSignature/NSInvocation consume them, entirely behind the scenes for ordinary message sends. Type encodings become directly visible, and directly load-bearing, in exactly three situations covered across this addendum: implementing custom message forwarding (forwardInvocation: and methodSignatureForSelector:), building dynamic proxy objects that construct NSInvocations for selectors they don't statically know about, and any tooling that walks the runtime's metadata directly (class-dumping tools, mocking frameworks, and archival/serialization code that needs to know a property's or method's exact underlying type). In every one of those situations, an encoding string that doesn't exactly match reality is the single most common source of hard-to-reproduce crashes.
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@0:8i16"];
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v24@0:8@16"];
The wrong version both omits the total argument-frame size after the return type and claims the one argument is an 'i' (int) rather than the '@' (object) the real method actually declares. Either error alone is enough to make NSInvocation read or write the wrong bytes; the frame size and every parameter's type must match the method's real, compiler-generated encoding exactly.
// Assuming struct member names appear in a struct's type encoding: // expecting @encode(CGRect) to look like "{CGRect=origin:{...} size:{...}}"
// Struct encodings never include member names -- only the struct's // tag name and its members' own types, recursively: // @encode(CGRect) -> "{CGRect={CGPoint=dd}{CGSize=dd}}"
It's a common assumption that a struct's encoding would preserve field names the way source code does, since that's the information a human reading the struct would want. It doesn't -- type encodings describe layout and type only, which is exactly the information NSInvocation and the runtime actually need to copy bytes correctly.
What does @encode(CGRect) produce, and why does it embed two other struct encodings inside it? A. "@" -- CGRect is treated as a generic object type B. "{CGRect=dddd}" -- CGRect is flattened into four raw doubles with no nesting C. "{CGRect={CGPoint=dd}{CGSize=dd}}" -- because CGRect is itself a struct containing a CGPoint and a CGSize, each of which is recursively encoded in full D. "[2{dd}]" -- CGRect is treated as an array of two two-field structs
C. "{CGRect={CGPoint=dd}{CGSize=dd}}" -- because CGRect is itself a struct containing a CGPoint and a CGSize, each of which is recursively encoded in full — CGRect's members are a CGPoint (origin) and a CGSize (size), not raw doubles directly, so its encoding recursively embeds each member struct's own full {Tag=...} encoding, concatenated in declaration order -- {CGRect={CGPoint=dd}{CGSize=dd}}.
In the method type-encoding string "@24@0:8@16", what does the very first '@' represent? A. The self parameter's type B. The method's return type -- in this case, an object (id) C. The total argument-frame size D. The _cmd parameter's type
B. The method's return type -- in this case, an object (id) — A full method type-encoding string always begins with the return type's own encoding. Here it's '@', meaning the method returns an object; self's encoding appears later, as the first offset-tagged parameter (@0).
Why can the same source-level BOOL value produce different single-character type encodings on different platforms or SDKs? A. It can't -- BOOL always encodes as 'B' on every Objective-C platform B. BOOL is a typedef, and its underlying real type differs by platform: a genuine C/C++ bool (encoding 'B') on modern 64-bit Apple platforms versus a legacy signed char (encoding 'c') historically on 32-bit platforms C. The encoding depends on the value stored in the BOOL (YES vs. NO), not on its type D. BOOL is always encoded identically to id, since both are considered generic types
B. BOOL is a typedef, and its underlying real type differs by platform: a genuine C/C++ bool (encoding 'B') on modern 64-bit Apple platforms versus a legacy signed char (encoding 'c') historically on 32-bit platforms — @encode reflects a type's actual underlying C representation, not the typedef name written in source. Because BOOL's underlying type has changed across platform generations (signed char historically, a real bool on modern 64-bit Apple platforms), its encoding character changes correspondingly.
Why is an exact, correct type-encoding string critical specifically for NSInvocation, rather than just a nice-to-have? A. NSInvocation ignores type encodings entirely and infers argument types at runtime by inspecting the actual objects passed B. NSInvocation uses the encoded type and size of each parameter to know exactly how many bytes to copy, and at what offset, into and out of a raw call frame -- a wrong encoding causes it to read or write the wrong bytes while the real underlying call still follows the method's true ABI C. A wrong type encoding only affects NSLog output formatting, never the actual invocation D. NSInvocation recompiles the method at runtime using whatever encoding it's given, so a wrong encoding just changes what the method does, safely
B. NSInvocation uses the encoded type and size of each parameter to know exactly how many bytes to copy, and at what offset, into and out of a raw call frame -- a wrong encoding causes it to read or write the wrong bytes while the real underlying call still follows the method's true ABI — NSInvocation has no independent way to verify a signature's accuracy -- it trusts the encoded string completely for byte counts and offsets, while the actual call underneath still obeys the method's real ABI. A mismatch is therefore a genuine memory-safety bug, not a cosmetic one.
Which two parameters does every Objective-C instance method's type encoding always include before any of its declared arguments? A. The class name and the method name, both encoded as C strings B. self, encoded '@', and _cmd, encoded ':' C. The method's return type, encoded twice for safety D. A retain count and an autorelease flag
B. self, encoded '@', and _cmd, encoded ':' — Every instance method secretly receives the receiver (self, encoded '@') and the selector being invoked (_cmd, encoded ':') as its first two parameters -- this is the literal mechanism a message send compiles down to at the ABI level, covered further in the objc_msgSend in Depth lesson.
What does the '^' character mean when it appears in a type encoding, such as in "^i"? A. It marks the start of a struct definition B. It means 'pointer to' -- combined with the following encoding, so "^i" means 'pointer to int' C. It is the encoding for a block pointer specifically D. It indicates the value is optional (nullable)
B. It means 'pointer to' -- combined with the following encoding, so "^i" means 'pointer to int' — '^' is a prefix operator in the encoding alphabet: it always combines with whatever encoding immediately follows it to mean 'pointer to that type' -- "^i" is pointer-to-int, "^v" is pointer-to-void, and so on.
Write a struct named Rect3D containing three CGFloat fields (width, height, depth), then write the @encode(...) string you would expect for it by hand, following the same rule CGRect was built from. Then declare an Objective-C method - (Rect3D)volumeBox:(CGFloat)scale; and write out, field by field the way "@24@0:8@16" was dissected above, what you would expect its full method type-encoding string's early portion (return type through _cmd) to look like, explaining each token.
@encode(Rect3D) -> "{Rect3D=ddd}" (three CGFloat/double members, tag name Rect3D, no member names).\n\nFor - (Rect3D)volumeBox:(CGFloat)scale;, the string begins with the RETURN type's full encoding -- not a single character, since Rect3D is a struct -- so it starts "{Rect3D=ddd}" followed by the total frame size, then "@0" for self, ":8" for _cmd (SEL is always 8 bytes on 64-bit), and finally "d16" for the scale argument (a CGFloat/double) at whatever offset follows _cmd.
A struct's encoding is its tag name, '=', then each member's own encoding concatenated with no separators -- CGFloat is a double-width float on 64-bit platforms, encoded 'd'. A method's full string always begins with the return type's own encoding, exactly the way a struct return type's full brace-encoding would appear there in place of a simple letter.