Fix: `addObserver` Not Receiving Notifications in Swift

Fix: `addObserver` Not Receiving Notifications in Swift

Written by

in

Quick answer: Use NotificationCenter.default.addObserver with a matching notification name and selector. The selector method must be exposed to Objective-C with @objc; for closure-based observers, keep the returned token and remove it when observation ends.

Why this happens

Swift uses the same NotificationCenter system as Objective-C, but its method and notification-name syntax is different. A common porting problem is to use the old Objective-C spelling, or to register a selector that does not match the Swift method’s signature.

Selector-based observers are called through the Objective-C runtime. The handler must therefore be Objective-C-compatible, and the selector passed to addObserver must identify the handler’s actual parameters. A notification name or object filter that does not match the posted notification can also make a correctly registered observer appear not to work.

How to fix it

1. Register a selector observer with Swift syntax

Use NotificationCenter.default and the Swift notification-name constant. The selector below includes the handler’s Notification parameter:

import UIKit

final class BatteryObserver {
    init() {
        UIDevice.current.isBatteryMonitoringEnabled = true

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(batteryLevelChanged(_:)),
            name: UIDevice.batteryLevelDidChangeNotification,
            object: nil
        )
    }

    @objc private func batteryLevelChanged(_ notification: Notification) {
        let level = UIDevice.current.batteryLevel
        print("Battery level: \(level)")
    }

    deinit {
        NotificationCenter.default.removeObserver(
            self,
            name: UIDevice.batteryLevelDidChangeNotification,
            object: nil
        )
    }
}

The @objc annotation makes the handler available to selector-based notification delivery. Keep the selector and method declaration in sync: #selector(batteryLevelChanged(_:)) refers to a method with one argument. If you change the method’s parameters, update the selector too.

For the battery example, enable monitoring before relying on battery-level values or notifications. isBatteryMonitoringEnabled is specific to UIDevice; do not add it for unrelated notifications. Battery values can be unavailable, so check for -1 before treating a level as a percentage.

2. Check the notification name and object filter

When porting this Objective-C registration:

[[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(batteryLevelChanged:)
    name:UIDeviceBatteryLevelDidChangeNotification
    object:nil];

use the Swift API and Swift name for the same notification:

NotificationCenter.default.addObserver(
    self,
    selector: #selector(batteryLevelChanged(_:)),
    name: UIDevice.batteryLevelDidChangeNotification,
    object: nil
)

object: nil means the observer does not require a particular notification sender. If you pass an object instead, the notification is delivered only when its sender matches that object. For custom notifications, the name used to register must be exactly the name used to post; spelling and capitalization matter.

3. Use a closure observer when you do not need a selector

The block-based API avoids @objc and lets you specify the queue on which the closure runs. Store its returned token so you can unregister it:

import UIKit

final class BatteryObserver {
    private var observer: NSObjectProtocol?

    init() {
        UIDevice.current.isBatteryMonitoringEnabled = true

        observer = NotificationCenter.default.addObserver(
            forName: UIDevice.batteryLevelDidChangeNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            self?.batteryLevelChanged(notification)
        }
    }

    private func batteryLevelChanged(_ notification: Notification) {
        let level = UIDevice.current.batteryLevel
        print("Battery level: \(level)")
    }

    deinit {
        if let observer = observer {
            NotificationCenter.default.removeObserver(observer)
        }
    }
}

Use a weak capture when the observer object owns the token and the closure calls back into that object. This avoids a retain cycle. The .main queue is useful when the handler updates UI; choose a different queue only if your work is safe to perform there.

Do not confuse the two cleanup forms: selector-based registration is removed by passing the observer object to removeObserver, while block-based registration is removed by passing the token returned from addObserver.

4. Post and receive a custom notification consistently

For a custom notification, define one reusable name instead of repeating string literals in different parts of the app:

import Foundation

extension Notification.Name {
    static let settingsDidChange = Notification.Name("settingsDidChange")
}

Post it with the same name:

NotificationCenter.default.post(
    name: .settingsDidChange,
    object: nil,
    userInfo: ["source": "settings"]
)

A selector observer can register for that name in the same way as the battery example. Its handler can inspect the payload using notification.userInfo. The sender can also be supplied as object; if the observer filters on a sender, make sure the posted notification uses that same object.

How to prevent it

  • Use NotificationCenter.default and Swift notification-name constants when writing Swift.
  • Match the selector exactly to the handler’s parameter list, and mark selector handlers @objc.
  • Confirm that the registered notification name and any object filter match the notification being posted.
  • Retain and remove the token for block-based observers; remove selector registrations when their owning object no longer needs them.
  • Enable UIDevice battery monitoring before depending on battery-level notifications or readings.

FAQ

Do I need @objc on a notification handler?

Yes, for a method used as a selector, mark the handler @objc so it can be called through the Objective-C runtime. A closure-based addObserver(forName:object:queue:using:) handler does not require @objc.

How do I remove an observer in Swift?

For selector-based registration, call NotificationCenter.default.removeObserver(self) or use the overload with a name and object. For block-based registration, pass the token returned by addObserver to removeObserver.

Why does my battery-level observer not fire?

Check that UIDevice.current.isBatteryMonitoringEnabled is true, that you registered for UIDevice.batteryLevelDidChangeNotification, and that the selector matches the handler. Also verify that your test setup produces a battery-level change; registration alone does not cause the notification to be posted.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.