NotificationCenter is a system mechanism in iOS for sending and receiving notifications between application components without a direct connection between sender and receiver. Based on the Observer pattern, NotificationCenter allows objects to subscribe to events and respond to them asynchronously. According to Apple Documentation (2025), NSNotificationCenter supports both synchronous notification posting via post(name:object:) and deferred posting via NotificationQueue. The notification center operates within a single process and does not cross application boundaries.
Key Takeaways
NotificationCenter (NSNotificationCenter) is a built-in iOS mechanism for implementing loosely coupled communication between objects. The Observer pattern allows one object (sender) to notify multiple other objects (observers) about an event without a direct reference to them. NotificationCenter operates with three entities: Notification.Name (notification identifier), Notification (container with data), and NotificationCenter (dispatcher). Each application has a shared default center.
Notification.Name is a structure that identifies the notification type. Created via extension Name: Notification.Name(“MyNotification”). Notification is an object containing name, object (sender), and userInfo (dictionary with data). System notifications are declared as constants: UIApplication.didBecomeActiveNotification, UIResponder.keyboardWillShowNotification. Custom notifications should be grouped via extension to avoid name collisions. Names should be reverse-domain.
// Defining custom notifications
extension Notification.Name {
static let dataDidUpdate =
Notification.Name("com.app.dataDidUpdate")
static let userLoggedOut =
Notification.Name("com.app.userLoggedOut")
}
// Sending a notification with data
let userInfo: [String: Any] = [
"userId": 123,
"timestamp": Date()
]
NotificationCenter.default.post(
name: .dataDidUpdate,
object: nil,
userInfo: userInfo
)
An observer subscribes to a notification via the addObserver(_:selector:name:object:) method. Selector is the method that will be called when the notification is received. The object parameter allows filtering notifications from a specific sender. If object is nil, the observer receives all notifications with the specified name from any sender. Since iOS 9, addObserver does not require manual removal for block-based API, but selector-based still requires removeObserver.
// Subscribing to a notification (selector-based)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleDataUpdate),
name: .dataDidUpdate,
object: nil
)
@objc func handleDataUpdate(_ notification: Notification) {
guard let userId = notification.userInfo?["userId"] as? Int else { return }
updateUI(for: userId)
}
// Subscribing to a notification (block-based, iOS 9+)
var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(
forName: .dataDidUpdate,
object: nil,
queue: .main
) { [weak self] notification in
guard let self else { return }
self.handleNotification(notification)
}
NotificationCenter stores a mapping table (name → set of observers). When the sender calls post(name:object:), the notification center synchronously iterates through all observers subscribed to that name and calls their selectors or blocks. Key feature: post blocks the current thread until all handlers complete. If handlers perform heavy operations, this delays the sender. NotificationQueue solves this problem by deferring notification delivery.
The post(name:object:userInfo:) method sends a notification immediately to all observers. The call is synchronous — code after post executes only after all handlers complete. The order of observer invocation is not guaranteed and may change between launches. For sequential processing, use NotificationQueue with coalescing. Do not call post inside a handler for the same notification — this leads to infinite recursion.
NotificationQueue adds notifications to a queue for asynchronous delivery. It supports coalescing (merging identical notifications) and delivery queue selection (asap, idle, modal). Coalescing is useful for frequent events (download progress) when you only need to notify with the latest value. NotificationQueue uses the run loop to fire, so it only works in threads with an active run loop.
// Delayed posting via NotificationQueue
let notification = Notification(
name: .dataDidUpdate,
object: self,
userInfo: ["progress": 0.5]
)
// Coalescing: multiple notifications are merged into one
NotificationQueue.default.enqueue(
notification,
postingStyle: .whenIdle,
coalesceMask: .onName,
forModes: [.common]
)
// Asynchronous delivery via DispatchQueue
DispatchQueue.main.async {
NotificationCenter.default.post(name: .dataDidUpdate, object: nil)
}
iOS provides three main mechanisms for communication between objects: NotificationCenter, Delegate, and KVO (Key-Value Observing). Each solves the notification problem but with different trade-offs in coupling, performance, and type safety. The choice of mechanism depends on the one-to-one or one-to-many relationship and the need for data transfer.
| Characteristic | NotificationCenter | Delegate | KVO |
|---|---|---|---|
| Coupling | Weak (notification name) | Strong (protocol) | Medium (key) |
| Relationship | One-to-many | One-to-one | One-to-many |
| Type Safety | Low (userInfo as Dictionary) | High (protocol methods) | Medium (Any?) |
| Performance | Medium (table traversal) | High (direct call) | Low (NSObject) |
| Asynchrony | Synchronous (post blocks) | Synchronous in sender’s thread | Synchronous on change |
NotificationCenter is ideal for events that multiple independent components need to respond to. Examples: app settings changes, user logout, receiving a push notification in the background. NotificationCenter is also suitable for loosely coupled modules (feature A should not know about feature B). The drawback is lack of type safety: userInfo keys are strings, not enums.
Delegate is the choice for one-to-one relationships with a clear contract (tableView.delegate). Delegate is faster and safer by types. KVO is the choice for observing changes to a specific model property (isLoading, progress). KVO requires inheritance from NSObject and can cause debugging difficulties (magic string keys). In modern Swift, Combine and async sequences replace all three approaches.
The addObserver method supports two subscription variants: selector-based (traditional) and block-based (with closure). Selector-based requires @objc compatibility and manual observer removal. Block-based (iOS 9+) allows using a capture list and is automatically managed by the OS when using blocks without strong references. Block-based also supports queue — the observer receives the notification in the specified queue.
The traditional way of subscribing via selector. The handler method must be marked @objc and accept an optional Notification. Advantage: can be used by any class, including legacy Objective-C. Disadvantages: lack of type safety for the selector, risk of typos in the selector name, mandatory removeObserver in deinit. If the observer is removed before the object, the handler will not be called.
Block-based API accepts a closure that executes when the notification is received. The queue parameter determines which queue the block runs on — main queue for UI updates or background queue for data processing. The return value NSObjectProtocol is used to remove the observer: NotificationCenter.default.removeObserver(observer). Block-based is preferred in modern Swift.
protocol NotificationToken {
func dispose()
}
extension NotificationCenter {
func observe(
name: NSNotification.Name,
object: Any? = nil,
queue: OperationQueue? = .main,
using block: @escaping (Notification) -> Void
) -> NotificationToken {
let observer = addObserver(forName: name, object: object,
queue: queue, using: block)
return NotificationTokenWrapper(observer: observer, center: self)
}
}
// Using with automatic removal
class ViewModel {
private var tokens: [NotificationToken] = []
func startObserving() {
let token = NotificationCenter.default.observe(
name: .dataDidUpdate,
queue: .main
) { [weak self] notification in
self?.handleUpdate(notification)
}
tokens.append(token)
}
deinit {
tokens.forEach { $0.dispose() }
}
}
Memory leaks are one of the main issues when working with NotificationCenter. If an observer is not removed before deallocation, when a notification is sent the center will try to call a method on an already deallocated object, resulting in EXC_BAD_ACCESS. Since iOS 9, block-based addObserver uses weak references, but selector-based still requires manual removeObserver. Best practice: remove the observer in deinit.
Selector-based: always call NotificationCenter.default.removeObserver(self) in deinit. If the observer is subscribed to multiple notifications, you can remove all at once (without parameters) or a specific one by name. Block-based: remove via removeObserver with the token returned by addObserver. For block-based on iOS 9+, a leak does not occur, but removal is still recommended for performance: deallocated observers will not be iterated during post.
class SafeObserver {
private var observers: [NSObjectProtocol] = []
func addSubscriptions() {
let token1 = NotificationCenter.default.addObserver(
forName: .dataDidUpdate, object: nil,
queue: .main) { [weak self] _ in
self?.refreshData()
}
let token2 = NotificationCenter.default.addObserver(
forName: .userLoggedOut, object: nil,
queue: .main) { [weak self] _ in
self?.logout()
}
observers.append(contentsOf: [token1, token2])
}
deinit {
observers.forEach { NotificationCenter.default.removeObserver($0) }
}
private func refreshData() { }
private func logout() { }
}
The Token pattern automates observer management. On subscription, a token object (NSObjectProtocol) is returned, which automatically removes the observer upon deallocation. NotificationTokenWrapper stores a weak reference to NotificationCenter and the observer token, calling removeObserver in deinit. This brings NotificationCenter closer to the Combine approach, where AnyCancellable manages the subscription lifecycle.
Thread safety: NotificationCenter guarantees that post can be called from any thread, and all observers will receive the notification on the same thread where post was called. This is critical for multithreaded applications: if a notification is sent from a background thread, handlers will also execute on the background thread. For UI updates, dispatch handling to the main queue via DispatchQueue.main.async.
NotificationCenter is thread-safe for post and addObserver calls from different threads. Internal synchronization uses locking, so frequent posts from multiple threads can create contention. For high-load scenarios (download progress of 1000 files), use a separate notification queue or Combine publisher. NotificationQueue with postingStyle .now is equivalent to direct post.
NotificationCenter supports Combine publisher via NotificationCenter.default.publisher(for:object:). Publisher turns each notification into a Combine event that can be transformed through map, filter, debounce, and throttle. This solves the synchronous delivery problem: Combine processes notifications asynchronously on the specified Scheduler. NotificationCenter.publisher is a bridge between the legacy mechanism and modern reactive programming.
import Combine
class ReactiveViewModel {
private var cancellables = Set<AnyCancellable>()
func setupCombineSubscription() {
NotificationCenter.default
.publisher(for: .dataDidUpdate)
.receive(on: DispatchQueue.main)
.compactMap { $0.userInfo?["progress"] as? Float }
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.sink { [weak self] progress in
self?.progressLabel.text = "\(Int(progress * 100))%"
}
.store(in: &cancellables)
}
}
Frequently Asked Questions
Yes, NotificationCenter is thread-safe for post and addObserver calls from any thread. However, handlers execute on the same thread where post was called. For UI updates, use queue: .main in block-based addObserver or DispatchQueue.main.async inside the handler. Combine publisher with receive(on:) also solves the thread problem.
Selector-based: crash EXC_BAD_ACCESS when sending a notification after observer deallocation. Block-based (iOS 9+): no leak thanks to weak reference, but the notification center continues to hold the block in memory until explicit removeObserver. It is recommended to always remove the observer in deinit or use the Token pattern for automatic management.
NotificationCenter is a broadcast mechanism for arbitrary events between unrelated components. KVO observes changes to a specific property of a specific object. KVO requires NSObject inheritance and automatically notifies on property changes via setter. NotificationCenter only notifies when post is explicitly called. For model observation, KVO or Combine is preferable.
One default center per application process. Additional centers can be created via NotificationCenter(), but in practice the shared default is used. Each center operates independently — post in one does not deliver to observers of another. For module isolation, use separate Name namespaces via reverse-domain notification names.
Partially. Combine provides NotificationCenter.Publisher, which wraps NotificationCenter into a reactive stream. Combine solves the synchrony problem (via receive(on:)), adds transformation operators, and automatic subscription management (AnyCancellable). However, NotificationCenter remains for iOS system notifications (UIApplication, UIKeyboard) and legacy code. Combine is an enhancement, not a replacement.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also