Avatar ·

Как правильно использовать NSNotificationCenter addObserver в Swift?

Всем привет! Я новичок в Swift и пытаюсь разобраться с механизмом уведомлений. Мне нужно, чтобы при изменении настроек в приложении (например, смене языка) об этом узнавали все активные экраны. Я слышал, что для этого используется `NSNotificationCenter`, но у меня возникают проблемы с его использованием. Вот мой код: ```swift // Отправка уведомления NSNotificationCenter.defaultCenter().postNotificationName("LanguageChanged", object: nil) // Подписка на уведомление (в viewDidLoad) NSNotificationCenter.defaultCenter().addObserver(self, selector: "languageChanged:", name: "LanguageChanged", object: nil) // Обработчик func languageChanged(notification: NSNotification) { print("Language changed!") } ``` Но Xcode выдаёт предупреждение: `'addObserver:selector:name:object:' is deprecated`. Я также заметил, что если я не удаляю наблюдателя в `deinit`, приложение иногда падает. Подскажите, пожалуйста: 1. Как правильно подписываться на уведомления в современном Swift? 2. Нужно ли обязательно удалять наблюдателя? 3. Что делать с предупреждением о deprecation? Буду благодарен за примеры кода! <<>>

Проблема и её решение

Вы столкнулись с типичной ситуацией: используете старый API из Objective-C, который в Swift считается устаревшим. Начиная с iOS 9 и Swift 3, Apple рекомендует использовать addObserver(forName:object:queue:using:) или, что ещё лучше, современные подходы на основе замыканий (closures) и NotificationCenter.

Правильный способ в современном Swift

Вот как должен выглядеть ваш код:

import UIKit

class ViewController: UIViewController {

    private var notificationObserver: NSObjectProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Подписка через замыкание (рекомендуемый способ)
        notificationObserver = NotificationCenter.default.addObserver(
            forName: NSNotification.Name("LanguageChanged"),
            object: nil,
            queue: .main
        ) { [weak self] notification in
            self?.languageChanged(notification)
        }
    }

    private func languageChanged(_ notification: Notification) {
        print("Language changed!")
        // Здесь обновляем UI
    }

    deinit {
        // Обязательно удаляем наблюдателя
        if let observer = notificationObserver {
            NotificationCenter.default.removeObserver(observer)
        }
    }
}

Ключевые моменты

  • Замыкание вместо селектора: Используйте addObserver(forName:object:queue:using:) — он принимает замыкание, а не селектор. Это типобезопасно и избавляет от строковых констант.
  • weak self: Обязательно используйте [weak self] в замыкании, чтобы избежать цикла сильных ссылок (retain cycle).
  • Хранение ссылки на наблюдателя: Метод возвращает объект типа NSObjectProtocol, который нужно сохранить (например, в свойстве класса).
  • Удаление в deinit: Вы обязаны удалить наблюдателя в deinit. Иначе, если объект будет уничтожен, но наблюдатель останется, при следующей отправке уведомления произойдёт обращение к освобождённой памяти (crash).

Альтернатива: блоки и селекторы

Если вам нужна обратная совместимость со старым кодом, можно использовать селектор, но с модификацией:

NotificationCenter.default.addObserver(
    self,
    selector: #selector(languageChanged(_:)),
    name: NSNotification.Name("LanguageChanged"),
    object: nil
)

@objc func languageChanged(_ notification: Notification) {
    print("Language changed!")
}

Здесь важно:

  • Добавить @objc перед функцией-обработчиком.
  • Использовать #selector вместо строки.
  • Всё равно удалять наблюдателя в deinit.

Отправка уведомления

Отправка тоже изменилась:

// Старый способ (ещё работает, но не рекомендуется)
NSNotificationCenter.defaultCenter().postNotificationName("LanguageChanged", object: nil)

// Современный способ
NotificationCenter.default.post(name: NSNotification.Name("LanguageChanged"), object: nil)

// Можно добавить userInfo для передачи данных
let userInfo: [String: Any] = ["language": "ru"]
NotificationCenter.default.post(
    name: NSNotification.Name("LanguageChanged"),
    object: nil,
    userInfo: userInfo
)

Итоговые рекомендации

  • Всегда используйте NotificationCenter.default.addObserver(forName:object:queue:using:) — это современный, безопасный и читаемый подход.
  • Никогда не забывайте удалять наблюдателя в deinit. Лучше всего использовать свойство для хранения ссылки на наблюдателя.
  • Для строковых имён уведомлений создавайте константы или используйте расширения, чтобы избежать опечаток.
  • Рассмотрите альтернативы: делегаты, KVO, Combine (начиная с iOS 13) или реактивные фреймворки (RxSwift).
<<>>

Использование Combine для уведомлений (iOS 13+)

Если ваше приложение поддерживает iOS 13 и выше, рекомендую использовать фреймворк Combine. Он предоставляет более декларативный и безопасный способ работы с уведомлениями.

import Combine
import UIKit

class ViewController: UIViewController {
    
    private var cancellable: AnyCancellable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        cancellable = NotificationCenter.default.publisher(
            for: NSNotification.Name("LanguageChanged")
        )
        .sink { [weak self] notification in
            self?.languageChanged(notification)
        }
    }
    
    private func languageChanged(_ notification: Notification) {
        print("Language changed via Combine!")
    }
    
    deinit {
        cancellable?.cancel()
    }
}

Преимущества Combine

  • Автоматическое управление памятью: При уничтожении объекта AnyCancellable подписка автоматически отменяется. Не нужно явно вызывать removeObserver.
  • Цепочки операторов: Можно легко фильтровать, трансформировать и комбинировать уведомления.
  • Потокобезопасность: Легко переключать получение уведомлений на главный поток с помощью .receive(on: DispatchQueue.main).

Однако, если вам нужна поддержка старых версий iOS, используйте первый вариант с замыканиями — он универсален и работает начиная с iOS 9.