@interface and @implementation: Splitting a Class into Public Contract and Private Body
Explain the purpose and syntax of @interface, @implementation, and @end; distinguish a class's public API from its private implementation details; and connect this split directly to the C header/source separation covered in Module 1.
The Class Anatomy lesson showed you a complete Person class — @interface, @implementation, @property, an initializer — all working together. This lesson slows down and looks at just two of those pieces in isolation: @interface and @implementation, the two blocks every Objective-C class is built from. They are not stylistic choices or optional organization; they are a deliberate, load-bearing split between what a class promises to the outside world and how it actually keeps that promise internally, and that split traces directly back to the header/source separation you already learned in plain C. Once you see the parallel, @interface and @implementation stop looking like new, arbitrary syntax and start looking like something you already understand, wearing a new outfit.
@interface: The Public Contract
An @interface block declares what a class exposes to the rest of your program — nothing more. As shown in the Class Anatomy lesson's Person class, the interface lists the class's properties (the data it's willing to let other code read and, depending on the property's attributes, write) and the signatures of the methods it responds to: a method's name, its parameters, and its return type, but not the code that runs when that method is called. @interface Person : NSObject opens the block, naming the class (Person) and its superclass (NSObject, the root class nearly everything in Objective-C ultimately inherits from); everything between that line and the matching @end is the declaration, not the definition.
Think of @interface as a promise, not an implementation. When you write - (void)celebrateBirthday; inside an @interface block, you are telling every other piece of code in the program: 'any Person object can be sent this message, it takes no arguments, and it returns nothing you need to capture.' You are not yet saying how celebrateBirthday actually increments the age or what else it might do — that's deliberately left out of the interface. This is exactly the same idea as a C function prototype in a header file, like int add(int a, int b); — the prototype tells every other .c file what add looks like and how to call it, without exposing (or requiring) the actual arithmetic inside its body.
Properties declared in @interface (@property (nonatomic, strong) NSString *name;, @property (nonatomic, assign) NSInteger age;) work the same way: they announce that a Person object has a name and an age that other code can access, and — because the compiler auto-generates accessor methods for every @property, as covered in Class Anatomy — the interface is implicitly also declaring -(NSString *)name;, -(void)setName:(NSString *)name;, and their counterparts for age, even though you never typed those method signatures by hand.
• @interface ClassName : SuperclassName declares a class's public contract: its properties and its method signatures. • A method signature in @interface is a promise of what can be called and what it returns — not the code that runs. • @property declarations in @interface implicitly declare their compiler-generated accessor methods too. • This is the direct Objective-C analogue of a C function prototype in a header file.
@implementation: Where the Real Work Happens
An @implementation block is where a class's method bodies actually live — the executable code that runs when a message is sent. @implementation Person opens the block (note: no superclass is repeated here; that was already stated in @interface), and inside it, every method declared in the interface gets its actual body: - (void)celebrateBirthday { self.age = self.age + 1; } is the real code that increments a Person's age. Nothing in @implementation is visible to other files just by virtue of being written there — only what was declared in the matching @interface is part of the class's known public surface.
This is the direct analogue of a C source file's function definitions. A C header declares int add(int a, int b);; the matching .c source file provides int add(int a, int b) { return a + b; } — the actual instructions. @implementation plays exactly that role for a class: it's where the promises made in @interface get kept, in real, runnable code. Just as a C source file can contain private helper functions never declared in the header (and therefore invisible to other files), an @implementation block can contain private methods never declared in @interface — a pattern covered in more depth in a later lesson on class extensions.
One detail worth internalizing now: the compiler checks @implementation against @interface. If you declare a method in @interface but never actually write its body in @implementation, you get a compiler warning (a broken promise); if you write a method in @implementation with a signature that doesn't match anything in @interface, it still compiles (Objective-C allows this, treating it as effectively private), but it means other files that only see the .h won't know it exists and can't call it without a compiler warning about an unrecognized selector.
• @implementation ClassName contains the actual method bodies — the real, executable code. • Direct analogue: a C source file's function definitions matching the header's prototypes. • A method can exist in @implementation without appearing in @interface — it behaves like a private method, invisible to other files. • The compiler expects every method promised in @interface to have a matching body in @implementation.
@end: Closing Both Kinds of Block
@end is simple but easy to forget: it marks the closing point of both an @interface block and an @implementation block — the same directive is reused for both, and Objective-C tells them apart from context (whether the matching opening line was @interface or @implementation). Every @interface must be closed with its own @end, and every @implementation must be closed with its own, separate @end — a single Person.h/Person.m pair of files therefore contains at least two @end lines total, one for each block.
Forgetting @end produces a compiler error, usually a confusing one about unexpected tokens or a missing @end somewhere later in the file, because the compiler keeps reading everything after the opening @interface or @implementation as if it still belonged to that block until it finds the closing @end. This is analogous to forgetting a closing curly brace } in C — the compiler doesn't necessarily error exactly where the mistake is; it errors wherever it finally realizes the structure never closed, which can be much further down the file and genuinely confusing to debug.
In practice, @interface...@end and @implementation...@end are typically written with the same indentation discipline you'd use for a C function body or an if-block, precisely so the eye can immediately see where each block starts and ends, the same visual habit that helps you spot a missing closing brace in C.
• @end closes both @interface and @implementation blocks — each opened block needs its own matching @end. • A single class's .h/.m files therefore contain at least two @end lines: one per block. • Forgetting @end causes a compiler error, often reported confusingly far from the actual missing line — like a missing closing brace in C. • Consistent indentation of the whole block helps you visually catch a missing @end, same as with C's { }.
Class Declaration vs. Class Implementation
It's worth being precise about the vocabulary here, because it maps directly onto ideas you already know. 'Declaring a class' means writing its @interface — stating its name, its superclass, its properties, and its method signatures, without providing any method bodies. 'Implementing a class' means writing its @implementation — providing the actual bodies for (at least) the methods that were declared. A single class conceptually needs both: a declaration alone can't run (there's no code to execute), and an implementation without a matching declaration compiles, but nothing outside that file can safely call its methods, because nothing else knows they exist.
In real Objective-C projects, @interface for a class almost always lives in its own .h header file (Person.h), and @implementation lives in the matching .m source file (Person.m) — the exact same file-splitting convention you already practiced with plain C headers and source files. Any other file that wants to create and use Person objects does #import "Person.h" (Objective-C's improved version of #include, which automatically prevents a header from being processed twice even without manual include guards) to see the interface, without ever needing to see or compile Person.m's actual implementation code directly — the linker resolves the connection at build time, exactly as it did for plain C object files.
This mirrors, almost line for line, the C pipeline lesson from Module 1: a C header declares what a function looks like; a C source file defines what it does; other files include the header to use the function, and the linker later stitches everything together into one executable. Objective-C's @interface/@implementation split is that same idea, wearing object-oriented syntax — which is exactly why, once you already understand headers and source files in C, this part of Objective-C requires no new mental model at all, only new keywords.
• Declaring a class = writing @interface (name, superclass, properties, method signatures, no bodies). • Implementing a class = writing @implementation (the actual method bodies). • Convention: @interface lives in a .h file, @implementation lives in the matching .m file. • #import "Person.h" is Objective-C's improved #include — it also prevents double-processing automatically. • This is the exact same declare-in-header / define-in-source / link-at-build-time pattern from plain C, expressed with new keywords.
Why Split at All: Encapsulation and Compile-Time Decoupling
This split is not incidental — it's a deliberate design choice with two real payoffs. The first is encapsulation: by only exposing what's in @interface, a class controls exactly what other code is allowed to depend on. Private helper methods, internal bookkeeping, implementation details that might change later, can all live in @implementation without ever becoming part of the class's public promise — meaning you're free to change how celebrateBirthday works internally (maybe it later also logs something, or checks a maximum age) without breaking any other file that only ever relied on the interface's promise: 'you can call celebrateBirthday, it takes nothing, it returns nothing.'
The second payoff is compile-time decoupling, directly inherited from C's header/source model. A file that only #imports Person.h does not need Person.m's source code to be present or even compilable in order to be compiled itself — it only needs to know the shapes described in the header. This means large projects can be split across many files and even many teams, each compiling independently against headers, with the linker doing the final stitching — exactly the pipeline benefit that made C's header/source split valuable in the first place, now serving the same role for classes instead of individual functions.
A useful way to hold both ideas together: @interface answers 'what can I do with this class?' and @implementation answers 'how does it actually do it?' — and any code outside the class should only ever need to answer the first question, never the second. This discipline — depend on the interface, never reach into the implementation — is one of the most durable habits in object-oriented programming generally, and it starts exactly here, with these two keywords.
• Encapsulation: only what's in @interface is a promise other code can rely on; @implementation details can change freely. • Compile-time decoupling: other files compile against the header alone, without needing the .m file's source present. • This enables large, multi-file (and multi-team) projects to build independently and let the linker stitch results together. • Rule of thumb: outside code should only ever need to know 'what can I do with this class', never 'how does it do it'.
Code Examples
This @interface declares Counter's public contract: it has an integer value property, and it responds to two messages, increment and resetToZero. Note that neither method has a body here — this file only states what exists and what can be called, exactly like a C header's function prototypes.
Counter.m #imports its own header so the compiler knows what it's implementing, then provides real bodies for both methods declared in the interface. Any other file that #imports Counter.h can create a Counter and call increment or resetToZero without ever seeing this file's source code directly.
logChange is called from inside increment but was never declared in Counter.h's @interface. It still compiles and works fine internally, but other files that only see the header don't know logChange exists and calling it from outside would trigger a compiler warning about an unrecognized method — this is Objective-C's informal way of keeping a method effectively private.
@interface Counter : NSObject - (void)increment; // missing @end
@interface Counter: NSObject - (void)increment; @end Every @interface block needs its own @end. Omitting it causes the compiler to keep parsing everything after it as if still inside the interface, usually producing a confusing error much later in the file rather than right at the missing line.
@implementation Counter - (void)decrement { self.value = self.value - 1; } @end
// In Counter.h: - (void)decrement; // In Counter.m: @implementation Counter - (void)decrement { self.value = self.value - 1; } @end
Implementing decrement without ever declaring it in @interface means other files that only see Counter.h have no way to know decrement exists — calling it from outside code (even though it technically works at runtime) triggers a compiler warning about an unrecognized selector, because the compiler is checking your call against the public interface, not the hidden implementation.
@interface Counter : NSObject @property (nonatomic, assign) NSInteger value; - (void)increment; @end @implementation Counter // increment never actually implemented
@implementation Counter - (void)increment { self.value = self.value + 1; } @end
Declaring a method in @interface without ever providing its body in @implementation breaks the promise the interface made. The compiler will warn that the method is declared but not implemented, and calling it at runtime, if it somehow got past that warning, would crash with an unrecognized-selector error.
What does an @interface block actually contain? A. The full executable code for every method B. Only property and method declarations — the class's public contract, with no method bodies C. Just the class name, nothing else D. Only private helper methods
B. Only property and method declarations — the class's public contract, with no method bodies — @interface declares what a class exposes — its properties and method signatures — without providing any method bodies. Those bodies belong in the matching @implementation block.
Which C concept is @interface most directly analogous to? A. A C source (.c) file's function definitions B. A C header (.h) file's function prototypes and declarations C. The C preprocessor's #define directive D. The linker stage of the compilation pipeline
B. A C header (.h) file's function prototypes and declarations — Just as a C header declares a function's signature without its body, @interface declares a class's method signatures and properties without providing any implementation code.
A method is written inside @implementation but was never declared in the matching @interface. What happens? A. It's a compiler error and the file won't build B. It compiles and works internally, but other files that only see the header don't know it exists and calling it triggers a warning C. It automatically gets added to the interface at compile time D. Objective-C doesn't allow methods that aren't declared in @interface
B. It compiles and works internally, but other files that only see the header don't know it exists and calling it triggers a warning — Objective-C allows this — it behaves like an informally private method, callable from within the same implementation file but unknown (and unsafe to call without a warning) from any file that only imports the header.
Why does splitting a class into @interface and @implementation provide compile-time decoupling? A. Because it makes the code run faster at runtime B. Because other files only need to compile against the header's declared shapes, not the full implementation source, letting the linker connect everything later C. Because @implementation files are optional D. Because it removes the need for a linker entirely
B. Because other files only need to compile against the header's declared shapes, not the full implementation source, letting the linker connect everything later — A file that #imports a class's header only needs to know its declared shapes to compile; it doesn't need the .m source present or compilable, exactly like C's header/source split — the linker resolves the actual connections at build time.
// Counter.h
#import <Foundation/Foundation.h>
@interface Counter : NSObject
@property (nonatomic, assign) NSInteger value;
- (void)increment;
- (void)resetToZero;
@end// Counter.m
#import "Counter.h"
@implementation Counter
- (void)increment {
self.value = self.value + 1;
}
- (void)resetToZero {
self.value = 0;
}
@end// Counter.m (continued)
@implementation Counter
- (void)increment {
[self logChange];
self.value = self.value + 1;
}
- (void)logChange {
NSLog(@"Counter is about to change from %ld", (long)self.value);
}
@end