Basic Syntax and Types

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.

Objective-C’s lexical layer is C’s: the same tokens, the same comments, the same identifier rules, the same preprocessor. What the language adds on top is a set of @-prefixed directives and a handful of typedefs and object types that the Foundation framework and the run time depend on.

Tokens, Comments and Identifiers

Comments are C’s: // to end of line and /* … */ for blocks. Identifiers are case-sensitive, start with a letter or underscore, and by convention are camelCase for variables and methods, UpperCamelCase for types.

Because Objective-C has no namespaces, class names are conventionally prefixed with two or three letters identifying the framework or project — NS (Foundation/AppKit, historically NeXTSTEP), UI (UIKit), CG (Core Graphics). Prefixes are a convention enforced only by discipline; see Coding Conventions and Style.

Every C keyword is a keyword here too. Objective-C’s own additions are spelled with a leading @ precisely so they cannot collide with an existing C identifier. For C’s own token grammar, identifier rules, comment forms and the full keyword list, see C Reference — Lexical Structure and Style.

The @ Directives

Directive Purpose

@interface / @implementation / @end

Declare and define a class or category.

@protocol

Declare a protocol (or, in an expression, produce a Protocol *).

@property

Declare a property.

@synthesize / @dynamic

Control accessor generation for a property.

@class

Forward-declare a class name without importing its header.

@private / @protected / @public / @package

Instance-variable visibility.

@required / @optional

Mark the following protocol methods as mandatory or not.

@selector(…​)

Produce a SEL from a method name at compile time.

@encode(…​)

Produce the run-time type-encoding string for a type.

@"…", @42, @YES, @[…], @{…}, @(expr)

Object literals and boxing.

@try / @catch / @finally / @throw

Exception handling.

@synchronized(obj)

Recursive mutex around a block, keyed on an object.

@autoreleasepool

Bound the lifetime of autoreleased temporaries.

@available(…​)

Run-time OS-version check.

@import

Import a Clang module (rather than textually including a header).

@compatibility_alias

Give an existing class a second name.

@defs, @compatibility_alias

Legacy/rare; @defs is removed from the modern run time.

The C Scalar Types, in Brief

Objective-C inherits C’s types unchanged: char, short, int, long, long long and their unsigned variants; float, double, long double; _Bool; and the <stdint.h> fixed-width types (int32_t, uint64_t). Sizes are implementation-defined in exactly the C way — on Apple’s 64-bit platforms int is 32 bits, long and pointers are 64 bits.

Pointers, arrays, struct, union, enum and function pointers all behave as in C. That material is C’s, not Objective-C’s, and is summarised here only far enough to read the examples in this section; the Objective-C-specific consequences are what the pages below foreground. For the C detail in depth, go to the C Reference:

The practical advice is simple and it is Apple’s: in Objective-C code, prefer the Foundation-defined typedefs below to raw C types when the value crosses an API boundary, because they are defined to widen correctly on 64-bit platforms.

Objective-C’s Own Types

BOOL, YES and NO

BOOL is Objective-C’s Boolean. Historically it was signed char with YES as 1 and NO as 0; on 64-bit Apple platforms it is the C _Bool. Two consequences matter:

BOOL flag = YES;
if (flag) { /* … */ }

// Do NOT compare against YES: on a signed-char BOOL any non-zero value is "true",
// but only the exact value 1 equals YES.
if (flag == YES) { /* fragile */ }
if (flag)        { /* correct */ }

// A pointer is not a BOOL, but it is usable as a truth value, as in C:
NSString *s = nil;
if (s) { /* not reached: nil is 0 */ }

Never store a value wider than a BOOL into one — (BOOL)256 truncates to NO under the signed char representation.

NSInteger, NSUInteger and CGFloat

These are width-adaptive typedefs: 32 bits on a 32-bit platform, 64 bits on a 64-bit one. Use them for sizes, counts, indexes and coordinates in API signatures.

NSUInteger count  = [array count];    // counts and indexes are NSUInteger
NSInteger  offset = -3;               // signed positions
CGFloat    width  = 42.5;             // double on 64-bit, float on 32-bit

// Format specifiers must match the width. The portable spelling casts explicitly:
NSLog(@"count = %lu", (unsigned long)count);
NSLog(@"offset = %ld", (long)offset);

Mismatched NSLog/printf specifiers are the single most common warning in Objective-C code; -Wall -Wextra catches them.

NSNotFound (an NSInteger equal to NSIntegerMax) is the conventional "no such index" sentinel returned by searches such as indexOfObject:. Test for it explicitly — it is not -1.

id, Class, SEL and IMP

These four are the run time’s own vocabulary.

Type Meaning

id

A pointer to any Objective-C object. Already a pointer — write id obj, never id *obj. Any message may be sent to an id without a compile-time check, provided some visible @interface declares it.

Class

A pointer to a class object. [obj class] returns it; [NSString class] is the class object for NSString. Classes are themselves objects and can receive messages.

SEL

A selector — the run time’s interned name for a method, e.g. @selector(setName:). Selectors encode the name and argument count, not the types or the class.

IMP

A function pointer to a method implementation: id (*IMP)(id self, SEL _cmd, …). Obtainable with methodForSelector: to bypass the dispatch machinery in hot loops.

id        anything  = @"a string";        // any object
Class     cls       = [anything class];   // NSString (in practice __NSCFConstantString)
SEL       sel       = @selector(length);
IMP       imp       = [anything methodForSelector:sel];

NSUInteger len = ((NSUInteger (*)(id, SEL))imp)(anything, sel);   // direct call, no dispatch

Class and SEL are covered in depth in Messaging and Selectors and The Objective-C Runtime.

instancetype

instancetype is a contextual keyword meaning "an instance of the receiving class". It exists so that initializers and factory methods return a correctly typed result in subclasses:

@interface Shape : NSObject
+ (instancetype)shape;          // in Circle, this returns Circle *
+ (id)legacyShape;              // returns id -- no type checking at the call site
@end

Circle *c = [Circle shape];         // type-checked: the compiler knows this is a Circle *
Circle *d = [Circle legacyShape];   // compiles, but any mistake goes unnoticed

Always use instancetype — never id — as the return type of init… and of convenience constructors.

nil, Nil, NULL and NSNull

Four spellings of "nothing", each with its own domain. Confusing them is a classic beginner’s error:

Spelling Type Use for

nil

id

A null object pointer.

Nil

Class

A null class pointer.

NULL

void *

A null C pointer (including NSError ** out-parameters you do not want).

NSNull

object

A real, non-null object standing in for "no value" inside a collection.

The last one exists because Foundation collections cannot store nil — inserting nil into an NSArray raises an exception, and nil also terminates the legacy arrayWithObjects: argument list. [NSNull null] is a singleton placeholder:

NSArray *withHole = @[ @"a", [NSNull null], @"c" ];    // a three-element array

id value = withHole[1];
if (value == [NSNull null]) {
    NSLog(@"explicitly no value here");
}

The defining property of nil, and one Objective-C leans on constantly, is that sending a message to nil is legal and does nothing, returning zero/nil/a zeroed struct. That is why Objective-C code contains far fewer null guards than C++ or Java code; see Messaging and Selectors.

Variables, Constants and Literals

Declaration syntax, storage classes, the C literal forms and initializer rules are C’s; see C Reference — Constants, Enumerations and Initialization and Storage Duration, Scope and Linkage. What follows is how the C forms sit alongside Objective-C’s object literals.

// C literals work unchanged
int      i   = 42;
double   d   = 3.14159;
char     c   = 'x';
const char *cstr = "a C string";      // NUL-terminated bytes, NOT an NSString

// Objective-C object literals
NSString     *s    = @"an NSString";          // compile-time constant string object
NSNumber     *n    = @42;                     // boxed int
NSNumber     *b    = @YES;                    // boxed BOOL
NSNumber     *e    = @(i * 2);                // boxed expression
NSArray      *arr  = @[ @"a", @"b" ];         // NSArray literal
NSDictionary *dict = @{ @"key": @"value" };   // NSDictionary literal

Constants come in two flavours, and the object-typed one is preferred in headers because it has a single address that can be compared with ==:

// In the header -- declared, not defined:
extern NSString * const MyLibraryErrorDomain;
extern const NSTimeInterval MyLibraryDefaultTimeout;

// In the implementation -- defined once:
NSString * const MyLibraryErrorDomain = @"com.example.MyLibrary";
const NSTimeInterval MyLibraryDefaultTimeout = 30.0;

Note the placement of const: NSString * const name is a constant pointer to an (immutable) string, which is what you want. const NSString *name is a pointer to a const NSString and produces awkward warnings at every use.

Prefer these to #define for values: they are typed, visible to the debugger, and do not leak through headers as text.

typedef, NS_ENUM and NS_OPTIONS

Plain C typedef and enum are available, but Foundation’s two macros are what Objective-C APIs actually use. They give the enumeration a fixed underlying type, which both keeps the ABI stable and lets Swift import the type properly.

// A set of mutually exclusive values.
typedef NS_ENUM(NSInteger, MyPlayerState) {
    MyPlayerStateStopped = 0,
    MyPlayerStatePlaying,
    MyPlayerStatePaused
};

// A bit-mask of independently combinable flags.
typedef NS_OPTIONS(NSUInteger, MyLayoutOptions) {
    MyLayoutOptionsNone       = 0,
    MyLayoutOptionsAlignLeft  = 1 << 0,
    MyLayoutOptionsAlignRight = 1 << 1,
    MyLayoutOptionsWrap       = 1 << 2
};

MyLayoutOptions opts = MyLayoutOptionsAlignLeft | MyLayoutOptionsWrap;
if (opts & MyLayoutOptionsWrap) { /* … */ }

Two conventions carry real weight. Name each constant with the type name as its prefix (MyPlayerStatePaused, not Paused) — there being no namespaces, this is what keeps the global identifier space navigable. And switch over an NS_ENUM without a default: clause: the compiler will then warn about any case you forget to handle, which is the main practical benefit of using the macro at all.

NS_ENUM imports into Swift as an enum, NS_OPTIONS as an OptionSet. Two related macros, NS_STRING_ENUM and NS_ERROR_ENUM, give string constants and error codes the same treatment — see Errors and Exceptions.

Casts and Conversions

The usual arithmetic conversions, integer promotion and C’s explicit cast syntax all apply unchanged — see C Reference — Basic Types and Values for the conversion ladder in full. Two Objective-C-specific notes:

  • Object casts are unchecked. (NSString *)someObject tells the compiler to stop complaining; it performs no run-time verification. Guard with isKindOfClass: when the type is genuinely uncertain.

  • Casting between an Objective-C object and a CoreFoundation type requires a bridging cast under ARC (bridge, bridge_retained, __bridge_transfer), because ownership must be stated explicitly. See Automatic Reference Counting.

id maybe = [dict objectForKey:@"name"];

if ([maybe isKindOfClass:[NSString class]]) {
    NSString *name = (NSString *)maybe;     // now a safe cast
    NSLog(@"%@", [name uppercaseString]);
}

Lightweight generics (NSArray<NSString *> *) narrow these casts at compile time without changing the run-time representation — see Lightweight Generics and Nullability.

See Also