iosdev

Type-safe Notification handling in Swift

“Avoid notification.object and userInfo”

Cocoa notifications are one of those unavoidable Cocoa pain points in Swift’s strongly-typed world. If you are like me and can’t stand the sight of Any in your code, then notification.userInfo must be driving you mad.

It’s quite understandable why that’s typed as [AnyHashable: Any] – you should be able to add any type of information into that dictionary. For the iOS SDK stuff you just can’t avoid it as it comes to you packaged as userInfo and the best you can do is create your own type with init?(dictionary: [AnyHashable: Any?] or something similar. Then one or two guard lets and you’re ok.

However, for notifications you declare, post and handle in your own app, things can be much different.

All the info bits you would add to userInfo are certainly properly typed on the posting side; the ideal solution would be to wrap both object and userInfo into a particular custom type and then access the instance of that type directly in the notification handler.

That is the gist of the Notifying micro-library from my Swift Essentials pack. It’s entirely based on the approach described in episodes 27 and 28 of the Swift Talk.

(side note: if you are not subscribed to Swift Talk, you are missing out a lot. Chris and Florian are amazing.)

Usage

Let’s say you have an AccountManager instance in your app, which keeps track of the currently active Account (logged-in customer or similar).

final class Account {
	let accountId: Int
	let name: String
}

final class AccountManager {
	var account: Account?
}

When the login is successful and account details are loaded, you want to broadcast that info to the rest of the app, passing the accountId in the userInfo.

The regular, old Cocoa

Regular Cocoa way would be something like this:

extension Notification.Name {
	static let AccountManagerDidLogin = "AccountManagerDidLoginNotification"
}

extension AccountManager {
	func login(...) {
		...
	
		let name = Notification.Name(AccountManagerDidLogin)
		let userInfo: [String: Int] = ["accountId": account.accountId]
		let notification = Notification(name: name, object: self, userInfo: userInfo)
		NotificationCenter.default.post(notification)
	}
}

On the receiving side, it would be:

let nc = NotificationCenter.default

nc.addObserver(forName: NSNotification.Name.AccountManagerDidLogin, object: nil, queue: .main) {
	[weak self] notification in
	guard let `self` = self else { return }
	//	validate...
	guard
		let am = notification.object as? AccountManager,
		let userInfo = notification.userInfo as? [String: Int]
	else { return }
	//	process...
}

This is tedious and very error-prone in the long run as you need to keep both sides type-happy.

Improvement: custom payload type

One way to improve this – and the only real improvement you can do with system-originating notifications – is to create a custom type for each kind of notification payload you are interested in.

extension AccountManager {
	struct LoginPayload {
		let object: AccountManager
		let accountId: Int
		
		init?(_ notification: Notification) {
			...
		}
	}
}

And then just try to build that object in the handler:

nc.addObserver(forName: NSNotification.Name.AccountManagerDidLogin, object: nil, queue: .main) {
	[weak self] notification in
	guard let `self` = self else { return }
	//	validate...
	guard
		let payload = AccountManager.LoginPayload(notification)
	else { return }
	//	process...
}

And this way you have the single-point of truth for any number of notification handlers.

Notifying way

Now, look closely to the LoginPayload. If we have the object property already, we don’t really need userInfo at all - we can directly access the logged-in Account using simply payload.object.account. Thus our notification is simplified since we only need the originating object.

Notifying is abstracting away the Notification itself in a way that it gives you the object as the handler argument. So instead of:

[weak self] notification in

you will have:

[weak self] accountManager in

Notifying also makes sure that the object is kept in memory, in case it’s something short-lived and not AccountManager which is likely a singleton instance. Plus it makes sure that notification observer properly de-registers itself.

For detailed explanation of the NotificationToken and NotificationDescriptor I will refer you to the Swift Talk episode 28. Here’s how the didLogin would be declared with Notifying:

extension AccountManager {
	enum Notify: String {
		case didLogin = "AccountManagerDidLoginNotification"

		var name: Notification.Name {
			return Notification.Name(rawValue: self.rawValue)
		}

		//	Descriptors

		static let didLoginDescriptor = NotificationDescriptor<DataManager>(name: didLogin.name)
	}
}

This is how you post it:

extension AccountManager {
	func login(...) {
		...
		let nc = NotificationCenter.default
		nc.post(Notify.didLoginDescriptor, value: self)
	}	
}

And this is how you receive and handle it:

final class ViewController: UIViewController {
	var tokenDidLogin: NotificationToken?
	
	override func viewDidLoad() {
		super.viewDidLoad()

		setupNotificationTokens()
	}
}

fileprivate extension ViewController {
	func setupNotificationTokens() {
		tokenDidLogin = nc.addObserver(for: AccountManager.Notify.didLoginDescriptor, queue: .main){
			[weak self] accountManager in
			guard let `self` = self else { return }

			//	process...
		}
	}
}

This is clean, 100% strongly-typed and it allows extraction into some protocol / protocol extension, to keep the code DRY.

I have been using this approach in multiple projects in the last 6+ months with great success.

You can pickup just the Notifying micro-library or the whole Swift Essentials pack.