How do you pass data in a notification safely?

2.4.3 · L2 · Learning

Predict first

What does this print?

let n = Notification(name: .init("x"), object: nil, userInfo: ["id": 1])
print(n.userInfo?["id"] as? Int as Any, n.userInfo?["id"] as? String as Any)
  • Optional(1) nil
  • Optional(1) Optional("1")
  • nil nil

Mental model. An untyped userInfo is a parcel with a handwritten label. A typed wrapper is the printed form that says which fields exist.

A Notification carries a name, an optional object (usually the sender) and an optional userInfo dictionary of type [AnyHashable: Any]. That dictionary is untyped: every read is a cast that can fail silently. Wrap it: define the name and the keys once, and give posters and observers a small typed helper so the cast lives in one place.

Name your notifications with a namespace ("com.app.didLogin") so they cannot collide, and declare them as static let on Notification.Name. Modern code often prefers notifications(named:) (an AsyncSequence) or Combine’s publisher(for:), and recent SDKs add typed, concurrency-aware notification messages, so check what your deployment target offers.

The cast to Int works; the cast to String fails quietly with nil. Nothing warns you.

Common traps

  • Casting the wrong type from userInfo. as? String on an Int yields nil, and the bug is silent.
  • Stringly-typed names. A typo in the literal makes the observer never fire.

They might follow up with

  • When is object better than userInfo? When the payload is the sender itself, such as the text field that changed.
  • Why not pass a whole model through userInfo? It hides a data dependency. Prefer an explicit API for that.

Related questions

  • 2.4.1 How does NotificationCenter deliver a notification, and on which thread?
  • 2.4.4 NotificationCenter, delegate, closure or KVO: how do you choose?

← How do you avoid leaks and crashes with notification observers?   NotificationCenter, delegate, closure or KVO: how do you choose? →