NSNotificationCenter: Broadcast Messaging and Its Overuse Risks

Understand NSNotificationCenter as a one-to-many broadcast mechanism for decoupled communication between objects, covering notification names, posting, both selector-based and block-based observing, userInfo payloads, observer lifetime, and the real architectural trade-offs of decoupling versus overuse.

Delegation is a one-to-one contract: an object has exactly one delegate, and it calls that delegate's methods directly, by reference. Key-Value Observing is a one-to-one-per-property contract, tied to a specific object and key path. NSNotificationCenter breaks that pattern entirely: it is a broadcast mechanism, where one object posts a notification without knowing or caring who, if anyone, is listening, and any number of unrelated observers, anywhere in the app, can register to be told about it. That one-to-many shape is exactly what makes notifications powerful for decoupling distant parts of a system, and exactly what makes them dangerous when reached for too casually, since a codebase full of notifications can become very hard to trace by simply reading the code. This lesson covers how notifications are named and posted, the two ways to observe them, the userInfo dictionary for attaching extra data, the observer lifetime and removal concerns notifications share with KVO, and — because the spec calls both out explicitly — the genuine decoupling benefit notifications provide alongside the real problems that come from overusing them.

Notifications as one-to-many broadcast

NSNotificationCenter, typically accessed through the shared singleton [NSNotificationCenter defaultCenter], sits in the middle of a publish-subscribe relationship. A publisher posts a notification without any reference to who might be observing it — it has no observer list of its own, no delegate property, nothing pointing at the objects that will eventually react. NotificationCenter itself owns the routing: it maintains the registry of who observes what, and when a matching notification is posted, it fans that single post out to every currently registered observer for that notification name.

This is a structurally different relationship from delegation. A delegate is a specific object, held by reference, that the delegating object calls directly and by design expects to have exactly one of (or, for a small number of well-defined delegate methods, zero). A notification's publisher, by contrast, has no idea how many observers exist for what it's about to post — zero, one, or a dozen unrelated parts of the app might all be listening, and the publisher's code is identical either way.

That anonymity is deliberate. Notifications exist specifically for situations where the publisher shouldn't need to know its observers, and where the number and identity of interested parties can change over the life of the app — a new screen added later can start observing an existing notification without the original publisher's code changing at all.

Notification names

A notification is identified by its name, an NSString (in modern SDKs, typically the NSNotificationName typedef, which is still fundamentally a string) that both the poster and every observer must agree on exactly. Apple's own frameworks post a large number of built-in notifications — UIKeyboardWillShowNotification, UIApplicationDidBecomeActiveNotification, and similar — each exposed as a global string constant so that referencing it is at least protected against typos by the compiler, since misspelling a constant's identifier fails to compile, unlike misspelling a raw string literal.

Application code defining its own custom notifications should follow the same pattern: declare a global NSNotificationName constant (extern in the header, defined once in the implementation) rather than scattering an inline string literal like @"UserDidLogOutNotification" across multiple files. This buys back some of the same compile-time safety KVC's stringly-typed keys give up — a typo in a constant's Objective-C identifier fails to build, whereas the same typo inside a raw string literal compiles silently and simply never matches at runtime.

Naming convention matters here too, mostly for readability at scale: a name like AppUserDidLogOutNotification, prefixed to avoid collisions with other frameworks and suffixed with Notification so its purpose is unambiguous at a glance, is far easier to grep for and reason about across a large codebase than a terse or generic name that could mean several different things depending on context.

Posting a notification

postNotificationName:object:userInfo: is the primary posting method: it takes the notification's name, an optional object reference (conventionally the object that triggered the notification, though observers can filter on it or ignore it entirely), and an optional userInfo dictionary carrying whatever additional data observers might need. All three notification-related pieces — name, object, userInfo — are delivered together to every matching observer in a single call.

Posting is synchronous by default: postNotificationName:object:userInfo: calls every matching observer's handler one after another, on the same thread and the same call stack as the post itself, before returning control to the code that posted it. This matters practically — a slow observer handler directly slows down whatever code posted the notification, and an observer that isn't expecting to run on a background thread can end up doing exactly that if the post happened there, since NotificationCenter delivers on the posting thread unless a queue was specified for a particular block-based registration.

Because delivery happens synchronously and in registration order (in practice, though this ordering is not part of the documented contract to rely on), a poorly designed set of observers can end up with subtle ordering dependencies — one observer's handler mutating state that another observer for the same notification then reads — which is exactly the kind of hard-to-trace coupling notifications are supposed to help you avoid, not introduce.

Observing: selector-based registration

The traditional way to observe is addObserver:selector:name:object:, which registers an object and a selector to be called on it whenever a matching notification is posted. The selector must match a specific method signature taking a single NSNotification * parameter, and that NSNotification object is how the handler accesses the notification's name, its object (the poster-supplied reference), and its userInfo dictionary.

Passing nil for the name parameter registers for every notification posted with the given object, regardless of name — rarely what you actually want, and usually a sign the registration should be narrowed. Passing nil for the object parameter (while specifying a name) registers for that notification name from any poster, which is by far the most common pattern, since most code cares about a kind of event happening, not specifically which object triggered it.

Selector-based observation predates blocks in Objective-C and remains extremely common in existing code, partly because it reads clearly at the registration call site and partly because the handler method can be reused, overridden, or referenced from multiple registration points without duplicating a block literal.

Observing: block-based registration

addObserverForName:object:queue:usingBlock: is the block-based alternative, registering a block to run instead of a selector. It takes the same name and object filtering as the selector-based method, plus a queue parameter specifying which operation queue the block should run on (passing nil runs it synchronously on the posting thread, the same as selector-based observation; passing [NSOperationQueue mainQueue] is a common way to guarantee the handler runs on the main thread regardless of which thread posted).

Crucially, this method returns an opaque observer token (an id, in practice an internal NSNotificationCenter-owned object) rather than registering against self the way the selector-based method implicitly does. That returned token is what you must hold onto and pass to removeObserver: later — losing track of it means losing your only handle for removing that specific block registration.

Block-based registration is convenient for short, local reactions — especially inside a method where capturing local context in the block is more natural than routing that context through a separate selector method's NSNotification userInfo — but it demands the same discipline around capturing self weakly inside the block (to avoid a retain cycle between the block, the notification center's internal registration, and self) that any long-lived block capturing self requires.

userInfo and reading a notification's payload

userInfo is an optional NSDictionary attached to a posted notification, carrying whatever additional data observers might need beyond just knowing the notification happened. A login notification might carry the logged-in user object under a documented key; a download-progress notification might carry a progress fraction and a byte count; the exact contents are entirely up to whatever convention the poster and its observers agree on.

Reading userInfo inside a handler means pulling values back out by key, with all the same stringly-typed risk KVC keys carry — a typo in the key used to read userInfo compiles cleanly and simply returns nil at runtime, silently producing a handler that behaves as if the expected data were never provided. As with notification names themselves, declaring userInfo keys as named constants rather than inline string literals is the standard mitigation, giving typos a chance to be caught as an unresolved identifier at compile time instead of a silent nil at runtime.

Because userInfo is untyped (NSDictionary<NSString *, id> at best), the receiving handler also has to know, out of band, what type to expect for each key and cast or unwrap accordingly — there's no compiler-enforced contract tying a notification's declared payload shape to what any given poster actually puts in the dictionary, which is one more place discipline has to substitute for compiler enforcement.

Observer lifetime, removal, and decoupling

Notification observers share the same lifetime discipline KVO observers do: every addObserver: call should eventually be matched by a removeObserver: call before the observing object is deallocated, and forgetting to do so used to be a reliable source of crashes in older Objective-C code, when NSNotificationCenter would attempt to message a deallocated observer. On modern Apple SDKs, NSNotificationCenter itself has been updated to hold observers weakly for the selector-based API, so a forgotten removeObserver: for that specific API is no longer the same hard crash it once was on current OS versions — but relying on that safety net rather than removing observers explicitly is still poor practice, both because it leaves the handler theoretically able to fire on a half-deallocated object in edge cases, and because much running and legacy code still targets versions or configurations where the old, stricter behavior applies.

The decoupling notifications provide is real and valuable: the object posting UserDidLogOutNotification does not need to import, reference, or know anything about the handful of screens across the app that need to react to a logout by clearing cached data or navigating back to a login screen. Each of those observers can be added, removed, or changed entirely independently of the posting code and of each other, which is exactly the kind of loose coupling that keeps a large codebase's modules from becoming tangled together through direct references.

That same anonymity is also notifications' biggest liability once a codebase leans on them too heavily. A direct method call or a delegate callback is something you can find with one "jump to definition"; a notification's observers can only be found by searching the whole codebase for that exact string or constant, and even then, understanding the full chain of cause and effect for a single posted notification means finding every observer, reading each one's handler, and mentally reconstructing an execution order that the code itself does nothing to make explicit. This is often described as spooky action at a distance — a change happens somewhere, and its effects ripple out to code that has no visible, traceable connection to the place that triggered it, making notifications genuinely harder to debug and reason about than the direct call they replaced, especially once a system accumulates dozens of overlapping notification names posted and observed across many files.

Code Examples

The notification name and its userInfo key are both declared as extern constants rather than inline string literals, so a typo referencing either fails to compile instead of silently missing at runtime. postNotificationName:object:userInfo: delivers the name, the poster (self, by convention), and the payload dictionary together, synchronously, to every currently registered observer.

NSNotificationName const AppUserDidLogOutNotification = @"AppUserDidLogOutNotification";

Registering with object:nil observes AppUserDidLogOutNotification from any poster, which is the common case. handleLogOut: reads reason back out of userInfo using the same declared constant key used to post it, avoiding a raw string literal on the read side. Removing in dealloc, using the same name and object filter as registration, keeps this observation properly balanced.

[[NSNotificationCenter defaultCenter] removeObserver:self name:AppUserDidLogOutNotification object:nil];

addObserverForName:object:queue:usingBlock: returns an opaque token that must be retained (here in logoutObserverToken) and later passed to removeObserver: -- unlike the selector-based API, there is no implicit 'self' registration to remove by. The block captures self weakly to avoid a retain cycle through the notification center's internal registration, and queue:[NSOperationQueue mainQueue] guarantees resetProfileUI runs on the main thread regardless of which thread the logout was posted from.

self.logoutObserverToken = [[NSNotificationCenter defaultCenter] addObserverForName:...usingBlock:...];

Common mistake: Discarding the returned token without storing it makes that specific block registration impossible to remove later.

// AppNotifications.h
extern NSNotificationName const AppUserDidLogOutNotification;
extern NSString * const AppUserDidLogOutReasonKey;

// AppNotifications.m
NSNotificationName const AppUserDidLogOutNotification = @"AppUserDidLogOutNotification";
NSString * const AppUserDidLogOutReasonKey = @"reason";

// Posting
[[NSNotificationCenter defaultCenter] postNotificationName:AppUserDidLogOutNotification
                                                      object:self
                                                    userInfo:@{ AppUserDidLogOutReasonKey: @"sessionExpired" }];
- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                              selector:@selector(handleLogOut:)
                                                  name:AppUserDidLogOutNotification
                                                object:nil];
}

- (void)handleLogOut:(NSNotification *)notification {
    NSString *reason = notification.userInfo[AppUserDidLogOutReasonKey];
    NSLog(@"Logged out: %@", reason);
    [self clearCachedUserData];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                      name:AppUserDidLogOutNotification
                                                    object:nil];
}
@interface ProfileViewController ()
@property (nonatomic, strong) id logoutObserverToken;
@end

@implementation ProfileViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.logoutObserverToken = [[NSNotificationCenter defaultCenter]
        addObserverForName:AppUserDidLogOutNotification
                    object:nil
                     queue:[NSOperationQueue mainQueue]
                usingBlock:^(NSNotification *note) {
                    [weakSelf resetProfileUI];
                }];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self.logoutObserverToken];
}

@end

Notification fan-out from publisher to observers

A publisher posts once to NotificationCenter, which fans the single notification out to every currently registered observer -- here A, B, and C -- with no direct reference between the publisher and any of them.

Publisher
    ↓
NotificationCenter
    ↓
┌──────┬──────┬──────┐
Observer A B   C

Where notifications genuinely earn their place

UIKit posts a large family of system notifications -- UIKeyboardWillShowNotification, UIApplicationDidEnterBackgroundNotification, UIDeviceOrientationDidChangeNotification -- because these are exactly the cases notifications are built for: an event that many, unrelated parts of an app might independently care about, originating from a system object no individual observer could reasonably hold a delegate reference to. A well-designed app-level notification, like a session-expired or logout event that several independent screens need to react to by clearing their own state, sits in the same category. The pattern breaks down when it's used for ordinary, local communication between two objects that already have a direct reference to each other -- a view and the specific view controller that owns it, or a network call and the one object that requested it -- where a delegate method, a completion block, or a plain method call would be strictly easier to find, read, and debug than a notification posted and observed somewhere else in the codebase.

[[NSNotificationCenter defaultCenter] postNotificationName:@"UserDidLogOut" object:nil];

extern NSNotificationName const AppUserDidLogOutNotification; // declared once, referenced everywhere [[NSNotificationCenter defaultCenter] postNotificationName:AppUserDidLogOutNotification object:self];

A raw string literal repeated at every post and observe site is one typo away from a notification that's posted but never observed, or observed under a name nothing ever posts, with no compiler warning either way. A shared constant catches the typo at compile time instead.

self.logoutToken = [[NSNotificationCenter defaultCenter] addObserverForName:AppUserDidLogOutNotification object:nil queue:nil usingBlock:^(NSNotification *n) { [self resetUI]; }]; // no removeObserver: call anywhere, and no weak self capture

__weak typeof(self) weakSelf = self; self.logoutToken = [[NSNotificationCenter defaultCenter] addObserverForName:AppUserDidLogOutNotification object:nil queue:nil usingBlock:^(NSNotification *n) { [weakSelf resetUI]; }]; // paired with [[NSNotificationCenter defaultCenter] removeObserver:self.logoutToken]; in dealloc

Capturing self strongly inside a long-lived observation block can create a retain cycle through the notification center's internal registration, and never removing the returned token leaves the block-based registration active for the app's entire lifetime instead of the observer's actual intended lifetime.

NSString *reason = notification.userInfo[@"Reason"]; // wrong case, not the key actually posted

NSString *reason = notification.userInfo[AppUserDidLogOutReasonKey];

userInfo is read by string key with exactly the same stringly-typed risk as KVC: a mismatched key compiles fine and simply returns nil at runtime, silently producing a handler that behaves as though no data were provided at all.

// Posting a notification for every minor state change instead of calling the interested object directly [[NSNotificationCenter defaultCenter] postNotificationName:@"ButtonTitleShouldUpdate" object:self userInfo:@{@"title": newTitle}];

// A direct call or delegate method is clearer when there's exactly one, known interested party [self.delegate buttonTitleDidChange:newTitle];

Reaching for a notification when there is exactly one known, nearby object that actually needs to react trades a traceable, one-step method call for a broadcast that has to be found by searching the whole codebase for a string -- overuse that makes control flow needlessly hard to follow without buying any real decoupling benefit.

How does NSNotificationCenter's relationship between a publisher and its observers differ structurally from delegation? A. It is identical to delegation, just renamed B. It is one-to-many: the publisher posts without knowing or holding a reference to any observer, and NotificationCenter fans the post out to every currently registered observer C. It only supports exactly one observer per notification name, same as a delegate D. It requires the publisher to hold a strong reference to every observer

B. It is one-to-many: the publisher posts without knowing or holding a reference to any observer, and NotificationCenter fans the post out to every currently registered observer — Unlike a delegate, which is a specific object held by reference, a notification's publisher has no reference to and no knowledge of its observers -- NotificationCenter owns the routing and fans a single post out to however many observers are currently registered, including zero.

What does addObserverForName:object:queue:usingBlock: return, and why does it matter? A. Nothing useful -- it returns void B. An opaque observer token that must be retained and later passed to removeObserver: to remove that specific block registration C. The notification's userInfo dictionary D. A reference to the object that will eventually post the notification

B. An opaque observer token that must be retained and later passed to removeObserver: to remove that specific block registration — Unlike the selector-based API, which registers implicitly against self and is removed by passing self, the block-based API returns a token representing that specific registration, and that token -- not self -- is what must be passed to removeObserver: to remove it.

What is the risk of reading a value out of a notification's userInfo dictionary by a raw string literal key? A. It is faster than using a declared constant B. A mismatched or misspelled key compiles cleanly and simply returns nil at runtime, silently producing a handler that behaves as though the data were never provided C. It causes a compile error immediately D. userInfo dictionaries cannot be read with string literals at all

B. A mismatched or misspelled key compiles cleanly and simply returns nil at runtime, silently producing a handler that behaves as though the data were never provided — userInfo is read by string key with the same stringly-typed risk as KVC -- a typo compiles fine and returns nil at runtime rather than failing loudly, which is why declaring userInfo keys as shared constants is the standard mitigation.

What genuine architectural benefit does the spec attribute to notifications, versus delegation's tighter contract? A. Notifications execute faster than any direct method call B. Decoupling -- the publisher doesn't need to know or care who, if anyone, is listening, letting observers be added or removed independently of the posting code C. Notifications guarantee exactly-once delivery to a single observer D. Notifications eliminate the need for any error handling

B. Decoupling -- the publisher doesn't need to know or care who, if anyone, is listening, letting observers be added or removed independently of the posting code — Because the publisher holds no reference to its observers, any number of unrelated parts of an app can start or stop observing a notification without the posting code changing at all -- a decoupling benefit delegation's one-to-one contract doesn't offer.

What problem does the spec identify with overusing notifications? A. They cannot carry any data beyond their name B. They make control flow hard to trace -- 'spooky action at a distance' -- since finding a notification's effects means searching the whole codebase for every observer rather than following one direct call C. They can only be posted from the main thread D. They are deprecated and no longer usable in modern Objective-C

B. They make control flow hard to trace -- 'spooky action at a distance' -- since finding a notification's effects means searching the whole codebase for every observer rather than following one direct call — The same anonymity that makes notifications good for decoupling makes them hard to trace when overused: unlike a direct method call, understanding a notification's full effect means finding every observer across the codebase and reconstructing an execution order the code itself doesn't make explicit.

Passing nil for the object parameter to addObserver:selector:name:object: (while specifying a name) does what? A. Registers for every notification regardless of name B. Fails to register at all, since object cannot be nil C. Registers for that specific notification name posted by any object, which is the most common registration pattern D. Registers only for notifications posted by the observer itself

C. Registers for that specific notification name posted by any object, which is the most common registration pattern — Passing nil for object while specifying a name filters only by notification name and accepts that notification from any poster -- the most common pattern, since most observers care about the kind of event, not specifically which object triggered it.

Define a custom notification AppDownloadDidFinishNotification carrying a userInfo payload with a fileURL key. Write the posting code for a Downloader class that posts it when a download completes, and write two different observers of it in a DownloadListViewController: one registered with the selector-based API and one with the block-based API. Make sure both observers are removed correctly.

// Constants extern NSNotificationName const AppDownloadDidFinishNotification; extern NSString * const AppDownloadFileURLKey; NSNotificationName const AppDownloadDidFinishNotification = @"AppDownloadDidFinishNotification"; NSString * const AppDownloadFileURLKey = @"fileURL"; // Posting, inside Downloader [[NSNotificationCenter defaultCenter] postNotificationName:AppDownloadDidFinishNotification object:self userInfo:@{ AppDownloadFileURLKey: fileURL }]; // Selector-based observer [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDownloadFinished:) name:AppDownloadDidFinishNotification object:nil]; // Block-based observer self.downloadToken = [[NSNotificationCenter defaultCenter] addObserverForName:AppDownloadDidFinishNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { NSURL *url = note.userInfo[AppDownloadFileURLKey]; [self reloadRowForFileURL:url]; }]; // Removal, in dealloc [[NSNotificationCenter defaultCenter] removeObserver:self name:AppDownloadDidFinishNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self.downloadToken];

Declare both the notification name and the userInfo key as extern constants in a header, defined once in an implementation file, exactly as shown in this lesson's first example -- and remember the block-based registration needs its returned token stored and passed to removeObserver:, while the selector-based one is removed by passing self.