Documentation

Learn how to write documentation directly in Objective-C headers that communicates real contracts — nullability, ownership, thread-safety, and error behavior — rather than restating method names, so callers can use an API correctly without reading its implementation.

Good documentation is not an afterthought bolted onto finished code; it is part of the API's contract, and in Objective-C it lives closest to where callers will actually see it: the public header. This lesson covers how to write documentation comments that Xcode's Quick Help and documentation generators understand, and — more importantly — what to put in them. The most valuable Objective-C documentation does not restate what a method's name already tells you; it captures the things a caller cannot infer from the signature alone: nullability, ownership, thread-safety, error conditions, and how to migrate when an API changes.

Header documentation: where callers actually look

In Objective-C, the public .h file is where documentation earns its keep, because that is what a caller opens (or what Xcode's Quick Help surfaces) when they want to know how to use your class without reading its implementation. Documentation buried only in the .m file, or worse, not written at all, is invisible at the exact moment it would be most useful — when someone is staring at your header trying to decide how to call a method.

The standard convention is a documentation comment written directly above the declaration, using either the /** ... */ block form or a run of /// line comments. Tools like Xcode's Quick Help (option-click on a symbol) and documentation generators such as jazzy or appledoc parse these comments and their light markup (like @param, @return, and Markdown-style emphasis) to produce readable, searchable documentation without any extra authoring step beyond writing the comment well in the first place.

A practical habit: every public class should have a short doc comment above its @interface explaining what it is for, and every public method (excluding perhaps the most self-explanatory trivial accessors) should have a doc comment above its declaration in the header.

API comments: describing behavior, not restating the name

The most common documentation mistake is writing a comment that just restates the method's name in prose. A comment like '// Fetches the user' above a method named fetchUser tells the reader nothing they didn't already know from the signature. A good comment above a method describes what it does at a level the name cannot capture: side effects, caching behavior, what triggers a network request versus a cache hit, what happens on failure, and any preconditions the caller must satisfy before calling it.

Think of the doc comment as answering the question a caller would actually ask before using this method for the first time: 'what do I need to know that I can't get from the signature?' If the answer is 'nothing,' a short comment is fine or even unnecessary for a trivial method. If the answer involves caching, threading, ownership, or error conditions, that is exactly what belongs in the comment — and several of those specific categories get their own subsections below because they matter enough to call out individually.

Nullability as enforced documentation

Nullability annotations, covered in depth in the dedicated Nullability lesson earlier in this course, deserve special mention here because they are, in a very real sense, documentation — just documentation the compiler enforces rather than documentation that is merely prose. Marking a parameter or return type as nullable or nonnull tells a caller exactly what to expect about optionality, and because it is checked at compile time (and surfaced to Swift callers as Optional versus non-Optional), it cannot silently go stale the way a comment can.

This is the core argument for preferring nullability annotations over a prose comment saying 'may return nil': a comment can be wrong the moment someone changes the implementation and forgets to update it, but NS_ASSUME_NONNULL_BEGIN/END and explicit _Nullable/_Nonnull markers are checked by the compiler on every build. Wherever a contract can be expressed as a nullability annotation instead of (or in addition to) a sentence, prefer the annotation — it is documentation that cannot lie.

Parameter descriptions with @param

For methods with parameters whose meaning is not obvious from their name and type alone, a documentation comment should describe each one using the @param convention (@param parameterName description). This matters most for parameters that take a unit that is not self-evident (a timeout in seconds versus milliseconds), a parameter whose valid range is constrained (an index that must be within bounds, or a string that must be non-empty), or a parameter whose effect on behavior is not obvious from its name.

It is not necessary — and can even be counterproductive — to write an @param entry for every single parameter regardless of how obvious it is. A parameter named userID of type NSString * rarely needs elaboration. Documentation is most valuable when it is selective: apply real effort to the parameters that actually need explaining, and let self-explanatory ones speak for themselves.

Ownership documentation

In pre-ARC Objective-C codebases, and in any code that bridges to manual memory management (Core Foundation objects being the most common case, discussed in the Manual Reference Counting lesson), documenting ownership explicitly in the header was essential: a method following the create/copy/new/alloc-implies-owned-return-value convention needed to say so, because getting it wrong meant a leak or a crash. Even under ARC, ownership can still have non-obvious behavior worth documenting — for instance, a method that unexpectedly retains a block or object passed into it beyond the duration of the call (storing it for later use rather than just using it synchronously), which a caller would not otherwise suspect from the signature.

The rule of thumb: if a method's ownership behavior matches what a caller would naturally assume (parameters used only for the duration of the call, return values following standard Cocoa naming conventions), no special documentation is needed. If it deviates from that assumption in any way, write it down explicitly — this is exactly the kind of contract that is invisible in the signature and expensive to get wrong.

Thread-safety documentation

Thread-safety is, without exaggeration, one of the most important things to document and one of the most commonly left undocumented. The dedicated Thread Safety lesson earlier in this course explains the underlying mechanics; this lesson emphasizes the documentation side of that contract specifically: whether a class or method is safe to call from any thread, must be called only from the main queue, or must be called only from a specific serial queue is not visible anywhere in a method's signature. There is no compiler-enforced nonnull-style marker for 'this must be called on the main thread' in plain Objective-C (unless you adopt annotations like NS_SWIFT_UI_ACTOR-style hints for Swift interop or your own convention), which means it MUST be written down in the header as prose, or it effectively does not exist as a contract at all.

A good thread-safety doc comment states its guarantee plainly: 'This method must be called on the main queue' or 'This class is safe to use from any thread; internal state is protected by a private serial queue' or 'Completion handlers are always invoked on the queue passed to the initializer.' Leaving this undocumented does not make a class thread-safe or thread-unsafe — it just means every caller has to guess, and guesses about concurrency are exactly the kind of mistake that produces intermittent, hard-to-reproduce crashes.

Error documentation

Building on the NSError conventions from the earlier Error Handling lesson, a well-documented method that can fail should say, in its header comment, what conditions produce an error, what error domain and codes a caller might see, and whether the method's primary return value is meaningful when an error occurred (in Cocoa convention, usually not — callers should check the error first). This is especially important for methods whose failure modes are not obvious: a network request failing due to no connectivity is expected, but a method that can also fail due to malformed cached data, an expired credential, or a validation failure deserves those specific cases spelled out.

Documenting error conditions also helps the author, not just the caller: writing down every way a method can fail is a good forcing function for noticing failure modes that were not actually handled in the implementation yet.

Examples and migration notes

For any API whose correct usage is not immediately obvious from its signature and prose description alone, including a short usage example directly in the documentation comment dramatically reduces the friction of using it correctly. A three-line example showing a typical call, including how to handle the completion block or error, saves a caller from having to reverse-engineer usage from other call sites scattered across the codebase — and it gives the reviewer of a new call site something concrete to compare against.

Migration notes matter whenever an API changes shape or is deprecated in favor of something new. The API Design for Swift lesson covers the mechanism for this — availability annotations like NS_DEPRECATED and NS_SWIFT_NAME direct callers toward the replacement and can even fail the build once a deprecated API is removed. Documentation is the explanation layered on top of that mechanism: it should say why the old API is being retired, what the new API does differently, and concretely how to adapt existing call sites, not just that the old one is deprecated.

Code Examples

This single comment covers behavior (caching), thread-safety (safe from any thread, completion always on main), a non-obvious @param note (scheme restriction), error documentation (specific domains and the nil-return convention), and a short usage example — every category this lesson covers, in one realistic, compact block.

Thread-safety: safe to call from any thread. The completion handler is always invoked on the main queue

Even under ARC, this method's behavior deviates from what a caller would naturally assume: the block is retained beyond the call rather than just being invoked synchronously and discarded. The ownership note makes the retain-cycle risk explicit before the caller writes the mistake, which is exactly the kind of contract that is invisible in the bare method signature.

Common mistake: Assuming ARC means ownership never needs documenting. ARC manages memory correctly according to the code as written, but it cannot warn you about an API that captures a block or object for longer than you'd expect.

NS_DEPRECATED_IOS is the compiler-enforced mechanism that flags every call site with a warning (and, past the deprecated version, an error). The doc comment layered on top explains why the change happened (multi-account support), gives a concrete before/after example, and calls out a behavioral difference (explicit error handling) that a caller would not learn from the deprecation warning alone.

NS_ASSUME_NONNULL_BEGIN

/**
 * Downloads and decodes an image from a remote URL, using an in-memory
 * cache keyed by URL to avoid redundant network requests.
 *
 * Thread-safety: safe to call from any thread. The completion handler
 * is always invoked on the main queue, regardless of which thread this
 * method was called from.
 *
 * @param url The remote image URL. Must use http or https scheme.
 * @param completion Called exactly once, either with a decoded image
 *        and a nil error, or with a nil image and a non-nil error.
 *        Possible error domains: ImageDownloaderErrorDomain for HTTP
 *        or decoding failures, NSURLErrorDomain for network failures.
 *
 * Example:
 *   [downloader downloadImageAtURL:url completion:^(UIImage *image, NSError *error) {
 *       if (error) { [self showPlaceholder]; return; }
 *       self.imageView.image = image;
 *   }];
 */
- (void)downloadImageAtURL:(NSURL *)url
                 completion:(void (^)(UIImage * _Nullable image,
                                       NSError * _Nullable error))completion;

NS_ASSUME_NONNULL_END
/**
 * Registers an observer block to be called whenever the session expires.
 *
 * Ownership note: this method stores a strong reference to `handler`
 * for the lifetime of the receiver (or until -removeExpirationHandler
 * is called). Capturing self strongly inside `handler` will create a
 * retain cycle with the receiver — capture self weakly instead.
 *
 * @param handler Block invoked on session expiration. Stored strongly;
 *        see ownership note above.
 */
- (void)setExpirationHandler:(void (^)(void))handler;

/// Removes a previously registered expiration handler, if any.
- (void)removeExpirationHandler;
/**
 * Fetches the current user's profile.
 *
 * @deprecated Use -fetchProfileForUserID:completion: instead. This
 * method assumed a single signed-in user, which no longer holds now
 * that the app supports multiple accounts. Replace call sites like:
 *
 *   [manager fetchCurrentUserProfile:^(UserProfile *profile) { ... }];
 *
 * with:
 *
 *   [manager fetchProfileForUserID:self.activeUserID
 *                        completion:^(UserProfile *profile, NSError *error) { ... }];
 *
 * Note the new completion block also surfaces errors explicitly, so
 * migrated call sites must add error handling that the old API did
 * not require.
 */
- (void)fetchCurrentUserProfile:(void (^)(UserProfile *profile))completion
    NS_DEPRECATED_IOS(9_0, 14_0, "Use fetchProfileForUserID:completion: instead");

Documentation as a review gate

Many mature Objective-C codebases treat documentation as part of the pull request review checklist, not an optional nicety: a new public method without a doc comment, or a changed method whose existing doc comment is now inaccurate, is grounds for review comments just like a memory-management bug would be. This works because the header is a durable public contract — other engineers, and often other teams, will call this API for years based only on what its header says. Investing a few extra minutes in a precise doc comment when the context is fresh in the author's mind is far cheaper than every future caller having to read the implementation (or worse, guess) to understand the same contract.

// Sets the name -- above a method named setName:

A comment that explains non-obvious behavior (validation performed, side effects, threading) or is omitted entirely if the method truly is self-explanatory.

A comment that only restates the method's name adds visual noise without adding information. Documentation should tell the reader something the signature doesn't.

Relying on a prose comment saying 'this parameter may be nil' instead of a _Nullable annotation.

Use NS_ASSUME_NONNULL_BEGIN/END and explicit _Nullable/_Nonnull markers, which the compiler checks on every build.

A prose comment can silently go stale when the implementation changes. A nullability annotation is checked by the compiler and by Swift interop, so it cannot drift from reality unnoticed.

Leaving thread-safety completely undocumented because 'it's obvious from context.'

Explicitly state in the header which queue(s) a class or method is safe to call from.

Thread-safety expectations are invisible in a method signature. Undocumented, every caller has to guess, and a wrong guess produces intermittent, hard-to-reproduce crashes.

Deprecating an API with only NS_DEPRECATED and no explanatory documentation.

Pair the availability annotation with a doc comment explaining why the change happened and a concrete before/after migration example.

The annotation tells a caller that something changed; the documentation tells them why and exactly how to adapt, which is what actually gets a migration done correctly and quickly.

Why should header documentation comments avoid simply restating a method's name? A. Because Xcode's Quick Help ignores restated names B. Because a comment that only restates the name adds no information the signature didn't already provide C. Because clang-format removes such comments automatically D. Because Objective-C headers cannot contain comments

B. Because a comment that only restates the name adds no information the signature didn't already provide — The value of a doc comment is telling the reader something they cannot get from the signature alone; restating the name wastes that opportunity.

Why are nullability annotations considered a superior form of documentation to a prose comment about nil? A. Prose comments are not allowed in Objective-C B. Nullability annotations are compiler-checked and cannot silently go stale the way a comment can C. Nullability annotations replace the need for any other documentation D. Prose comments run slower at compile time

B. Nullability annotations are compiler-checked and cannot silently go stale the way a comment can — A nullability annotation is verified by the compiler on every build (and surfaced to Swift), so unlike a comment, it cannot drift out of sync with the actual implementation without being noticed.

Why must thread-safety information usually be written down explicitly in documentation? A. Because it is always visible directly in the method signature B. Because there is generally no compiler-enforced marker for it in plain Objective-C, so it is invisible unless documented C. Because Xcode automatically infers and displays it D. Because it never matters for correctness

B. Because there is generally no compiler-enforced marker for it in plain Objective-C, so it is invisible unless documented — Unlike nullability, thread-safety expectations typically have no compiler-checked marker in Objective-C, so the only way a caller can know the contract is if it is written down explicitly.

When should ownership behavior be explicitly documented under ARC? A. Never, since ARC manages all memory automatically B. Only in Objective-C++ files C. When a method's ownership behavior deviates from what a caller would naturally assume, such as retaining a passed-in block beyond the call D. Only for methods that return void

C. When a method's ownership behavior deviates from what a caller would naturally assume, such as retaining a passed-in block beyond the call — ARC removes manual retain/release bookkeeping, but it does not make non-obvious ownership behavior (like storing a block for later use) self-evident to a caller; that still needs documentation.

What should accompany an NS_DEPRECATED annotation to make a migration actually easy for callers? A. Nothing further is needed; the annotation alone is sufficient B. A documentation comment explaining why the change happened and a concrete before/after migration example C. Removing the old method immediately so no one can call it D. A change to the method's name only

B. A documentation comment explaining why the change happened and a concrete before/after migration example — The availability annotation is the compiler-enforced mechanism, but documentation explaining the why and providing a concrete migration example is what actually helps a caller adapt correctly and quickly.

According to this lesson, when is a usage example in documentation most valuable? A. For every single method regardless of complexity B. Never, since examples belong only in separate tutorials C. For APIs whose correct usage is not immediately obvious from the signature and prose alone D. Only for private, internal-only methods

C. For APIs whose correct usage is not immediately obvious from the signature and prose alone — A short usage example earns its place when it meaningfully reduces the friction of using a non-obvious API correctly; trivial methods don't need one.

Write a documentation comment for a method - (BOOL)saveToken:(NSString *)token error:(NSError **)error that stores a credential in the Keychain, given that it can fail if the Keychain is locked or if the token is empty, and that it is safe to call from any thread.

A comment noting: this stores token securely in the Keychain under a fixed service identifier; safe to call from any thread; the token parameter must be non-empty (empty tokens return NO with an error); on failure, error is populated and describes either a locked Keychain (errSecInteractionNotAllowed) or an invalid empty token; returns YES only if the token was successfully persisted.

Cover: what the method does beyond its name, the @param details worth calling out, thread-safety, and the specific error conditions.