C with a Smalltalk message layer bolted on, and forty years of framework built on top of it. This sheet assumes you
know what a vtable, a reference count and a trampoline are, and spends its space on what is actually peculiar: dispatch
through objc_msgSend, the ownership contract ARC infers from method names, the class clusters and
two-stage initialisation that make Foundation look stranger than it is, toll-free bridging, and what still bites in a
mixed Swift codebase. Cards 1–34 are the guide; the rest is a filterable index of directives, runtime
functions, and the Foundation and AppKit/UIKit classes worth knowing. Press / to jump to the filter box;
hover any name for its signature.
A strict superset of C with one idea added: Smalltalk-style message passing, resolved by a
runtime library rather than by the linker. Every C construct still works and means what it always meant;
everything Objective-C adds is either a new punctuation mark (@, [ ],
^) or a call into libobjc.
| Fact | Consequence |
|---|---|
| Brad Cox & Tom Love, 1984; licensed by NeXT 1988; Apple by acquisition, 1997 | The framework
API is a 1990s NeXTSTEP design that has been kept source-compatible for thirty years. That explains the
NS prefix and most of the odd corners. |
| Method calls are messages looked up by name at send time | No vtables, no devirtualisation, no inlining across a send. You can add, swap and forward methods while the program runs. |
| Classes are themselves objects | + (class) methods are just messages to the class
object; metaclasses close the loop. |
Single inheritance, from NSObject | Protocols carry the multiple-inheritance load; categories carry the mix-in load. |
| No namespaces, no operator overloading, no templates | Two-and-three-letter class prefixes
(NS, UI, CA, AV) are the namespace mechanism. |
The legacy (fragile) runtime was 32-bit macOS: subclass ivar offsets were compiled in, so adding an
ivar to a superclass broke every subclass binary. The modern (non-fragile) runtime — everything
64-bit, iOS from the start — fixes ivar offsets at load time. GNUstep’s libobjc2 is
the third; nothing here is written for it.
retain or declares
ivars in the @interface braces, it predates 2011 and is being quoted for archaeology, not style.Declared properties, fast enumeration, @optional protocol methods, garbage collection
(dead — deprecated 10.8, removed 10.12), and the non-fragile ivar layout. ARC arrived in 2011,
literals and subscripting in 2012, nullability and lightweight generics in 2015. Nothing substantial has been
added to the language since — the investment went to Swift.
Because AppKit, UIKit, Foundation and Core Data are still Objective-C underneath, every Swift crash report
in those frameworks lands in Objective-C frames, and the runtime tricks that make KVO, Core Data faulting,
NSUndoManager and every mocking library work have no Swift equivalent.
| Flag | Effect |
|---|---|
-fobjc-arc | Automatic Reference Counting. Per file, so a mixed target is legal. |
-fno-objc-arc | Turn ARC off for one file that still hand-rolls retain/release. |
-fobjc-weak | __weak without full ARC (MRR files that still want zeroing weak refs). |
-fobjc-arc-exceptions | Make ARC emit cleanup on exception unwind. Off by default in ObjC — an @throw past ARC code leaks. |
-fmodules | Enables @import; also makes #import <Foundation/Foundation.h> compile as a module. |
-fobjc-exceptions | Allow @try/@throw (on by default for ObjC). |
-Wobjc-missing-super-calls | Honours NS_REQUIRES_SUPER. Turn it on. |
-fobjc-abi-version=2 | Modern runtime; implied on all Apple 64-bit targets. |
#import is #include with an implicit include-guard, keyed on the resolved file.
It is idempotent by construction, so Cocoa headers carry no guards. @import Foundation; goes
further: it loads a precompiled module, is order-independent, and links the framework for you.
@class in the
.h and #import in the .m keeps compile times sane and breaks the circular
imports that two classes referring to each other will otherwise create. @protocol Foo; does the same
for protocols.A framework is a versioned bundle: headers, a dylib, resources, a module map. -framework Foo
links it; -F dir adds a search path. The umbrella header (Foo/Foo.h) imports the rest.
| Type | Is |
|---|---|
id | struct objc_object * — any object. No compile-time checking of
messages sent to it; dot syntax is not allowed on it. |
instancetype | A contextual keyword meaning “this class”. The correct return type for every initialiser and factory method. |
Class | A class object (struct objc_class *). Nil is its
null. |
SEL | A uniqued selector — effectively an interned string, not a function pointer.
Compare with ==, build with @selector() or NSSelectorFromString(). |
IMP | id (*)(id, SEL, ...) — the actual function that implements a
method. |
Method, Ivar, Protocol *, objc_property_t | Opaque runtime handles. |
BOOL | bool on arm64 and modern 64-bit; signed char
historically. Values YES/NO. |
nil. [array addObject:nil] raises
NSInvalidArgumentException, and @[a, b, c] throws if any element is nil. Use
[NSNull null] as the placeholder — and remember it is truthy, so
if (obj) does not screen it out. NSDictionary uses
setObject:forKey: vs setValue:forKey: differently: the latter removes the key
when the value is nil.NSNotFound is not -1. It is NSIntegerMax. Testing
if (idx >= 0) after indexOfObject: is always true and is a classic bug; test
if (idx != NSNotFound).- (instancetype)initWithName:(NSString *)name andAge:(NSInteger)age;
The selector is the concatenation of the keywords with their colons. initWithName:andAge:
and initWithName: are unrelated methods. Arity is fixed by the number of colons.
| Placement | Verdict |
|---|---|
@implementation Foo { ... } | Right. Private, invisible in the header, no fragile-base-class issue. |
Class extension @interface Foo () { ... } | Fine; equivalent. |
@interface Foo : NSObject { ... } | Legacy. Publishes your layout to every client. |
Auto-synthesised _name | What you actually get from @property. Prefer it. |
performSelector:. Prefix genuinely internal methods to avoid collisions, and never assume
inaccessibility is a security boundary.Every [receiver selector] compiles to a call to objc_msgSend(receiver, @selector(...), ...)
(or a sibling for struct/float returns on some ABIs). That function walks the receiver’s class for a
method matching the selector, consults a per-class cache first, and jumps to the found IMP with
the original arguments and stack frame intact. Failing that it climbs the superclass chain, then enters the
forwarding machinery.
Every method really has the signature ret method(id self, SEL _cmd, ...). Inside any method
body self and _cmd are in scope; _cmd is genuinely useful for logging
and for generic implementations installed at runtime.
Legal, and defined. A message to nil returns zero of the return type: nil
for objects, 0 for integers, 0.0 for floats, and a zero-filled struct. No exception,
no crash.
super is a compiler directive, not a pointer. [super foo] starts the method
search at the superclass but still passes self as the receiver. So a
[super description] inside an override reaches the parent implementation, while
[self description] from the parent’s code reaches the override.
IMP only after
Instruments says to. __attribute__((objc_direct)) on a method (or
objc_direct_members on a category) compiles it as a plain C function: no dispatch, no swizzling,
no overriding, invisible to the runtime.An unhandled selector is not an immediate crash. The runtime offers three escape hatches, in order, and only
then raises. Each one is a supported extension point, and everything from NSProxy-based mocks to
Core Data faulting lives here.
| Stage | Hook | Cost |
|---|---|---|
| 1. Dynamic resolution | +resolveInstanceMethod: / +resolveClassMethod: —
add an IMP with class_addMethod and return YES; the send is retried and permanently fast afterwards. | Once |
| 2. Fast forwarding | -forwardingTargetForSelector: — return another object and the
message is re-sent to it. Cheap: no NSInvocation is built. | Low |
| 3. Full forwarding | -methodSignatureForSelector: (must return non-nil) then
-forwardInvocation:, which receives a reified NSInvocation you can inspect, mutate,
store, or send anywhere. | High |
| 4. Failure | -doesNotRecognizeSelector: raises
NSInvalidArgumentException: unrecognized selector sent to instance 0x… | — |
respondsToSelector:. A proxy that forwards
-length will still answer NO to [proxy respondsToSelector:@selector(length)] unless you
override that too — and Cocoa asks that question constantly before it sends optional delegate messages.
Override respondsToSelector: and conformsToProtocol: alongside.The other root class. It implements almost nothing but forwardInvocation: and
methodSignatureForSelector:, which are abstract — you must supply both. Subclass it, rather
than NSObject, when you want every message forwarded, including the ones
NSObject would otherwise answer itself.
A property is a declaration of accessors, not of storage. @property NSString *name;
declares -name and -setName:; the compiler then auto-synthesises an ivar
_name and both bodies unless you supply them.
| Attribute | Meaning |
|---|---|
strong (default for objects) | Retains the new value, releases the old. Was retain. |
weak | Does not retain; zeroed automatically when the target deallocates. Never for delegates’ storage duration guarantees, always for the delegate itself. |
copy | Stores [value copy]. Mandatory for NSString, NSArray, NSDictionary and blocks. |
assign (default for scalars) | Raw store. On an object under ARC this is __unsafe_unretained: a dangling pointer waiting to happen. |
unsafe_unretained | Explicit non-zeroing weak reference. For objects that do not support weak refs (a handful of classes) or for performance. |
atomic (default) / nonatomic | Atomic wraps the accessors in a spinlock so a read never sees a half-assigned pointer. Everyone writes nonatomic because atomic costs and buys almost nothing. |
readonly / readwrite | Getter only / both (default). Redeclare readwrite in the class extension for the internal setter. |
getter= / setter= | Rename the accessors. getter=isHidden is the Cocoa convention for BOOLs. |
nullable / nonnull / null_resettable | Nullability, which Swift imports as optionality. |
class | Class-level property. You must write both accessors; there is no ivar to synthesise. |
direct | Non-dispatched accessors (clang extension). Fast, unswizzlable, invisible to KVO. |
atomic is not thread safety. It guarantees one accessor call is not torn.
It says nothing about a read-modify-write across two calls, nor about the object’s own invariants:
self.count = self.count + 1 races just as freely on an atomic property.strong on an NSString or NSArray is a bug. The
caller can hand you an NSMutableString, keep its own reference, and mutate your state behind your
back. copy on an already-immutable object is just a retain, so it costs nothing when it is not
needed.Auto-synthesis stops if you implement both accessors of a readwrite property (or the getter of a readonly
one), or if the property comes from a protocol — in those cases the ivar is not created and you must
declare it or @synthesize it yourself.
init or dealloc. A subclass override
would run against a half-built or half-torn-down object. Assign the ivar directly: _title = [t copy];.Objective-C has always been reference counted. ARC did not change the semantics; it made the compiler write
the retain/release calls, and it inferred the rules from a naming convention that
predates it by twenty years. That convention is still the contract, and you must obey it in your own method
names or ARC will get the counts wrong.
| Method name begins with | Returns |
|---|---|
alloc, new, copy, mutableCopy (and init) | +1 — the caller owns it |
| anything else | +0 — autoreleased or otherwise not owned |
newToken or copyStrategy that returns an
autoreleased object will be over-released by ARC and by the static analyser. Either rename it, or annotate:
NS_RETURNS_NOT_RETAINED / NS_RETURNS_RETAINED. This is why Cocoa has
-description and not -newDescription.You own what you alloc/new/copy/retain; you must
release or autorelease exactly that many times; you must never release what you do not
own. Under ARC all four messages are compile errors.
Every thread the run loop owns drains a pool per event cycle. A thread you create yourself has none until you
make one — a tight loop of +0 factory calls on a bare NSThread or GCD block will
grow without bound until it ends. @autoreleasepool is a scope, not an object; the old
NSAutoreleasePool is forbidden under ARC.
ARC is reference counting, not tracing: cycles leak. The three that account for nearly all of them:
dealloc runs on whatever thread released the last reference. It may not be the
main thread; do not touch UI in it. Under ARC you do not release ivars — the compiler does — but you
must still tear down observers, timers, KVO registrations and CF objects.Allocation and initialisation are separate messages, and both can be overridden. +alloc zeroes
the instance and sets its isa; -init… establishes the invariants and
may return a different object than it was sent to — which is why you always assign the result.
| Rule | Why |
|---|---|
A class has one (rarely two) designated initialiser; every other initialiser calls it via self. | One place establishes the invariants. |
The designated initialiser calls the superclass’s designated initialiser via super. | Otherwise the inherited state is not set up. |
Mark it NS_DESIGNATED_INITIALIZER. | The compiler then enforces both rules and warns on violations. |
Mark inherited initialisers you do not support NS_UNAVAILABLE. | Stops callers reaching -init on a class that needs arguments. |
Always self = [super init]. | The superclass may substitute a different instance (class clusters do this constantly). |
NSString, NSArray, NSDictionary, NSNumber and
NSData are abstract public faces over private concrete subclasses.
[NSString alloc] hands you a placeholder; the initialiser throws it away and returns something
whose real class is __NSCFString or NSTaggedPointerString.
isKindOfClass:[NSString class]-driven logic that assumes the concrete class.Small NSNumbers, short NSStrings and NSDates are not heap objects at
all: the value is packed into the pointer, with a low-bit tag telling the runtime to decode rather than
dereference. Consequences: retain/release are no-ops on them,
object_getClass() is not *(Class *)obj, they never appear in the heap tools, and
their addresses look like nonsense in the debugger. Everything through the API still behaves normally.
+load | +initialize | |
|---|---|---|
| When | Image load, before main | First message to the class, ever |
| Inherited? | No — every implementation runs | Yes — a subclass without one runs the parent’s, again |
| Safe to | Almost nothing; other classes may not be loaded | Most things; it runs lazily and thread-safely |
Guard +initialize with if (self == [MyClass class]), or a subclass will re-run your
setup. +load is where swizzling is traditionally installed, and is a measurable part of app launch
time — use it sparingly.
If [a isEqual:b] then a.hash == b.hash. The converse need not hold. Equality must
be reflexive, symmetric and transitive, and hash must not change while the object is in a collection
— which is the real reason dictionary keys are copied.
hash whenever you override isEqual:. Forget it and
your objects behave correctly with ==, correctly in arrays, and wrongly in every
NSSet and NSDictionary — two equal objects land in different buckets and both
survive de-duplication. It is the single most common silent bug in hand-written value types.isKindOfClass: in isEqual: makes equality asymmetric across a
subclass boundary (parent equals child, child does not equal parent). isMemberOfClass: or a
[self class] == [other class] test is the symmetric choice. Pick deliberately.An immutable class implements copyWithZone: as return self; (a retain) —
this is why copy on an NSString is nearly free. A class with a mutable variant
implements NSMutableCopying too; -copy then returns the immutable flavour and
-mutableCopy the mutable one.
[arrayOfMutableStrings copy] gives you a
new array holding the same mutable strings. For depth: initWithArray:copyItems:YES (one level
deep), or archive and unarchive, which is the only genuinely deep copy Foundation offers.-description backs %@ and po; -debugDescription backs
po when it exists and defaults to description. Overriding description on
your model objects is the highest-return five minutes in this language.
respondsToSelector: before an @optional method.
conformsToProtocol: only says the class declared the protocol; it says nothing about which
optional members exist. This is exactly why every Cocoa delegate call site is wrapped in a
respondsToSelector: check.There is no multiple inheritance, no abstract class keyword, and no generics beyond the lightweight kind.
Protocols are therefore the only way to express “these unrelated types share an interface”, and Cocoa
leans on them for delegation, data sources, archiving (NSCoding), copying, comparison and
collection behaviour (NSFastEnumeration).
Note the conventions the whole framework follows: the first argument is always the sender, the selector reads
as a sentence about what happened, and will…/did…/should…
distinguish notice-before, notice-after and permission-asking.
@property. That imposes accessors on the adopter but
synthesises nothing — the adopting class must declare a matching property or write the accessors itself.A category adds methods to an existing class — including one whose source you do not have. The methods are installed into the real class at load time, so every instance in the process gains them, including ones created by Apple’s code.
| Form | Named | Can add ivars | Compiler checks implementation |
|---|---|---|---|
Category @interface NSString (Trim) | Yes | No | Warns if missing |
Class extension @interface Foo () | No | Yes | Must be in the class’s own @implementation |
The extension is the anonymous category, and it is a genuinely different mechanism: it is compiled as part of
the class, so it may add ivars and properties and may promote a readonly property to
readwrite. It only works in the class’s own compilation unit — which is precisely why it
is the private-interface idiom.
gc_trimmed, not trimmed.super to
reach the original, and the outcome across multiple categories is undefined. Subclass, or swizzle deliberately.Associations are released automatically when the object deallocates. The key is compared by pointer, so use the address of a static, never a string literal.
class_addMethod first
or you will silently patch the superclass for every one of its subclasses.Blocks are closures with a C ABI: a stack-allocated struct holding a function pointer, a descriptor, and the
captured variables. They exist at the C level (clang extension, also usable from plain C), which is why the
declaration syntax is C’s function-pointer syntax with ^ for *.
void (^h)(NSError *) is
“h is a block taking NSError * and returning void”. The
typedef exists because the nested form — a method returning a block that takes a block —
is unreadable.| Variable | Captured as |
|---|---|
| Local scalar | const copy at block-creation time. Assigning to it inside is a compile error. |
| Local object pointer | Retained (ARC), const copy of the pointer. |
__block variable | Shared box, promoted to the heap with the block. Readable and writable from both sides. |
Ivar (_foo) | Captures self, not the ivar. The classic accidental cycle. |
static / global | Not captured — referenced directly. |
__strong variable, a copy property, or pass it to a method that
retains it — but not when you stash it in a C struct or an array of raw pointers, and not for
blocks assigned into __weak or __unsafe_unretained variables. Non-ARC:
Block_copy() / Block_release().weakSelf. A block passed to
dispatch_async, enumerateObjectsUsingBlock: or a one-shot completion handler is
released when it runs, so the retain of self is temporary and correct — and a
weakSelf there can let the object die mid-flight. The cycle only forms when the object itself
holds the block.[@[] mutableCopy] or
[NSMutableArray array] when you need to mutate. And @[a, b] raises
NSInvalidArgumentException the moment one of them is nil — a common crash when building an
array from optional values.These are plain selectors: implement them on your own class and it becomes subscriptable. Note that
dict[@"k"] = nil removes the key rather than throwing — unlike
setObject:forKey:.
Backed by -countByEnumeratingWithState:objects:count:, which hands out batches of raw pointers
— it is substantially faster than objectAtIndex: in a loop, and adopting the protocol makes
your own collections work with it.
NSGenericException: collection was mutated while being enumerated. Collect the changes and apply
them after, or enumerate a copy, or iterate an index range backwards when removing.NSEnumerationConcurrent is a genuine parallel-for over the collection — the block must be
thread-safe and the ordering of side effects is undefined.
NSError is for everything
that can go wrong at runtime. A file that is missing, a server that is down, a malformed document —
those are NSError. An index out of bounds, a nil inserted into an array, an unrecognised selector
— those are exceptions, and they mean the program is wrong.| Rule | Detail |
|---|---|
| The return value signals failure | NO or nil. The error pointer is only meaningful then — a method may write to it on success too. |
NULL must be accepted | Callers pass NULL when they do not care. Always if (error) *error = .... |
The parameter is __autoreleasing | Implicit for NSError **. Explicit when you build the type yourself. |
| Never over-release | The caller owns nothing; the error comes back autoreleased. |
Standard domains: NSCocoaErrorDomain, NSPOSIXErrorDomain,
NSOSStatusErrorDomain, NSMachErrorDomain, NSURLErrorDomain. Define your
own as a string constant; codes are an NS_ERROR_ENUM.
-fobjc-arc-exceptions, every object owned by a frame an exception passes through is leaked. Apple
assumes exceptions are fatal and this is a deliberate size/speed trade. So: never use exceptions for control
flow, and never assume a caught exception left the process in a usable state — framework code between
you and the @throw is not exception-safe either.A failed NSAssert raises NSInternalInconsistencyException, so it goes through the
same machinery. NSSetUncaughtExceptionHandler() installs a last-chance logger before the process
dies.
Indirect access to properties by string name. It is not reflection bolted on for convenience —
it is load-bearing infrastructure: Cocoa Bindings, KVO, Core Data, NSPredicate,
NSSortDescriptor, scripting support and nib outlet connection are all built on it.
valueForKey:@"name" resolvesIn order: -getName, -name, -isName, -_name; then the
collection accessors (-countOfName + -objectInNameAtIndex:, which synthesise an array
proxy); then, if +accessInstanceVariablesDirectly returns YES (it does by default), the ivars
_name, _isName, name, isName; then
-valueForUndefinedKey:, which raises NSUnknownKeyException.
NSStringFromSelector(@selector(name)) instead of @"name" and the compiler will at
least check the selector exists somewhere.These compose with ordinary key paths: @"employees.@avg.salary",
@"@distinctUnionOfObjects.department.name". NSPredicate and
NSExpression speak the same language, which is why a predicate string can aggregate.
NSDictionary answers valueForKey: with the value
for that key: it overrides the whole search. Hence the setValue:forKey: /
setObject:forKey: asymmetry — setValue: with a nil value calls
removeObjectForKey:.Register for change notifications on a KVC-compliant property of any object, without its cooperation.
The implementation is the language’s most audacious trick: on first observation the runtime creates a
hidden subclass NSKVONotifying_Foo, overrides the setters to bracket them with change
notifications, and swizzles the object’s isa pointer to point at it. The object’s
class silently changes underneath it, and -class is overridden to lie about it.
Setters synthesised by @property are automatically compliant. Anything else you notify by hand:
context, always
remove in dealloc, and never call removeObserver: for a registration you are not sure
you made.-[NSObject observeValueForKeyPath:…] has no
block form in Objective-C; Swift’s NSKeyValueObservation token (auto-removing on deinit)
does not bridge back. In Objective-C the practical mitigations are a small wrapper object that owns the
registration and unregisters in its own dealloc, or skipping KVO for
NSNotificationCenter or an explicit delegate.Other things KVO breaks on: properties mutated by direct ivar access (no notification), collections mutated
without mutableArrayValueForKey:, objc_direct accessors, and objects whose class you
also swizzle — the two isa tricks interact badly.
Everything the compiler emits is queryable and mutable at runtime through a plain C API. This is not a
back door; it is the documented interface that KVO, Core Data, NSCoder, every mock framework and
the debugger are built on.
@encode() and method_getTypeEncoding() speak the same compact language:
return type first, then @ (self) and : (_cmd), then the arguments.
copy in class_copy…List is literal. Those functions
return malloc’d C arrays that ARC knows nothing about — free() them. And a class you
create with objc_allocateClassPair can never gain another ivar after
objc_registerClassPair.lldb: _ivarDescription and
_methodDescription on NSObject, and _shortMethodDescription, which prints
just the class’s own methods. Debugger only — shipping a call to them is a rejection.Foundation’s value classes are immutable by default with a mutable sibling, are mostly class clusters, and are toll-free bridged to Core Foundation where a CF equivalent exists.
| Class | Notes |
|---|---|
NSString / NSMutableString | UTF-16 backing store, cluster, bridged to CFStringRef. See the strings card. |
NSNumber | Boxed scalar. isEqual: compares numeric value across types: @1 equals @1.0 equals @YES. |
NSDecimalNumber | Base-10, 38 significant digits, exact for money. Arithmetic by message (decimalNumberByAdding:), with an NSDecimalNumberBehaviors rounding policy. |
NSValue | Boxes any C struct or pointer. NSNumber is a subclass. |
NSData / NSMutableData | Byte buffer. bytes is the raw pointer; memory-mapped with NSDataReadingMappedIfSafe. |
NSDate | An absolute instant: a double of seconds since 2001-01-01 GMT. No calendar, no time zone. |
NSUUID, NSNull, NSURL | Identity, the nothing-singleton, and RFC 1808 URLs — which are also the correct type for file paths. |
NSLocale, NSMeasurement, NSUnit | Formatting culture; typed physical quantities with conversion. |
NSCalendar, always. And never build a
user-facing date string with a fixed format — set dateStyle/timeStyle and let the
locale decide; use a fixed format only for machine-readable output, and then set
locale to en_US_POSIX or the user’s 12/24-hour preference will corrupt it.NSString is a sequence of UTF-16 code units, not characters. -length counts
code units; a character outside the BMP counts two, and a family emoji built from ZWJ sequences counts a dozen.
Indices and NSRanges are in the same units.
isEqualToString: says NO. Normalise, or use
compare:options:NSDiacriticInsensitiveSearch-style comparison deliberately.| Spec | For |
|---|---|
%@ | Any object — calls -description. Also the only correct one for NSString. |
%ld / %lu + a cast | NSInteger / NSUInteger. Cast to long, or the code breaks on 32-bit. |
%zd | The cast-free alternative for NSInteger on Apple platforms. |
%d %u %f %g %s %c %% | Plain C. %s is a char *, never an NSString. |
%p | Pointer, hex. |
%1$@ %2$@ | Positional — required in localisable strings, where translation reorders the arguments. |
NSLog(str) with a non-literal str is a format-string
vulnerability exactly as in C. Write NSLog(@"%@", str). Clang’s
-Wformat-security catches it; leave it on.| Class | Semantics |
|---|---|
NSArray / NSMutableArray | Ordered, duplicates allowed, O(1) index. Backed by a circular buffer — insertion at either end is cheap. |
NSDictionary / NSMutableDictionary | Hash map. Keys are copied and must be NSCopying + hash/isEqual:. Values are retained. |
NSSet / NSMutableSet | Unordered, unique. containsObject: is O(1) — the reason to prefer it over an array for membership. |
NSCountedSet | A bag: adds a multiplicity countForObject:. |
NSOrderedSet / mutable | Unique and ordered. Not a subclass of either parent. |
NSIndexSet | A compressed set of NSUIntegers stored as ranges. What table views speak. |
NSCache | Dictionary-like, but evicts under memory pressure and is thread-safe. Does not copy keys. |
NSMapTable, NSHashTable, NSPointerArray | Configurable weak/strong, copy/no-copy, object/pointer. The way to hold weak references in a collection. |
[NSHashTable weakObjectsHashTable] or
[NSMapTable strongToWeakObjectsMapTable].Predicate format understands == >= BETWEEN IN
LIKE MATCHES (regex) BEGINSWITH CONTAINS, the modifiers
[c] and [d] for case- and diacritic-insensitivity, ANY/ALL
over to-many keys, and the KVC collection operators.
%@/%K substitutions.a[0] on an empty array raises NSRangeException;
a.firstObject returns nil. Cocoa collections are consistently fail-fast on out-of-range access and
consistently forgiving of nil receivers — the opposite of the C intuition.dispatch_sync to the queue you are already on deadlocks immediately. The
common form is dispatch_sync(dispatch_get_main_queue(), ...) from code that is already on the main
thread. There is no re-entrancy check and no diagnostic — the app simply stops. Guard with
NSThread.isMainThread, or restructure so the call site knows its queue.Choose NSOperationQueue when you need cancellation, dependencies, priorities or KVO on
progress; choose GCD for everything smaller and cheaper.
AppKit and UIKit are not thread-safe. Every view, window and controller message must be sent on the main thread, and violations manifest as corrupted drawing or a crash minutes later rather than an immediate error. Foundation value objects are safe to read from many threads; mutable collections are not safe to mutate concurrently at all.
An NSRunLoop is a per-thread event loop: it blocks in mach_msg until an input
source fires, dispatches, and blocks again. The main thread’s is started for you by
NSApplicationMain/UIApplicationMain; a thread you create has one but it does not run
until you tell it to.
NSDefaultRunLoopMode does not fire in tracking mode. Add it to
NSRunLoopCommonModes. Same for NSURLConnection-era networking and any
performSelector:withObject:afterDelay:.NSTimer retains its target and the run loop retains the
timer, so self can never deallocate and dealloc can never invalidate it. Use the
block form with a weak self, or invalidate from an explicit teardown method
(viewWillDisappear:, windowWillClose:), not from dealloc.| Property | Consequence |
|---|---|
| Synchronous | post does not return until every observer has run. It is a fan-out function call, not a queue. |
| Same thread as the poster | A notification posted from a background thread runs your handler there. UI code needs an explicit hop — or the queue: parameter of the block form. |
| Observers are unretained | Under the modern runtime the selector form zeroes automatically, but the block form retains everything the block captures and never cleans up. That token must be removed. |
object:nil | Means “from any sender”, not “from no sender”. Passing a specific object is the cheap filter. |
~/Library/…. Under App Sandbox that path is redirected
into a container and the literal is wrong. URLsForDirectory:inDomains: (or
NSSearchPathForDirectoriesInDomains) always answers correctly.Defaults are a layered search: argument domain (-key value on the command line, which is a
superb debugging affordance), then application, global, language, and finally your registered defaults.
decodeObjectForKey: instantiates whatever class the archive names. That is
a remote code execution primitive on untrusted data, and it is why NSSecureCoding exists:
decodeObjectOfClass:forKey: checks the class before allocating. Never unarchive data you
did not write with the insecure API.The old +archiveRootObject:toFile: and +unarchiveObjectWithData: are deprecated
because they cannot be made secure. Archives preserve object graphs including cycles and identity;
+setClassName:forClass: handles renamed classes.
NSNull, not nil. Every field of a decoded payload needs
an NSNull check as well as a type check — [obj isKindOfClass:NSString.class]
is the honest guard, because a hostile or merely careless server can put a number where you expected a
string and NSNumber answers length with an exception.Plist types are exactly: NSString, NSNumber, NSDate,
NSData, NSArray, NSDictionary (string keys only) and booleans. Anything
else fails at write time.
| Piece | Role |
|---|---|
NSURLSessionConfiguration | defaultSessionConfiguration (cached, cookies, credentials), ephemeralSessionConfiguration (nothing on disk), backgroundSessionConfigurationWithIdentifier: (continues after the app exits; delegate-only, no completion handlers). |
NSURLSessionDataTask | Response into memory. |
NSURLSessionDownloadTask | Response to a temp file; resumable; the only kind allowed in a background session alongside upload. |
NSURLSessionUploadTask | Body from data, file or stream. |
NSURLSessionWebSocketTask | RFC 6455, since iOS 13 / macOS 10.15. |
-finishTasksAndInvalidate
or -invalidateAndCancel. A long-lived session whose delegate is a view controller is a permanent
leak. And never mix: a task created with a completion handler ignores most delegate callbacks.NSURLErrorAppTransportSecurityRequiresSecureConnection. Exceptions go in
Info.plist under NSAppTransportSecurity and need justification for App Store review.
NSAllowsLocalNetworking is the right key for a lab device on the LAN.Core Foundation is the C-language sibling of Foundation: an opaque-pointer object model with manual reference counting and its own naming conventions. A dozen central types are toll-free bridged — the same object, castable in either direction with no conversion at all.
| Foundation | Core Foundation |
|---|---|
NSString / NSMutableString | CFStringRef / CFMutableStringRef |
NSArray, NSDictionary, NSSet | CFArrayRef, CFDictionaryRef, CFSetRef |
NSData, NSNumber, NSDate | CFDataRef, CFNumberRef, CFDateRef |
NSURL, NSError, NSTimeZone, NSLocale | CFURLRef, CFErrorRef, CFTimeZoneRef, CFLocaleRef |
NSRunLoop… | CFRunLoopRef — not bridged; related, different objects |
Create or Copy in its name
returns an object you own and must CFRelease. A function with Get returns one you do
not own and must not release — retain it if you need it to outlive the call. That is the whole
convention, and it is the same rule as Objective-C’s, spelled differently.| Cast | Does |
|---|---|
(__bridge CFTypeRef)obj | Reinterpret, no ownership transfer. The common case — valid only while ARC keeps the object alive. |
(__bridge_retained CFTypeRef)obj | ARC hands ownership out; you must CFRelease. Same as CFBridgingRetain(obj). |
(__bridge_transfer id)cfObj | ARC takes ownership in; do not release. Same as CFBridgingRelease(cfObj). |
CFTypeRef
variable. Every Create/Copy needs a matching CFRelease, and passing NULL
to CFRelease crashes (unlike messaging nil). Use CFAutorelease() when you must
return a CF object without transferring ownership.SecKeychain. They still use
Create/Get, CFRelease, and CFTypeRef.Everything on this card is metadata: it changes diagnostics and, crucially, how the API looks from Swift. None of it changes the generated code. Annotating a header is the cheapest possible improvement to a mixed codebase.
nullable/nonnull for
Objective-C pointer positions and property attributes, _Nullable/_Nonnull for C
pointer positions like the double-indirect NSError **. Nothing is enforced at runtime —
a nonnull parameter can still receive nil from an unannotated caller, and Swift will then get a
non-optional that is nil, which crashes far away.NSArray<NSString *> * will happily hold
NSNumbers if they arrive through an unannotated API or a cast; you get a warning at the insertion
site and a crash at the use site. The annotation is a promise to readers and to Swift, not a check.NS_ENUM imports into Swift as an enum with a fixed backing type and gives you
exhaustiveness warnings in switch; NS_OPTIONS imports as an
OptionSet. A bare C enum gets neither.
NSApplicationMain creates NSApp, loads the main nib named in
Info.plist, connects the delegate, and runs the main run loop. Everything after that is
event-driven.
| Class | Job |
|---|---|
NSApplication (NSApp) | The singleton event pump. -run, -terminate:, -sendEvent:, the delegate. |
NSResponder | Base of the responder chain. NSView, NSWindow, NSViewController, NSApplication all descend from it. |
NSWindow / NSWindowController | A window and its lifetime owner. contentView, makeKeyAndOrderFront:, firstResponder. |
NSView | Rectangle, coordinate system (origin bottom-left unless isFlipped), -drawRect: or a CALayer, -setNeedsDisplay:. |
NSViewController | viewDidLoad, viewWillAppear, representedObject. |
NSDocument | The document architecture: NSDocumentController, undo, autosave, versions, all for free if you adopt readFromURL:/dataOfType:. |
NSCell | The lightweight pre-2011 drawing object still lurking inside controls and table columns. Legacy, but visible in APIs. |
A nil target is the mechanism behind menus: Copy is -copy: sent to nil, which walks
first responder → its superviews → window → window delegate → NSApp → app
delegate. Enable state comes from -validateUserInterfaceItem:.
| Class | Job |
|---|---|
UIApplication | Singleton; openURL:, background task assertions, state transitions. |
UIApplicationDelegate | application:didFinishLaunchingWithOptions: and the lifecycle. Since iOS 13, scene lifecycle moved to UISceneDelegate. |
UIWindow / UIWindowScene | One per scene; rootViewController owns the hierarchy. |
UIViewController | The unit of composition. Containment (addChildViewController:), navigation, presentation. |
UIView | Origin top-left, unlike AppKit. Always backed by a CALayer; frame vs bounds vs transform. |
UIResponder | Touch and press handling, becomeFirstResponder, the chain up to the app delegate. |
UITableView / UICollectionView | Data source + delegate, cell reuse by identifier. The single most important pattern in the framework. |
viewDidLoad runs once; viewWillAppear: runs every time.
Putting a data refresh in the former means stale content after a push-and-pop; putting one-time setup in the
latter means duplicate observers and duplicated subviews. This split accounts for a large share of UIKit bugs.-prepareForReuse.Objective-C and Swift share a runtime and a memory model, so bridging is real interop, not FFI. But the visibility is asymmetric: Swift sees nearly all of Objective-C; Objective-C sees only the part of Swift that can be expressed in the Objective-C runtime.
| Direction | Mechanism |
|---|---|
| Swift sees ObjC (app target) | The bridging header, <Target>-Bridging-Header.h. Import your ObjC headers there. |
| Swift sees ObjC (framework) | The framework’s umbrella header. No bridging header allowed. |
| ObjC sees Swift | #import "<Target>-Swift.h" — generated, never edited, and only contains @objc-visible declarations. |
Swift generics, tuples, structs (other than the bridged value types), enums with associated values, protocol extensions with default implementations, existentials with associated types, top-level functions, optionals of non-object types, and anything using Swift-only features in its signature. If it will not bridge, wrap it in a class that will.
T! and every collection as [Any], which pushes the failure from a compile error to a
runtime crash in Swift code that looks correct. Wrapping headers in
NS_ASSUME_NONNULL_BEGIN/_END and adding lightweight generics is a few minutes per
header and is the highest-value work in a mixed codebase.nonisolated by default;
NS_SWIFT_UI_ACTOR (and the UIView/NSView hierarchy’s own
annotations) mark main-actor APIs. A completion handler that fires on a background queue and is imported as
async still resumes wherever it was called — the bridge does not add a hop.| Pattern | Where it shows up |
|---|---|
| Delegation | A weak delegate plus an @optional protocol. Cocoa’s answer to subclassing for customisation — you configure an object rather than inherit from it. |
| Data source | A second delegate, separated because it supplies content rather than policy. NSTableView, UITableView. |
| Target-action | One selector plus one object; the nil-target form walks the responder chain. Menus and toolbars run on it. |
| Notification | Anonymous one-to-many. Use it when the sender must not know the receivers. |
| KVO / bindings | Observation without the observed object’s cooperation. |
| Class cluster | One public abstract face, many private concrete implementations chosen by the initialiser. |
| Two-stage creation | alloc then init…, so the initialiser may substitute or fail. |
| Singleton | +sharedThing with dispatch_once. Note the name: shared, not instance, and it does not forbid others. |
| Responder chain | Unhandled events walk up a dynamic hierarchy. The same idea as forwarding, at the UI level. |
-stringByAppendingPathComponent: is long because it tells you it returns a new string
(…By…ing) rather than mutating (-appendPathComponent: would). That
suffix convention runs through the whole framework and is worth imitating exactly.Document and a framework’s is a link-time duplicate-symbol
error at best and silent misbehaviour at worst.Not the Smalltalk triangle: the controller sits between model and view, and neither knows the other. Views communicate upward by target-action and delegation; models communicate upward by notification and KVO. A view that imports your model header is the design error the framework is built to prevent.
objc_exception_throw. Cocoa exceptions unwind to a top-level handler that logs and continues, so
by the time you see the console message the stack is gone. Breaking at the throw keeps it.strong where copy belongs. An NSString or
NSArray property declared strong can be mutated behind your back by whoever passed it
in.isEqual: without hash. Works everywhere except sets and
dictionary keys, where it silently produces duplicates.NSNotFound is NSIntegerMax, not −1.
if (idx >= 0) after a search is always true.init/dealloc. A subclass override runs against a
half-constructed or half-destroyed object. Touch the ivar directly.self implicitly. Referring to _ivar inside a
block captures self just as surely as writing self. If the object owns the block, that
is a cycle.[task resume]. NSURLSession tasks start suspended,
and a forgotten resume looks exactly like a network that never answers.dispatch_sync onto your own queue. Instant deadlock, no diagnostic. The
main-queue variant from main-thread code is the usual form.NSTimer retains its
target; a block-form notification observer retains its captures; a KVO registration must be removed before
dealloc finishes. All three crash or leak later, somewhere else.%d for an NSInteger. Correct on 32-bit, truncating on 64-bit.
Use %ld with a (long) cast, or %zd.NSLog(str). A format-string vulnerability, exactly as in C.CFRelease(NULL) crashes. Core Foundation has none of Objective-C’s
nil tolerance, and ARC does not manage CF objects even when they are bridged.null is NSNull, which is truthy.
if (value) does not screen it out; isKindOfClass: does.NSString, NSArray and their
relatives have no storage to inherit. Compose or add a category instead.atomic mistaken for thread safety. It protects a single accessor call and
nothing else.-fobjc-arc-exceptions. Treat a caught NSException as a message about a broken program,
not as recoverable control flow.