Skip to content

Notifable Protocol and the NotificationCenter

Isabella edited this page Aug 6, 2025 · 1 revision

Any type which needs to respond to NSNotificationCenter notifications should conform to the Notifiable protocol. This type provides some threading protections that will help prevent runtime crashes for types which also use Swift Concurrency.

Note: Swift 6.2 adds additional protections around NSNotificationCenter, especially for threading with Swift Concurrency, so we may be able to retire the Notifiable protocol at a later time.

Why conform to Notifiable?

The Notifiable protocol ensures that the @objc #selector method (which fires in response to posted notifications) is nonisolated. This is because (as of 2025 with our Swift 6.0 code migration) it is possible to annotate the @objc method used in a #selector with @MainActor to please the compiler.

However, at run time, the #selector method will run on the same thread as the notification was posted, violating @MainActor promises.

As an unrelated benefit, using Notifiable means you do not have to worry about removing your notification observers before your type is deallocated.

How to use Notifiable

Step 1: Conform to Notifiable

Your type should conform to Notifiable.

class MyType: Notifiable { 
	...
}

Upon initialization of your type, you should also pass in a NotificationProtocol, which will be used by the Notifiable methods. This is a thin wrapper for NotificationCenter.default which allows unit tests to mock out the notification center.

class MyType: Notifiable { 
	...
	var notificationCenter: NotificationProtocol
	
	init(notificationCenter: NotificationProtocol = NotificationCenter.default) {
            ...
            self.notificationCenter = notificationCenter
        }
	
	...
}

Step 2: Set up your notification listener

Register for the notifications your type wishes to listen to.

Most likely, you will call this in your type's init or viewDidLoad.

startObservingNotifications(
            withNotificationCenter: notificationCenter,
            forObserver: self,
            observing: [UIApplication.didBecomeActiveNotification,
                        UIApplication.willResignActiveNotification,
                        UIApplication.didEnterBackgroundNotification]
        )

Step 3: Implement the notification handler method

To conform to Notifiable, you must implement the handleNotifications() method.

When any of the registered notifications are posted, the Notifiable's handleNotifications() method will be called.

// MARK: - Notifiable

func handleNotifications(_ notification: Notification) {
	switch notification.name {
	case UIApplication.didEnterBackgroundNotification:
		ensureMainThread {
			if self.didTapButton {
				self.dismiss(animated: false)
				self.buttonTappedFinishFlow?()
			}
		}
	...
	...
	...
	default:
		break
	}
}

Note: This method inherits @objc and nonisolated from the Notifiable protocol definition:

@objc
nonisolated func handleNotifications(_ notification: Notification) {
	...
}

You can use a switch statement to handle each type of notification.

Threading in a nonisolated context inside handleNotifications()

For work that must happen on the main thread, you have two options:

  1. (Preferred) Use ensureMainThread { ... } to isolate your work to the main actor using GCD.
  2. Use Task { @MainActor in ... } to isolate your work with Swift Concurrency

Why is option 1 preferred? handleNotifications() will be called on the main thread if a notification is posted on the main thread. In that case, the ensureMainThread() method performs your UI updates on the same run loop. This is preferable to waiting for the next run loop in certain situations where the timing of events might be important in our legacy code base.

Step 4: Do we need to remove notification listeners?

No. Since Notifiable uses #selector variant of the NotificationCenter's addObserver() method, conforming types are not required to remove observers. See the documentation from Apple.

Clone this wiki locally