Objective-C Class Anatomy: Reading a Complete Class, Line by Line

Read and fully explain every line of a complete, real Objective-C class — the @interface declaration, @property declarations, method declarations, the @implementation, the designated initializer's self = [super init] pattern and nil-check, direct ivar access versus property access, defensive copying, and NSLog's %@ specifier — building the foundational mental model for every class you'll write in the rest of this course.

Everything up to this point — C fundamentals, message-send syntax, Objective-C's type vocabulary — has been building toward this moment: reading a complete, real class definition, top to bottom, and understanding exactly what every single line does and why it's written that way. This is one of the most important lessons in the entire course, because the pattern you're about to see — an @interface, a handful of @property declarations, an initializer with a very particular shape, and a few instance methods — is the shape that the overwhelming majority of Objective-C classes you will ever write or read follow. Several pieces here are previewed rather than fully exhausted (properties, copy semantics, the full mechanics of inheritance) because dedicated modules later in the course go much deeper into each — but by the end of this lesson, you should be able to look at any simple Objective-C class and understand, concretely, what every part of it is doing.

The Full Example: A Person Class

Here is the complete class this lesson is built around, split (as real Objective-C code always is) into two files' worth of content: an interface, declaring what the class looks like from the outside, and an implementation, providing the actual code. We'll look at the whole thing first, then take it apart piece by piece.

The interface (conventionally placed in a .h header file) declares that Person exists, what data it exposes (name and age), and what it can do (sayHello, and an initializer). The implementation (conventionally in a matching .m file) provides the actual working code behind those declarations. This split — declare in one place, define in another — mirrors a pattern you may already sense an echo of from C: a function's declaration (its signature) can be separate from its definition (its body), and Objective-C classes extend that same idea to an entire bundle of behavior and data at once.

• @interface...@end declares what a class looks like from the outside: its properties and method signatures. • @implementation...@end provides the actual working code behind those declarations. • Conventionally: interface in a .h file, implementation in a matching .m file. • This mirrors C's declaration/definition split, extended to an entire class's data and behavior.

@interface Person : NSObject — Declaring the Class

@interface Person : NSObject is the opening line of the class declaration. @interface is an Objective-C keyword (the @ prefix marks it, and many other constructs you'll meet, as Objective-C-specific syntax layered on top of plain C). Person is the name being given to this new class. The colon followed by NSObject declares that Person is a subclass of NSObject — meaning Person inherits all of NSObject's behavior and adds its own on top.

NSObject is the root class of the vast majority of the Objective-C class hierarchy — nearly every object you'll work with in this course, directly or indirectly, ultimately descends from NSObject. It provides foundational behavior every object needs: memory management hooks, the ability to respond to being asked what class it is, basic equality and description support, and more, all covered properly in later modules. Subclassing it here means Person starts with all of that machinery already working, and only needs to add what's specific to representing a person: a name, an age, and a greeting.

It's worth noting the general shape here, because you'll type it constantly: @interface SomeClassName : SomeSuperclass is the pattern for declaring any class as a subclass of any other. When Person later needs to be more specialized (say, an Employee subclass of Person), the exact same pattern applies, just with Person in the superclass position instead of NSObject — a topic the Inheritance module covers in full depth.

• @interface — the keyword beginning a class declaration; the @ prefix marks Objective-C-specific syntax. • Person — the name of the new class being declared. • : NSObject — declares Person as a subclass of NSObject, inheriting its behavior. • NSObject is the root class most Objective-C classes ultimately descend from, providing foundational object behavior. • General pattern: @interface SubclassName : SuperclassName for declaring any class as a subclass of any other.

The @property Declarations: A Preview

@property (nonatomic, copy) NSString *name; and @property (nonatomic, assign) NSInteger age; declare Person's two pieces of data. @property is a declaration that tells the compiler: this class exposes a piece of data named name (an NSString *, following the object-pointer rule from the previous lesson) and another named age (an NSInteger, the portable whole-number type also from the previous lesson).

Writing @property does much more than a plain instance variable declaration would: behind the scenes, it generates a getter method (so other code can read person.name or, equivalently, call [person name]), a setter method (so other code can write person.name = @"Ana";, equivalently [person setName:@"Ana"]), and backing storage to actually hold the value between those calls. All of this is previewed here specifically so the initializer below makes sense; the full mechanics of what @property generates, exactly how the getter/setter pair work, and what the different attributes in parentheses mean, are covered in complete depth in the dedicated Properties module later in this course.

The words in parentheses — nonatomic, copy, assign — are property attributes, and you'll learn their full meaning later, but it's worth a preview now since they appear in the code you're reading: nonatomic relates to thread-safety behavior of the generated getter/setter (covered in a later module), copy means the generated setter for name will store a copy of whatever string it's given rather than the original object (directly connected to the [name copy] call you'll see in the initializer below), and assign means the generated setter for age simply stores the value directly, which is the normal, simplest behavior for a primitive value like NSInteger that isn't a pointer to begin with.

• @property (nonatomic, copy) NSString *name; and @property (nonatomic, assign) NSInteger age; declare Person's data. • @property generates a getter, a setter, and backing storage — far more than a plain instance variable declaration. • Full @property mechanics (getter/setter generation, attribute meanings) are covered in depth in the Properties module. • copy (for name): the setter stores a copy of the given string, connecting directly to [name copy] in the initializer below. • assign (for age): the setter stores the primitive value directly — the normal behavior for a non-pointer type.

The Method Declarations: Instance Methods and a Named-Parameter Initializer

- (void)sayHello; declares an instance method. The leading - (as opposed to +, covered in a later module for class methods) marks this as an instance method — one you call on a specific instance of Person, not on the Person class itself. (void) is the method's return type in parentheses, exactly mirroring how you've already seen return types written for methods — here, void means sayHello doesn't hand back a value. sayHello is the selector — the name identifying this method, with no colon, matching the no-argument selector pattern from the Messaging Mental Model lesson.

- (instancetype)initWithName:(NSString *)name age:(NSInteger)age; declares the second method, spread across two lines purely for readability (line breaks inside a declaration like this don't change its meaning). instancetype is a special return type specifically meant for initializers and factory methods — it tells the compiler 'this method returns an instance of whatever class it was actually called on,' which matters correctly even for subclasses (covered further in the Inheritance module) in a way that writing Person or id explicitly would not handle as precisely.

The selector here is initWithName:age: — a two-part selector, following exactly the multi-argument pattern from the previous Messaging lesson: initWithName: is immediately followed by its parameter (NSString *)name (type in parentheses, then the parameter's local name), and age: is immediately followed by (NSInteger)age. Reading the whole declaration as a sentence: 'this method, called init-with-name-and-age, takes an NSString pointer named name and an NSInteger named age, and returns instancetype.'

• - (void)sayHello; — instance method (leading -), returns void, no-argument selector sayHello. • - (instancetype)initWithName:(NSString *)name age:(NSInteger)age; — instance method, two-part selector initWithName:age:. • instancetype: a special return type for initializers/factory methods, meaning 'an instance of whatever class this was actually called on'. • Each selector part is immediately followed by its (Type)paramName in parentheses, exactly matching the multi-argument message pattern already learned.

@implementation and @end — Where the Real Code Lives

@implementation Person opens the block where Person's actual method bodies live — the working code behind the declarations made in the @interface. Just as @interface Person : NSObject ... @end bracketed the declaration, @implementation Person ... @end brackets the implementation; every method whose signature was declared in the interface needs a matching body here (with the exception of anything the class inherits unchanged from NSObject and doesn't override).

Notice that the method signatures reappear here, almost identically to how they looked in the @interface, but now followed by a body in curly braces instead of a semicolon: - (instancetype)initWithName:(NSString *)name age:(NSInteger)age { ... } rather than ending in ;. This mirrors, once again, the C pattern of a function declaration (ending in ;, no body) versus a function definition (ending in { ... }, with a body) — Objective-C simply applies that same declared-versus-defined distinction at the level of an entire class's methods.

@end closes the implementation block, just as it closed the interface block earlier. Every @interface and every @implementation in Objective-C is bracketed by its own matching @end — forgetting it is a compiler error, much like forgetting a closing curly brace in C.

• @implementation Person ... @end — provides the actual method bodies for everything declared in @interface. • Method signatures reappear here nearly identically, but end in a { ... } body instead of a semicolon. • Mirrors C's declaration-vs-definition distinction, applied to an entire class's methods. • @end closes both @interface and @implementation blocks; omitting it is a compiler error.

The Initializer: self = [super init]; and the if (self) Nil-Check

The initializer's body is the single most important pattern in this entire lesson, and it's worth reading with real care: self = [super init]; followed by if (self) { ... } followed by return self;. This exact shape appears, almost verbatim, at the start of nearly every custom initializer you will ever write in Objective-C, so it's worth understanding precisely what each part does and why.

[super init] sends the init message to super — a special reference (not a real variable, but a compiler-understood keyword) meaning 'my superclass's implementation, rather than my own.' Sending init to super here calls NSObject's own initializer, letting NSObject set up whatever it needs to set up for a valid, minimally-working object before Person's own initializer adds anything on top. This matters because every subclass's initializer is expected to first let its superclass finish initializing itself, working up the inheritance chain, rather than skipping straight to the subclass's own setup on a not-yet-properly-initialized object.

The crucial, easy-to-miss detail is self = [super init]; — assigning the result of [super init] back to self, rather than just calling [super init]; on its own and assuming it succeeded. Why? Because init methods in Objective-C are permitted to return something other than the object you started with — in rare cases, a superclass's initializer might return nil (signaling that initialization failed and there is no usable object at all), or, less commonly, might even return a different object entirely (a pattern used by certain class clusters, previewed here, covered later). If you simply called [super init]; without capturing and reassigning self, and the superclass's initializer happened to return something different from (or nothing at all, if it fails) the original self, you'd continue working with a stale, no-longer-correct reference. Assigning the result back to self ensures you're always working with whatever [super init] actually decided is the real, live object going forward — never assuming success or an unchanged identity.

if (self) { ... } is the defensive nil-check this pattern exists to protect: because self = [super init]; might have set self to nil (if the superclass's initialization failed for any reason), checking if (self) before doing any further setup ensures Person's own initialization code — setting _name and _age — only runs when there's actually a valid object to set them on. If self is nil, the if block is skipped entirely, and return self; correctly hands back nil, faithfully propagating the failure up to whoever called this initializer, rather than crashing or silently operating on a nonexistent object.

• [super init] calls the superclass's (NSObject's) own initializer first, before Person's own setup runs. • self = [super init]; reassigns self to whatever [super init] actually returns — never assume it returns the original self unchanged. • Reassigning matters because a superclass initializer can, in principle, return nil (failure) or even a different object entirely. • if (self) { ... } is a defensive nil-check: Person's own setup (_name, _age) only runs if initialization has succeeded so far. • return self; faithfully returns nil if initialization failed upstream, or the fully-initialized object if it succeeded.

_name and _age: Direct Ivar Access Inside the Class's Own Code

Inside the initializer, the actual assignments are _name = [name copy]; and _age = age; — note the leading underscore, and note that this is different from writing self.name = ... or self.age = .... _name and _age refer directly to the instance variables (ivars) that back the name and age properties — the actual raw storage @property generates behind the scenes, accessed directly rather than through the generated getter/setter methods.

Why access the ivar directly here, rather than going through the property (self.name = [name copy];)? Inside an initializer specifically, this is the conventional, recommended approach, for a subtle but important reason previewed here and covered fully in the Properties module: going through self.name = ... would invoke the generated setter method, and a setter can be overridden by a subclass to do additional work (side effects, notifications, triggering other logic) that may not be safe or meaningful to run on an object that isn't fully initialized yet. Setting the ivar directly bypasses any of that generated or overridden setter behavior entirely, guaranteeing the initializer only does exactly what it says: store the value, nothing more, during the one window (initialization) where that guarantee matters most.

Outside of initializers (and a few other special cases covered later), the convention flips: ordinary code, including Person's own other methods, should generally prefer going through the property (self.name) rather than the ivar directly, exactly because that's what lets the generated (or overridden) getter/setter logic actually run as intended. You'll see this distinction again directly below, in sayHello.

• _name / _age — direct access to the instance variables (ivars) that @property generates as backing storage. • Different from self.name / self.age, which go through the generated getter/setter methods instead. • Convention: use direct ivar access (_name) inside initializers, to avoid triggering setter side effects on a not-yet-fully-initialized object. • Convention: use property access (self.name) in ordinary methods outside initializers, so getter/setter logic runs as intended.

[name copy]: Defensive Copying — A Preview

_name = [name copy]; doesn't just store the name parameter directly — it sends copy to it first and stores the result. This is defensive copying, and the reason it matters is previewed here (with the full explanation reserved for the Copy Semantics module later): NSString * is the type declared for the parameter, but Objective-C also has a mutable variant, NSMutableString, which is a subclass of NSString and can be passed anywhere an NSString * is expected.

If Person simply stored the caller's original string object directly (_name = name;, without copying), and the caller happened to pass in a mutable string, the caller could later mutate that same string object out from under Person — changing what person.name reports, from outside the class, without Person doing anything at all. Sending copy produces a new, independent string object with the same contents at that moment, fully insulating Person's own stored value from any future mutation of whatever the caller originally passed in.

This is exactly why the name property was declared @property (nonatomic, copy) NSString *name; earlier — the copy attribute tells the generated setter to do exactly this same defensive copying automatically. The initializer does it manually here (because, as covered above, initializers bypass the generated setter and set the ivar directly), matching by hand exactly what the property's own copy attribute would do automatically if you used self.name = name; instead.

• [name copy] creates a new, independent string with the same contents, rather than storing the caller's original object. • Defends against the caller passing a mutable NSMutableString and later changing it out from under Person. • Matches the @property's own copy attribute, which would do the same thing automatically via the generated setter. • Full copy semantics (deep vs. shallow, mutable vs. immutable classes) covered in the dedicated Copy Semantics module later.

sayHello: Property Access and NSLog's %@ Specifier

- (void)sayHello { NSLog(@"Hello %@", self.name); } is the simplest method in the class, and it demonstrates the property-access convention mentioned above: outside the initializer, self.name is used rather than _name directly, going through the generated getter as ordinary methods should.

NSLog is Objective-C's standard logging function — a close cousin of C's printf, but specifically meant for debug/diagnostic output, which additionally timestamps its output and writes to the system log as well as the console. The @"Hello %@" argument is an Objective-C string literal (the @ prefix before a double-quoted string creates an actual NSString * object, rather than a plain C string) used as a format string, exactly the way printf's format strings worked back in Module 1.

%@ is a format specifier, but a distinctly Objective-C one, absent from plain C's printf: it means 'insert this argument's object description here,' and works for any Objective-C object (not just strings) by sending it the description message (previewed here, covered later) and inserting whatever text that returns. This is different from C's %d, %f, %c specifiers, which each expect a specific primitive type — %@ expects an object pointer, and asks that object, dynamically, to describe itself as text.

Putting it together: sayHello reads self.name (going through Person's generated getter), builds a string like "Hello Ana" by substituting that name's description in for %@, and hands it to NSLog to print, timestamped, to the console — a small but complete demonstration of property access and Objective-C's extended printf-style formatting working together.

• self.name here (in an ordinary method, not the initializer) uses the generated getter, following the property-access convention. • NSLog: Objective-C's logging function, a cousin of printf, additionally timestamping output and writing to the system log. • @"..." is an Objective-C string literal, creating a real NSString * object (the @ prefix is what makes it Objective-C rather than plain C). • %@ — an Objective-C-specific format specifier that inserts an object's description (by sending it the description message), unlike C's primitive-only specifiers.

Code Examples

Person subclasses NSObject, the root class. It declares two properties (name, backed by a copied NSString; age, an NSInteger stored directly) and two instance methods: a no-argument sayHello, and a two-part initWithName:age: initializer returning instancetype. Everything here is a declaration only -- no bodies, each ending in a semicolon.

The initializer calls [super init] first and reassigns self to its result, checks if (self) before doing Person-specific setup, sets the ivars _name (defensively copied) and _age directly, and returns self (possibly nil, if initialization failed upstream). sayHello uses self.name (the generated getter, not the ivar) and NSLog's %@ specifier to print a greeting.

[[Person alloc] initWithName:@"Alice" age:30] first allocates memory for a Person (alloc, covered in the Memory module), then sends the initWithName:age: message to set it up, matching the two-part selector declared earlier. alice.age reads the age property through its generated getter; %ld with an explicit (long) cast is used here because NSInteger's exact underlying type varies by architecture, as covered in the previous lesson.

• (instancetype)initWithName:(NSString *)name age:(NSInteger)age { [super init]; // result discarded _name = name; // no copy, no nil-check _age = age; return self; } - (instancetype)initWithName:(NSString *)name age:(NSInteger)age { self = [super init]; if (self) { _name = [name copy]; _age = age; } return self; }

Discarding [super init]'s result means self might not actually be the valid object the superclass decided to hand back (or might need to be nil, on failure) -- always reassign self = [super init]; and guard subsequent setup with if (self). Also, storing name directly instead of [name copy] leaves Person vulnerable to the caller later mutating a passed-in mutable string.

• (instancetype)initWithName:(NSString *)name age:(NSInteger)age { self = [super init]; if (self) { self.name = [name copy]; // goes through the setter during init self.age = age; } return self; }

Using self.name/self.age inside an initializer routes through the generated (or possibly subclass-overridden) setter methods, which may assume the object is already fully initialized -- convention is to set the backing ivars (_name, _age) directly inside initializers, and use property access (self.name) elsewhere.

@interface Person : NSObject @property (nonatomic, copy) NSString *name; // missing @end

@interface Person : NSObject @property (nonatomic, copy) NSString *name; @end

Every @interface and @implementation block must be closed with a matching @end -- omitting it is a compiler error, exactly analogous to forgetting a closing curly brace in C.

NSLog(@"Hello %s", self.name); // %s expects a raw C string, not an NSString object

NSLog(@"Hello %@", self.name); // %@ correctly formats an NSObject-descended object self.name is an NSString * (an object), not a raw C string (char *) -- %s expects the latter and produces garbage or a crash with an object pointer. %@ is the Objective-C-specific specifier for any object, correctly sending it the description message.

Why does the initializer write self = [super init]; instead of simply [super init]; on its own? A. There's no real difference; both are equally correct B. Because [super init] might return nil (on failure) or a different object than the original self, and self must be reassigned to whatever it actually returns before continuing C. Because [super init] must always be assigned to a variable in Objective-C, regardless of context D. Because self is otherwise undefined inside an initializer

B. Because [super init] might return nil (on failure) or a different object than the original self, and self must be reassigned to whatever it actually returns before continuing — A superclass initializer can, in principle, return nil to signal failure or a different object entirely; reassigning self to its result ensures the rest of the initializer works with whatever object [super init] actually decided is correct, rather than a stale or invalid reference.

What is the purpose of the if (self) { ... } check in the initializer? A. It checks whether the Person class exists B. It's purely stylistic and has no functional effect C. It defensively guards Person's own setup (_name, _age) so it only runs if [super init] actually succeeded (self is not nil) D. It checks whether name and age were passed as nil

C. It defensively guards Person's own setup (_name, _age) so it only runs if [super init] actually succeeded (self is not nil) — Since self = [super init]; might set self to nil if the superclass's initialization failed, checking if (self) ensures Person's own field assignments only run on a genuinely valid object, and the initializer correctly returns nil if initialization already failed upstream.

Why does the initializer use _name = [name copy]; (direct ivar access) rather than self.name = [name copy]; (property access)? A. Property access doesn't work at all inside Objective-C initializers B. Direct ivar access is required by the compiler for every method C. Using the property setter during initialization could trigger generated or subclass-overridden setter behavior on an object that isn't fully initialized yet; setting the ivar directly avoids that D. There is no difference between the two in any context

C. Using the property setter during initialization could trigger generated or subclass-overridden setter behavior on an object that isn't fully initialized yet; setting the ivar directly avoids that — Convention is to access ivars directly inside initializers specifically to avoid invoking setter logic (which could be overridden by a subclass or have side effects) on a not-yet-fully-initialized object; ordinary methods outside initializers should generally prefer property access instead.

What does sending copy to the name parameter ([name copy]) defend against? A. It prevents the app from crashing if name is nil B. It protects Person's stored value from being changed later if the caller passed in a mutable string (NSMutableString) and subsequently mutates it C. It converts name from an NSString to a C string D. It has no real effect and is purely idiomatic decoration

B. It protects Person's stored value from being changed later if the caller passed in a mutable string (NSMutableString) and subsequently mutates it — Since NSMutableString (a mutable subclass of NSString) can be passed anywhere an NSString * is expected, copying produces an independent string with the same contents at that moment, insulating Person from any later mutation of the original object the caller passed in.

What does the %@ format specifier do in NSLog(@"Hello %@", self.name);? A. It expects a raw C string (char *) and prints it directly B. It is Objective-C-specific and inserts an object's description, obtained by sending it the description message, working for any Objective-C object C. It only works with NSString and no other object type D. It prints the object's raw memory address only

B. It is Objective-C-specific and inserts an object's description, obtained by sending it the description message, working for any Objective-C object — %@ is a format specifier unique to Objective-C's extended printf-style formatting: it accepts any object pointer and inserts the text produced by sending that object the description message, unlike C's primitive-only specifiers such as %d or %s.

What does @interface Person : NSObject declare? A. That Person is a global variable of type NSObject B. That Person is a new class, subclassing NSObject and inheriting its behavior C. That Person overrides every method NSObject defines D. That Person and NSObject are the same class under two names

B. That Person is a new class, subclassing NSObject and inheriting its behavior — The @interface ... : NSObject syntax declares a new class named Person as a subclass of NSObject, meaning Person inherits NSObject's foundational behavior and adds its own properties and methods on top.

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

- (void)sayHello;
- (instancetype)initWithName:(NSString *)name
                          age:(NSInteger)age;

@end
@implementation Person

- (instancetype)initWithName:(NSString *)name
                          age:(NSInteger)age {
    self = [super init];

    if (self) {
        _name = [name copy];
        _age = age;
    }

    return self;
}

- (void)sayHello {
    NSLog(@"Hello %@", self.name);
}

@end
Person *alice = [[Person alloc] initWithName:@"Alice" age:30];
[alice sayHello];          // logs: Hello Alice

NSLog(@"Age: %ld", (long)alice.age);