NotificationCenter — це системний механізм iOS для надсилання та отримання сповіщень між компонентами застосунку без прямого зв'язку між відправником та отримувачем. Заснований на паттерні Observer, NotificationCenter дозволяє об'єктам підписуватися на події та реагувати на них асинхронно. За даними Apple Documentation (2025), NSNotificationCenter підтримує як синхронне надсилання сповіщень через post(name:object:), так і відкладене через NotificationQueue. Центр сповіщень працює в межах одного процесу та не перетинає меж застосунків.
Головне
NotificationCenter (NSNotificationCenter) — це вбудований механізм iOS для реалізації слабкозв'язаної комунікації між об'єктами. Паттерн Observer дозволяє одному об'єкту (відправнику) сповіщати безліч інших об'єктів (спостерігачів) про настання події без прямого посилання на них. NotificationCenter оперує трьома сутностями: Notification.Name (ідентифікатор сповіщення), Notification (контейнер з даними) та NotificationCenter (диспетчер). Кожен застосунок має спільний default center.
Notification.Name — це структура, яка ідентифікує тип сповіщення. Створюється через extension Name: Notification.Name(“MyNotification”). Notification — це об'єкт, що містить name, object (відправник) та userInfo (словник з даними). Системні сповіщення оголошені як константи: UIApplication.didBecomeActiveNotification, UIResponder.keyboardWillShowNotification. Користувацькі сповіщення слід групувати через extension для уникнення колізій імен. Імена мають бути зворотно-доменними.
// Визначення кастомних сповіщень
extension Notification.Name {
static let dataDidUpdate =
Notification.Name("com.app.dataDidUpdate")
static let userLoggedOut =
Notification.Name("com.app.userLoggedOut")
}
// Надсилання сповіщення з даними
let userInfo: [String: Any] = [
"userId": 123,
"timestamp": Date()
]
NotificationCenter.default.post(
name: .dataDidUpdate,
object: nil,
userInfo: userInfo
)
Спостерігач підписується на сповіщення через метод addObserver(_:selector:name:object:). Selector — метод, який буде викликано при отриманні сповіщення. Параметр object дозволяє фільтрувати сповіщення від конкретного відправника. Якщо object дорівнює nil, спостерігач отримує всі сповіщення з вказаним іменем від будь-яких відправників. Починаючи з iOS 9, addObserver не вимагає ручного видалення для block-based API, але selector-based все ще потребує removeObserver.
// Підписка на сповіщення (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)
}
// Підписка на сповіщення (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 зберігає таблицю відображення (ім'я → набір спостерігачів). Коли відправник викликає post(name:object:), центр сповіщень синхронно обходить всіх спостерігачів, підписаних на це ім'я, та викликає їх селектори або блоки. Ключова особливість: post блокує поточний потік до завершення всіх обробників. Якщо обробники виконують важкі операції, це затримує відправника. NotificationQueue вирішує цю проблему, відкладаючи доставку сповіщень.
Метод post(name:object:userInfo:) надсилає сповіщення негайно всім спостерігачам. Виклик синхронний — код після post виконується тільки після завершення всіх обробників. Порядок виклику спостерігачів не гарантований і може змінюватися між запусками. Для послідовної обробки використовуйте NotificationQueue з coalescing. Не викликайте post всередині обробника того ж сповіщення — це призводить до нескінченної рекурсії.
NotificationQueue додає сповіщення до черги для асинхронної доставки. Підтримує coalescing (об'єднання однакових сповіщень) та вибір черги доставки (asap, idle, modal). Coalescing корисний для частих подій (прогрес завантаження), коли потрібно сповістити лише останнім значенням. NotificationQueue використовує run loop для спрацьовування, тому працює тільки в потоках з активним run loop.
// Відкладене надсилання через NotificationQueue
let notification = Notification(
name: .dataDidUpdate,
object: self,
userInfo: ["progress": 0.5]
)
// Coalescing: кілька сповіщень об'єднуються в одне
NotificationQueue.default.enqueue(
notification,
postingStyle: .whenIdle,
coalesceMask: .onName,
forModes: [.common]
)
// Асинхронна доставка через DispatchQueue
DispatchQueue.main.async {
NotificationCenter.default.post(name: .dataDidUpdate, object: nil)
}
iOS надає три основні механізми для комунікації між об'єктами: NotificationCenter, Delegate та KVO (Key-Value Observing). Кожен вирішує задачу сповіщення, але з різними компромісами щодо зв'язаності, продуктивності та типової безпеки. Вибір механізму залежить від відношення «один-до-одного» чи «один-до-багатьох» та необхідності передачі даних.
| Характеристика | NotificationCenter | Delegate | KVO |
|---|---|---|---|
| Зв'язаність | Слабка (ім'я сповіщення) | Сильна (протокол) | Середня (ключ) |
| Відношення | Один-до-багатьох | Один-до-одного | Один-до-багатьох |
| Типова безпека | Низька (userInfo як Dictionary) | Висока (методи протоколу) | Середня (Any?) |
| Продуктивність | Середня (обхід таблиці) | Висока (прямий виклик) | Низька (NSObject) |
| Асинхронність | Синхронно (post блокує) | Синхронно в потоці відправника | Синхронно при зміні |
NotificationCenter ідеальний для подій, на які повинні реагувати кілька незалежних компонентів. Приклади: зміна налаштувань застосунку, вихід користувача з облікового запису, отримання push-сповіщення у фоновому режимі. NotificationCenter також підходить для слабко зв'язаних модулів (функція A не повинна знати про функцію B). Недолік — відсутність типової безпеки: ключі userInfo — рядки, а не enum.
Delegate обирайте для відношення один-до-одного з чітким контрактом (tableView.delegate). Delegate швидший і безпечніший за типами. KVO обирайте для спостереження за зміною конкретної властивості моделі (isLoading, progress). KVO вимагає успадкування від NSObject і може викликати труднощі з налагодженням (магічні рядки ключів). У сучасному Swift Combine та async sequences замінюють усі три підходи.
Метод addObserver підтримує два варіанти підписки: selector-based (традиційний) та block-based (із замиканням). Selector-based вимагає @objc-сумісності та ручного видалення спостерігача. Block-based (iOS 9+) дозволяє використовувати capture list та автоматично керується OS при використанні блоків без сильних посилань. Block-based також підтримує queue — спостерігач отримує сповіщення у вказаній черзі.
Традиційний спосіб підписки через селектор. Метод-обробник повинен бути позначений @objc та приймати опціональний Notification. Перевага: можливість використання будь-яким класом, включаючи legacy Objective-C. Недоліки: відсутність типової безпеки селектора, ризик помилок у назві селектора, обов'язковий removeObserver в deinit. При видаленні спостерігача раніше об'єкта обробник не буде викликаний.
Block-based API приймає замикання, яке виконується при отриманні сповіщення. Параметр queue визначає, в якій черзі виконується блок — головній черзі для оновлень UI або фоновій черзі для обробки даних. Значення, що повертається NSObjectProtocol, використовується для видалення спостерігача: NotificationCenter.default.removeObserver(observer). Block-based переважніший у сучасному 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)
}
}
// Використання з автоматичним видаленням
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() }
}
}
Витоки пам'яті — одна з головних проблем при роботі з NotificationCenter. Якщо спостерігач не видалений до деалокації, при надсиланні сповіщення центр спробує викликати метод вже звільненого об'єкта, що призведе до EXC_BAD_ACCESS. Починаючи з iOS 9, block-based addObserver використовує слабкі посилання, але selector-based все ще потребує ручного removeObserver. Найкраща практика: видаляти спостерігача в deinit.
Selector-based: обов'язково викликайте NotificationCenter.default.removeObserver(self) в deinit. Якщо спостерігач підписаний на кілька сповіщень, можна видалити всі одразу (без параметрів) або конкретне за іменем. Block-based: видаліть через removeObserver з токеном, отриманим від addObserver. Для block-based на iOS 9+ витік не відбувається, але видалення все одно рекомендується для продуктивності: звільнені спостерігачі не будуть обходитися при 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-паттерн автоматизує управління спостерігачами. При підписці повертається об'єкт-токен (NSObjectProtocol), який при деалокації автоматично видаляє спостерігача. NotificationTokenWrapper зберігає слабке посилання на NotificationCenter та токен спостерігача, викликаючи removeObserver в deinit. Це наближає NotificationCenter до Combine-підходу, де AnyCancellable керує життєвим циклом підписки.
Thread safety: NotificationCenter гарантує, що post може бути викликаний з будь-якого потоку, і всі спостерігачі отримають сповіщення в тому ж потоці, де був викликаний post. Це критично важливо для багатопотокових застосунків: якщо сповіщення надіслано з фонового потоку, обробники також виконаються в фоновому потоці. Для оновлення UI необхідно диспетчеризувати обробку на головну чергу через DispatchQueue.main.async.
NotificationCenter потокобезпечний для викликів post та addObserver з різних потоків. Внутрішня синхронізація використовує блокування, тому часті post з кількох потоків можуть створювати contention. Для високонавантажених сценаріїв (прогрес завантаження 1000 файлів) використовуйте окрему чергу сповіщень або Combine publisher. NotificationQueue з postingStyle .now еквівалентний прямому post.
NotificationCenter підтримує Combine publisher через NotificationCenter.default.publisher(for:object:). Publisher перетворює кожне сповіщення на подію Combine, яку можна трансформувати через map, filter, debounce та throttle. Це вирішує проблему синхронної доставки: Combine обробляє сповіщення асинхронно на вказаному Scheduler. NotificationCenter.publisher — міст між legacy-механізмом та сучасним реактивним програмуванням.
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)
}
}
Часті запитання
Так, NotificationCenter потокобезпечний для викликів post та addObserver з будь-яких потоків. Однак обробники виконуються в тому ж потоці, де був викликаний post. Для оновлення UI використовуйте queue: .main в block-based addObserver або DispatchQueue.main.async всередині обробника. Combine publisher з receive(on:) також вирішує проблему потоку.
Selector-based: crash EXC_BAD_ACCESS при надсиланні сповіщення після деалокації спостерігача. Block-based (iOS 9+): витоку немає завдяки слабкому посиланню, але центр сповіщень продовжує зберігати блок у пам'яті до явного removeObserver. Рекомендується завжди видаляти спостерігача в deinit або використовувати Token-паттерн для автоматичного керування.
NotificationCenter — широкомовна розсилка довільних подій між непов'язаними компонентами. KVO — спостереження за зміною конкретної властивості конкретного об'єкта. KVO вимагає успадкування NSObject та автоматично сповіщає при зміні властивості через setter. NotificationCenter сповіщає тільки при явному виклику post. Для спостереження за моделлю переважніший KVO або Combine.
Один default center на процес застосунку. Додаткові центри можна створити через NotificationCenter(), але на практиці використовується спільний default. Кожен центр працює незалежно — post в одному не доставляється спостерігачам іншого. Для ізоляції модулів використовуйте окремі Name-простори через зворотно-доменні імена сповіщень.
Частково. Combine надає NotificationCenter.Publisher, який обертає NotificationCenter у реактивний потік. Combine вирішує проблему синхронності (через receive(on:)), додає оператори трансформації та автоматичне керування підписками (AnyCancellable). Однак NotificationCenter залишається для системних сповіщень iOS (UIApplication, UIKeyboard) та legacy-коду. Combine — надбудова, а не заміна.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також