NotificationCenter — to systemowy mechanizm iOS do wysyłania i odbierania powiadomień między komponentami aplikacji bez bezpośredniego połączenia między nadawcą a odbiorcą. Oparty na wzorcu Observer, NotificationCenter pozwala obiektom subskrybować zdarzenia i reagować na nie asynchronicznie. Według Apple Documentation (2025), NSNotificationCenter obsługuje zarówno synchroniczne wysyłanie powiadomień przez post(name:object:), jak i opóźnione przez NotificationQueue. Centrum powiadomień działa w ramach jednego procesu i nie przekracza granic aplikacji.
Najważniejsze
NotificationCenter (NSNotificationCenter) — to wbudowany mechanizm iOS do realizacji słabo powiązanej komunikacji między obiektami. Wzorzec Observer pozwala jednemu obiektowi (nadawcy) powiadamiać wiele innych obiektów (obserwatorów) o wystąpieniu zdarzenia bez bezpośredniego odwołania do nich. NotificationCenter operuje na trzech bytach: Notification.Name (identyfikator powiadomienia), Notification (kontener z danymi) i NotificationCenter (dyspozytor). Każda aplikacja ma współdzielone default center.
Notification.Name — to struktura identyfikująca typ powiadomienia. Tworzona przez extension Name: Notification.Name("MyNotification"). Notification — to obiekt zawierający name, object (nadawca) i userInfo (słownik z danymi). Systemowe powiadomienia są zadeklarowane jako stałe: UIApplication.didBecomeActiveNotification, UIResponder.keyboardWillShowNotification. Niestandardowe powiadomienia należy grupować przez extension, aby uniknąć kolizji nazw. Nazwy powinny być odwrotnie domenowe.
// Definiowanie niestandardowych powiadomień
extension Notification.Name {
static let dataDidUpdate =
Notification.Name("com.app.dataDidUpdate")
static let userLoggedOut =
Notification.Name("com.app.userLoggedOut")
}
// Wysyłanie powiadomienia z danymi
let userInfo: [String: Any] = [
"userId": 123,
"timestamp": Date()
]
NotificationCenter.default.post(
name: .dataDidUpdate,
object: nil,
userInfo: userInfo
)
Obserwator subskrybuje powiadomienie przez metodę addObserver(_:selector:name:object:). Selektor — metoda, która zostanie wywołana po otrzymaniu powiadomienia. Parametr object pozwala filtrować powiadomienia od konkretnego nadawcy. Jeśli object ma wartość nil, obserwator otrzymuje wszystkie powiadomienia o podanej nazwie od dowolnych nadawców. Od iOS 9 addObserver nie wymaga ręcznego usuwania dla block-based API, ale selector-based nadal wymaga removeObserver.
// Subskrypcja powiadomienia (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)
}
// Subskrypcja powiadomienia (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 przechowuje tabelę mapowania (name → zestaw obserwatorów). Gdy nadawca wywołuje post(name:object:), centrum powiadomień synchronicznie przegląda wszystkich obserwatorów subskrybujących tę nazwę i wywołuje ich selektory lub bloki. Kluczowa cecha: post blokuje bieżący wątek do czasu zakończenia wszystkich handlerów. Jeśli handlery wykonują ciężkie operacje, opóźnia to nadawcę. NotificationQueue rozwiązuje ten problem, opóźniając dostarczanie powiadomień.
Metoda post(name:object:userInfo:) wysyła powiadomienie natychmiast do wszystkich obserwatorów. Wywołanie jest synchroniczne — kod po post wykonuje się dopiero po zakończeniu wszystkich handlerów. Kolejność wywołania obserwatorów nie jest gwarantowana i może się zmieniać między uruchomieniami. Do sekwencyjnego przetwarzania używaj NotificationQueue z coalescing. Nie wywołuj post wewnątrz handlera tego samego powiadomienia — prowadzi to do nieskończonej rekurencji.
NotificationQueue dodaje powiadomienia do kolejki w celu asynchronicznej dostawy. Obsługuje coalescing (łączenie identycznych powiadomień) i wybór kolejki dostawy (asap, idle, modal). Coalescing jest przydatny w przypadku częstych zdarzeń (postęp ładowania), gdy trzeba powiadomić tylko ostatnią wartością. NotificationQueue używa run loop do wyzwalania, więc działa tylko w wątkach z aktywnym run loop.
// Opóźnione wysyłanie przez NotificationQueue
let notification = Notification(
name: .dataDidUpdate,
object: self,
userInfo: ["progress": 0.5]
)
// Coalescing: wiele powiadomień łączy się w jedno
NotificationQueue.default.enqueue(
notification,
postingStyle: .whenIdle,
coalesceMask: .onName,
forModes: [.common]
)
// Asynchroniczne wysyłanie przez DispatchQueue
DispatchQueue.main.async {
NotificationCenter.default.post(name: .dataDidUpdate, object: nil)
}
iOS udostępnia trzy główne mechanizmy komunikacji między obiektami: NotificationCenter, Delegate i KVO (Key-Value Observing). Każdy rozwiązuje zadanie powiadamiania, ale z różnymi kompromisami dotyczącymi powiązania, wydajności i bezpieczeństwa typów. Wybór mechanizmu zależy od relacji „jeden-do-jednego” lub „jeden-do-wielu” oraz potrzeby przesyłania danych.
| Cecha | NotificationCenter | Delegate | KVO |
|---|---|---|---|
| Powiązanie | Słabe (nazwa powiadomienia) | Silne (protokół) | Średnie (klucz) |
| Relacja | Jeden-do-wielu | Jeden-do-jednego | Jeden-do-wielu |
| Bezpieczeństwo typów | Niskie (userInfo jako Dictionary) | Wysokie (metody protokołu) | Średnie (Any?) |
| Wydajność | Średnia (przeglądanie tabeli) | Wysoka (bezpośrednie wywołanie) | Niska (NSObject) |
| Asynchroniczność | Synchronicznie (post blokuje) | Synchronicznie w wątku nadawcy | Synchronicznie przy zmianie |
NotificationCenter jest idealny dla zdarzeń, na które musi reagować kilka niezależnych komponentów. Przykłady: zmiana ustawień aplikacji, wylogowanie użytkownika, otrzymanie push powiadomienia w tle. NotificationCenter nadaje się również dla słabo powiązanych modułów (funkcja A nie musi wiedzieć o funkcji B). Wadą jest brak bezpieczeństwa typów: klucze userInfo to ciągi znaków, a nie enum.
Delegate wybieraj dla relacji jeden-do-jednego z jasnym kontraktem (tableView.delegate). Delegate jest szybszy i bezpieczniejszy typowo. KVO wybieraj do obserwowania zmiany konkretnej właściwości modelu (isLoading, progress). KVO wymaga dziedziczenia po NSObject i może sprawiać trudności w debugowaniu (magiczne ciągi kluczy). We współczesnym Swift Combine i async sequences zastępują wszystkie trzy podejścia.
Metoda addObserver obsługuje dwa warianty subskrypcji: selector-based (tradycyjny) i block-based (z domknięciem). Selector-based wymaga zgodności z @objc i ręcznego usuwania obserwatora. Block-based (iOS 9+) pozwala używać capture list i jest automatycznie zarządzany przez OS przy użyciu bloków bez silnych referencji. Block-based obsługuje również queue — obserwator otrzymuje powiadomienie w określonej kolejce.
Tradycyjny sposób subskrypcji przez selektor. Metoda-handler musi być oznaczona @objc i przyjmować opcjonalny Notification. Zaleta: możliwość użycia przez dowolną klasę, w tym legacy Objective-C. Wady: brak bezpieczeństwa typów selektora, ryzyko literówek w nazwie selektora, obowiązkowy removeObserver w deinit. Po usunięciu obserwatora wcześniej niż obiekt, handler nie zostanie wywołany.
Block-based API przyjmuje domknięcie, które wykonuje się po otrzymaniu powiadomienia. Parametr queue określa, w której kolejce wykonuje się blok — main queue dla aktualizacji UI lub background queue do przetwarzania danych. Zwracana wartość NSObjectProtocol jest używana do usunięcia obserwatora: NotificationCenter.default.removeObserver(observer). Block-based jest preferowany we współczesnym 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)
}
}
// Użycie z automatycznym usuwaniem
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() }
}
}
Wycieki pamięci — jeden z głównych problemów podczas pracy z NotificationCenter. Jeśli obserwator nie zostanie usunięty przed dealokacją, przy wysyłaniu powiadomienia centrum spróbuje wywołać metodę już zwolnionego obiektu, co doprowadzi do EXC_BAD_ACCESS. Od iOS 9 block-based addObserver używa słabych referencji, ale selector-based nadal wymaga ręcznego removeObserver. Best practice: usuwać obserwatora w deinit.
Selector-based: obowiązkowo wywołuj NotificationCenter.default.removeObserver(self) w deinit. Jeśli obserwator subskrybuje kilka powiadomień, można usunąć wszystkie naraz (bez parametrów) lub konkretne po nazwie. Block-based: usuń przez removeObserver z tokenem otrzymanym z addObserver. Dla block-based na iOS 9+ wyciek nie występuje, ale usunięcie jest nadal zalecane dla wydajności: zwolnieni obserwatorzy nie będą przeglądani przy 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() { }
}
Token-pattern automatyzuje zarządzanie obserwatorami. Przy subskrypcji zwracany jest obiekt-token (NSObjectProtocol), który przy dealokacji automatycznie usuwa obserwatora. NotificationTokenWrapper przechowuje słabą referencję do NotificationCenter i token obserwatora, wywołując removeObserver w deinit. To przybliża NotificationCenter do podejścia Combine, gdzie AnyCancellable zarządza cyklem życia subskrypcji.
Thread safety NotificationCenter gwarantuje, że post może być wywołany z dowolnego wątku, a wszyscy obserwatorzy otrzymają powiadomienie w tym samym wątku, w którym został wywołany post. Jest to krytyczne dla aplikacji wielowątkowych: jeśli powiadomienie zostało wysłane z wątku tła, handlery również wykonają się w wątku tła. Do aktualizacji UI należy zdysponować przetwarzanie na main queue przez DispatchQueue.main.async.
NotificationCenter jest bezpieczny wątkowo dla wywołań post i addObserver z różnych wątków. Wewnętrzna synchronizacja używa blokady, więc częste post z wielu wątków mogą tworzyć contention. Dla scenariuszy o wysokim obciążeniu (postęp ładowania 1000 plików) używaj osobnej kolejki powiadomień lub Combine publisher. NotificationQueue z postingStyle .now jest równoważny bezpośredniemu post.
NotificationCenter obsługuje Combine publisher przez NotificationCenter.default.publisher(for:object:). Publisher zamienia każde powiadomienie w zdarzenie Combine, które można transformować przez map, filter, debounce i throttle. To rozwiązuje problem synchronicznej dostawy: Combine przetwarza powiadomienia asynchronicznie na określonym Scheduler. NotificationCenter.publisher — to most między legacy mechanizmem a nowoczesnym programowaniem reaktywnym.
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)
}
}
Często zadawane pytania
Tak, NotificationCenter jest bezpieczny wątkowo dla wywołań post i addObserver z dowolnych wątków. Jednak handlery wykonują się w tym samym wątku, w którym został wywołany post. Do aktualizacji UI używaj queue: .main w block-based addObserver lub DispatchQueue.main.async wewnątrz handlera. Combine publisher z receive(on:) również rozwiązuje problem wątku.
Selector-based: crash EXC_BAD_ACCESS przy wysyłaniu powiadomienia po dealokacji obserwatora. Block-based (iOS 9+): nie ma wycieku dzięki słabej referencji, ale centrum powiadomień nadal przechowuje blok w pamięci do jawnego removeObserver. Zaleca się zawsze usuwać obserwatora w deinit lub używać Token-pattern do automatycznego zarządzania.
NotificationCenter — rozgłaszanie dowolnych zdarzeń między niepowiązanymi komponentami. KVO — obserwowanie zmiany konkretnej właściwości konkretnego obiektu. KVO wymaga dziedziczenia NSObject i automatycznie powiadamia przy zmianie właściwości przez setter. NotificationCenter powiadamia tylko przy jawnym wywołaniu post. Do obserwowania modelu preferowany jest KVO lub Combine.
Jedno default center na proces aplikacji. Dodatkowe centra można utworzyć przez NotificationCenter(), ale w praktyce używa się wspólnego default. Każde centrum działa niezależnie — post w jednym nie jest dostarczany obserwatorom drugiego. Do izolacji modułów używaj oddzielnych przestrzeni nazw Name przez odwrotnie domenowe nazwy powiadomień.
Częściowo. Combine udostępnia NotificationCenter.Publisher, który opakowuje NotificationCenter w strumień reaktywny. Combine rozwiązuje problem synchroniczności (przez receive(on:)), dodaje operatory transformacji i automatyczne zarządzanie subskrypcjami (AnyCancellable). Jednak NotificationCenter pozostaje dla systemowych powiadomień iOS (UIApplication, UIKeyboard) i legacy kodu. Combine to nadbudowa, a nie zamiennik.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również