Classes and Objects

This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, instancetype, Clang modules, lightweight generics, nullability and @available — as published at Apple Developer Documentation and the Clang Objective-C specifications, which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

An Objective-C class is declared in two halves: an @interface that says what the class offers, conventionally in a .h file, and an @implementation that says how, in the matching .m. Clients import the header and see only the interface. This split is not merely stylistic — it is the language’s encapsulation mechanism, since there are no access modifiers on methods.

Anatomy of an Objective-C class: which declarations live in the .h interface and which in the .m implementation, including the class extension

@interface and @implementation

// Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject

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

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

+ (instancetype)personWithName:(NSString *)name;

@end
// Person.m
#import "Person.h"

@implementation Person

- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
    self = [super init];
    if (self) {
        _name = [name copy];
        _age  = age;
    }
    return self;
}

- (NSString *)greeting {
    return [NSString stringWithFormat:@"Hello, %@!", self.name];
}

+ (instancetype)personWithName:(NSString *)name {
    return [[self alloc] initWithName:name age:0];
}

@end

The first line of the interface, @interface Person : NSObject, names the class and its superclass. Almost every class descends — directly or transitively — from NSObject.

A method that appears only in the @implementation is effectively private: it exists and can be called, but no client that imports the header knows its name. The compiler no longer requires such methods to be declared ahead of use, so a "private methods" section in the header is an obsolete habit.

Instance Variables

Instance variables (ivars) hold an object’s state. In modern code you rarely declare them by hand — a @property synthesises one automatically — but the syntax matters for reading existing code and for the cases where a property would be misleading.

@interface Person : NSObject {
    @private
    NSMutableArray *_history;      // visible only to Person
    @protected
    NSInteger _internalVersion;    // visible to Person and its subclasses (the default)
    @public
    NSInteger publicCounter;       // visible to anyone: person->publicCounter
    @package
    NSString *_frameworkOnly;      // visible within the same framework/image
}
@end
Directive Scope

@private

The declaring class only.

@protected

The declaring class and its subclasses. This is the default.

@public

Anyone, via the operator. Avoid — it defeats encapsulation entirely.

@package

Anywhere within the same framework or executable image; @private elsewhere.

Declare ivars in the @implementation block (or in a class extension) rather than the header whenever possible, so they do not appear in the public interface at all:

// Person.m
@implementation Person {
    NSMutableArray *_history;      // genuinely private: not in the header
}

The leading underscore is a strong convention: it marks the name as the backing storage for a property and keeps ivar access visually distinct from the self.name property access that should usually be preferred.

@class Forward Declarations

@class Foo; tells the compiler that Foo is a class name without pulling in its header. Use it in headers whenever only the name is needed — a property type, a parameter type, a return type:

// Order.h
@class Customer;                    // no #import needed here

@interface Order : NSObject
@property (nonatomic, strong) Customer *customer;
@end

// Order.m
#import "Customer.h"                // the real declaration, needed to send messages

This shortens build times and, crucially, breaks circular imports: if Customer.h also refers to Order, importing both headers from each other would not compile. @protocol Foo; does the same for protocols.

Class and Instance Methods

A leading - declares an instance method, sent to an instance. A leading + declares a class method, sent to the class object itself:

- (NSString *)greeting;                       // [person greeting]
+ (instancetype)personWithName:(NSString *)n; // [Person personWithName:@"Ada"]

Inside a class method, self is the class object, not an instance — which is why [[self alloc] initWithName:…] in a factory method correctly allocates a subclass when the message is sent to a subclass. Writing [[Person alloc] …] there would hard-code the base class and break subclassing.

Class methods are used for factory ("convenience") constructors, for singletons, and for functionality with no per-instance state. They are inherited like instance methods.

Method Declaration Syntax

- (void)insertObject:(id)object atIndex:(NSUInteger)index;

Reading left to right: - (instance method), (void) (return type), then alternating keywords and arguments. The method’s name — its selector — is the concatenation of the keywords with their colons: insertObject:atIndex:. Both of these are also complete selectors:

Declaration Selector

- (NSUInteger)length;

length

- (void)setName:(NSString *)name;

setName:

- (id)objectAtIndex:(NSUInteger)i;

objectAtIndex:

- (void)a:(int)x b:(int)y c:(int)z;

a:b:c:

Because the selector carries only names and arity, there is no overloading by argument type. Two methods whose selectors are identical are the same method; distinguish them by naming them differently (initWithName: and initWithData:), which is why Objective-C selectors read so verbosely.

Methods may also be variadic, in the C sense, terminated by nil ([NSArray arrayWithObjects:a, b, nil]) — a pattern largely superseded by the @[…] literal.

Creating Objects: alloc and init

Object creation is always two steps: allocate, then initialise.

Person *p = [[Person alloc] initWithName:@"Ada" age:36];
  • +alloc (from NSObject) allocates zero-filled memory for the instance and sets its isa pointer. It does not run any of your initialisation.

  • -init… populates the instance and returns it — possibly returning a different object than it was sent to, which is why the result must always be used rather than discarded.

Never send init twice to the same object, and never use an object between alloc and init.

Designated and Convenience Initializers

A class picks exactly one designated initializer: the one that does the real work and is the only one that calls `super’s designated initializer. Every other initializer is a convenience initializer that funnels into it.

// Person.h
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithName:(NSString *)name;      // convenience
- (instancetype)init NS_UNAVAILABLE;                // this class requires a name
// Person.m
// Designated: calls super's designated initializer, then sets up all state.
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
    self = [super init];
    if (self) {
        _name = [name copy];
        _age  = age;
    }
    return self;
}

// Convenience: calls *this* class's designated initializer, not super's.
- (instancetype)initWithName:(NSString *)name {
    return [self initWithName:name age:0];
}

The self = [super init]; if (self) { … } return self; shape is not ceremony. [super init] may return nil (initialisation failed) or a substituted object (as class clusters do), and assigning the result is what keeps the rest of the method operating on the right instance.

Marking the designated initializer with NS_DESIGNATED_INITIALIZER turns the rule into a compiler-checked one: Clang then warns if a subclass’s designated initializer fails to chain to it, or if a convenience initializer calls super instead of self. NS_UNAVAILABLE removes an inherited initializer that would leave the object in an invalid state.

+new is exactly [[Class alloc] init]. It is fine for classes with a meaningful plain init, but cannot pass arguments, so most classes use alloc/init… or a factory method instead.

Initialisers may fail by returning nil — but release nothing and do nothing else first:

- (instancetype)initWithURL:(NSURL *)url {
    self = [super init];
    if (self) {
        if (!url.isFileURL) {
            return nil;             // under ARC, self is cleaned up automatically
        }
        _url = url;
    }
    return self;
}

self and super

self is the receiver of the current message — an implicit parameter of every method, along with _cmd, the selector being executed. super is not an object: it is a directive meaning "start the method lookup at my superclass instead of at my own class", with self unchanged as the receiver.

- (NSString *)description {
    return [NSString stringWithFormat:@"%@ (name=%@)",
            [super description],     // NSObject's implementation, still on this instance
            self.name];
}

Inside an initializer, prefer direct ivar assignment (_name = …) to property access (self.name = …): a subclass may have overridden the setter, and calling an override on a half-initialised object is a well-known hazard. The same applies in dealloc.

dealloc

dealloc is sent when an object’s last strong reference goes away. Under ARC you implement it only to release resources ARC does not know about, and you never call [super dealloc] — the compiler inserts it.

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [_connection invalidate];
    CFRelease(_cfObject);          // CoreFoundation object: still manual
    // no [super dealloc] under ARC
}

Never resurrect self from dealloc, and do not rely on it running at a predictable moment. Memory management proper is covered in Automatic Reference Counting.

NSObject, the isa Pointer and description

NSObject is the root class of virtually every Objective-C class hierarchy (the only common alternative is NSProxy). It supplies allocation, lifetime, equality, hashing, introspection and message forwarding — the machinery that makes an instance a first-class participant in the run time rather than a bare struct.

Every instance begins with an isa pointer to its class object; the class object holds the method list, the ivar layout and a pointer to the superclass. That single indirection is what objc_msgSend follows on every message send, and what makes class introspection possible at run time:

Person *p = [[Person alloc] initWithName:@"Ada" age:36];

[p class];                              // Person -- the class object
[p isKindOfClass:[NSObject class]];     // YES  -- this class or any ancestor
[p isMemberOfClass:[Person class]];     // YES  -- exactly this class
[p respondsToSelector:@selector(greeting)];   // YES
NSStringFromClass([p class]);           // @"Person"

The details of isa, metaclasses and the dispatch path are in The Objective-C Runtime.

description

NSObject’s `description returns <ClassName: 0xaddress>. Override it to make %@, NSLog and the debugger’s po useful:

- (NSString *)description {
    return [NSString stringWithFormat:@"<%@: %p, name=%@, age=%ld>",
            NSStringFromClass([self class]), self, self.name, (long)self.age];
}

A companion method, debugDescription, is what po prefers; by default it calls description. Override it separately when the debugger should show more detail than a log line.

See Also