Notification in Swift (NSNotification)
In Swift, communication between entities in the app can be implemented in a few ways: Notification (NSNotification), Key-Value Observing (KVO), delegation, etc. In series of articles I will share how to select proper tool for a job and some best practices. This one is about Notification.
Notifications are often compared to delegates. You can find out more in my article Delegate in Swift.
Notification and Notification Center
Notification
object is a container for information related to a change or an event. Notifications are broadcasted using centralized instance of NotificationCenter
.
Notification
object, identified by name
, may contain reference to the object
that has sent the notification, and some arbitrary information in theuserInfo
dictionary.
NotificationCenter
registers observers and delivers notifications. Objects use NotificationCenter
instance to post and observe a notification. Each app (process) has a default notification center, accessible via NotificationCenter.default
property.
Note, on macOS DistributedNotificationCenter
can be used to communicate between processes.
For demo purpose I created the URLContainer
sample class that stores an array of URL
objects.
class URLContainer { private(set) var urls: [URL] = [] func add(_ url: URL) {
urls.append(url)
}
}
Creating Notification
To create a new notification we need a name. Names use the nested NSNotification.Name
type.
Under the hood names are actually strings, therefore it is a good practice to come up with unique names. Best practice is to declare a constant for the name. I prefer using name of the constant as a string, same convenience used in Cocoa/Cocoa Touch.
// URLContainerextension Notification.Name { static let URLContainerDidAddURL
= NSNotification.Name("URLContainerDidAddURL")}
URLContainerDidAddULR
is the name of the notification posted when change occurs. It is declared using extension on Notification.Name
. Benefit from adopting this approach is that compiler will warn you when trying to create a duplicate name.
When naming the notification include object name and meaningful description of the change. Use auxiliary verbs Will and Did to indicate if notification posted before or after the change. When not sure, look into existing names, just type Notification.Name
and browse over autocompletion results.
Note on Swift 4 and Swift 4.2 differences
Cocoa Touch in Swift 4 uses global NSNotification.Name
namespace to declare notification name constants:
// UIApplicationextension NSNotification.Name { public static let UIApplicationDidChangeStatusBarOrientation: NSNotification.Name
}
In Swift 4.2 constants are nested with type:
extension UIApplication { public class let didChangeStatusBarOrientationNotification: NSNotification.Name
}
String value stays the same - "UIApplicationDidChangeStatusBarOrientationNotification"
. However, the way we refer to it changes:
// Swift 4
Notification.Name.UIApplicationDidChangeStatusBarOrientation // Full
.UIApplicationDidChangeStatusBarOrientation // Short// Swift 4.2
UIApplication.didChangeStatusBarOrientationNotification
In Swift 4.2 you must use Type.constantName
syntax. This makes it more structured, but we loose short syntax and ability to introspect all notifications on theNotification.Name
type.
Posting Notification
Most convenient way to post a notification is usingNotificationCenter
convenience method.
func post(name aName: NSNotification.Name,
object anObject: Any?,
userInfo aUserInfo: [AnyHashable : Any]? = nil)
You rarely need to create Notification
objects yourself. Instead provide notification center with the name, posting object, and user info dictionary.
class URLContainer { private(set) var urls: [URL] = [] func add(_ url: URL) {
urls.append(url)
NotificationCenter.default.post(
name: .URLContainerDidAddURL,
object: self,
userInfo: [URLContainer.urlKey : url])
} static let urlKey = "URL"
}
URLContainer
notifies when new url was added. userInfo
dictionary contains the new url under "URL"
key. Good practice is to declare constants for userInfo
dictionary keys.
Typically object
argument is the object that posts the notification. In special cases it may be nil
. Examples are system notifications like clock or time zone change. The best practice make sure it is the object that posts the notification is to always pass self
. This way notifications will only be posted from one of the object methods and not external code.
Observing Notification using Selector
One way to observe notifications is to add the observer and its selector to NotificationCenter
.
func addObserver(_ observer: Any,
selector aSelector: Selector,
name aName: NSNotification.Name?,
object anObject: Any?)
NotificationCenter
expects the name of the notification and the object whose notifications the observer wants to receive.
This approach is similar to the target-action pattern you see in UI controls.
class Controller { func subscribe(for container: URLContainer) {
NotificationCenter.default.addObserver(self,
selector: #selector(urlContainerDidChange(_:)),
name: .URLContainerDidAddURL,
object: container)
} @objc
func urlContainerDidAddURL(_ notification: Notification) {
let url = notification.userInfo?[URLContainer.urlKey]
print(url)
}}
In this example Controller
object responds to the URLContainerDidAddURL
notification with urlContainerDidAddURL(_:)
method.
The observer method must have one argument (an instance of Notification
). Method without arguments still works. But this is rather a side effect and documentation explicitly requires to follow the signature.
Even when you’re not interested in notification object itself, but the fact of the event, Notification
instance as an argument helps to separate and search notification handling methods in code. So do keep Notification
argument in notification handling methods as a best practice.
Another useful practice to adopt is using name of the notification as the method name: URLContainerDidAddURL
- urlContainerDidAddURL(_:)
. This small trick will help you quick search for methods that respond to the notification.
When we’re no longer interested in observing the notifications, the observer can be removed from NotificationCenter
.
func removeObserver(_ observer: Any,
name aName: NSNotification.Name?,
object anObject: Any?)
For this you need the observer object, name of the notification, and the object whose notifications the observer receiving.
class Controller { func unsubscribe(from container: URLContainer) {
NotificationCenter.default.removeObserver(self,
name: .URLContainerDidAddURL,
object: container)
}}
Here, Observer
object stops responding to URLContainerDidAddURL
notification.
Note, prior to iOS 9 and macOS 10.11 we must remove observers by calling this method before objects are deallocated.
Observing Notification using Closure
NotificationCenter
provides closure API to observe notifications.
func addObserver(forName name: NSNotification.Name?,
object obj: Any?,
queue: OperationQueue?,
using block: @escaping (Notification) -> Void) -> NSObjectProtocol
Like with previous method, NotificationCenter
expects the name of the notification and the object whose notifications the observer wants to receive. But this time, closure reacts to the notification, and the object that acts as the observer is returned from the method.
class Controller { private var observer: AnyObject? func subscribe(for container: URLContainer) {
guard observer == nil else { return }
observer = NotificationCenter.default.addObserver(
forName: .URLContainerDidAddURL,
object: container,
queue: nil) { notification in let url = notification.userInfo?[URLContainer.urlKey]
print(url) }
} func unsubscribe() {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
self.observer = nil
}
} deinit {
unsubscribe()
}}
In this example responding to URLContainerDidAddURL
is done using closure and Controller
object stores the observer instance.
You must remove observers by invoking removeObserver(_:)
or removeObserver(_:name:object:)
before objects are deallocated.
The closure is retained by the notification center until the observer registration is removed. Naturally the closure is escaping. This may create a strong reference cycle.
observer = NotificationCenter.default.addObserver(
forName: .URLContainerDidAddURL,
object: container,
queue: nil) { [weak self] notification in guard let `self` = self else {
return
} self.processNotification(notification) }
}
In my example the observer is removed in deinit
method, but with strong reference cycle it will never happen. I use [weak self]
to break strong reference cycle.
General rule of thumb with weak references in closures is to capture strong reference before the closure routine. This is important because the weak reference can become nil
at any moment. This may lead to undefined behaviour and unexpected side effects. I use next pattern for convenience:
guard let `self` = self else {
return
}
Multiple Subscriptions
Multiple subscriptions will trigger the responding selector multiple times. But when unsubscribing — all observer selectors will be removed.
NotificationCenter.default.addObserver(self,
selector: #selector(urlContainerDidChange(_:)),
name: .URLContainerDidAddURL,
object: container)NotificationCenter.default.addObserver(self,
selector: #selector(urlContainerDidChange(_:)),
name: .URLContainerDidAddURL,
object: container)container.add(URL(string: "https://developer.apple.com")!)NotificationCenter.default.removeObserver(self,
name: .URLContainerDidAddURL, object: container)container.add(URL(string: "https://itunesconnect.apple.com")!)
Output:
https://developer.apple.com
https://developer.apple.com
In this example urlContainerDidChange(_:)
will be triggered two times for the first URL and will not fire for the second URL.
Probably not what we expect. Usually we need to unsubscribe from the previous object before subscribing to the new one. This is especially simple when object is stored as a property.
class Controller { var container: URLContainer? {
willSet {
if let container = container {
unsubscribe(from: container)
}
} didSet {
if let container = container {
subscribe(for: container)
}
}
}
Using willSet/didSet
(Property Observers) with helper subscribe(for:)/unsubscribe(from:)
methods.
Therefore, another good practice is to create separate methods for subscribing and unsubscribing to notifications.
When using closure we need to take extra care. Because if we overwrite previous observer — it can never be removed. But it may actually be simpler to control.
private var observer: AnyObject?func subscribe(for container: URLContainer) {
guard observer == nil else {
assertionFailure("Previous observer must be removed before add new one")
return
} observer = NotificationCenter.default.addObserver(
forName: .URLContainerDidAddURL,
object: container,
queue: nil) { notification in let url = notification.userInfo?[URLContainer.urlKey]
print(url) }
}
Here we validate if previous observer was removed before adding new one. Assertion will help us to catch this bugs during testing.
Bulk Subscriptions
One of the interesting feature of NotificationCenter
is ability to subscribe for all notifications from a certain object; for a certain notification from all objects; or basically for all notifications in the notification center.
// Any notifications from container
let observer1 = NotificationCenter.default.addObserver(
forName: nil,
object: container,
queue: nil) { notification in
}// URLContainerDidChange notifications from all objects
let observer2 = NotificationCenter.default.addObserver(
forName: .URLContainerDidChange,
object: nil,
queue: nil) { notification in
}// All notifications from all objects in localNotificationCenter
let observer3 = localNotificationCenter.addObserver(
forName: nil,
object: nil,
queue: nil) { notification in
}
All you need to do is to pass nil as one of the arguments. Same works for selector based subscriptions. This is useful when you don’t have a reference to the object posting the notification, when all notifications can be handled by one closure/method, or when using local notification center for certain parts of the app.
Same works for unsubscribing from notifications.
// Unsubscribe observer of URLContainerDidChange from all objects
NotificationCenter.default.removeObserver(observer,
name: .URLContainerDidChange, object: nil)// Unsubscribe observer from container object
NotificationCenter.default.removeObserver(observer,
name: nil, object: container)// Remove observer from notification center
NotificationCenter.default.removeObserver(observer)
Use this methods wisely. In general it is better to explicitly enumerate all notifications you’re interested in observing and specific objects. Otherwise you may end up with undefined behavior and side-effects.
When removing observer make sure you remove only notifications you explicitly subscribed to. Especially if you use inheritance and subclass/superclass can subscribe for notifications.
Notifications and Multithreading
When observing the notification that may occur on the secondary thread it is important to respond on the thread we expect. One example is updating UI when responding to model change that may happen in background.
Notification center deliver notifications on the thread in which the notification was posted. Redirecting notifications is possible and described in documentation. But the implementation is rather complicated and limited in several aspects.
When observing notification using closure we can specify OperationQueue
for the closure.
func addObserver(forName name: NSNotification.Name?,
object obj: Any?,
queue: OperationQueue?,
using block: @escaping (Notification) -> Void) -> NSObjectProtocol
Using DispatchQueue
with selectors is another simple solution for the complex problem.
class Controller { @objc
func urlContainerDidAddURL(_ notification: Notification) {
DispatchQueue.main.async {
// Update UI
}
}}
Notification Delivery
When using NotificationCenter
's post(name:object:userInfo:)
, the notification will be distributed synchronously. Notification center uses a queue (NotificationQueue
), that maintains notifications in a First In First Out (FIFO) order.
container.add(URL(string: "https://apple.com")!)
container.add(URL(string: "https://developer.apple.com")!)
container.add(URL(string: "https://itunesconnect.apple.com")!)Output:
https://apple.com
https://developer.apple.com
https://itunesconnect.apple.com
In this example notifications appear synchronously and in order.
This as well means, that before the posting object can resume its thread, it must wait until the notification center dispatches the notification to all observers.
As you can see from this stack trace, urlContainerDidAddURL(_:)
is executed synchronously.
But… notifications placed into the queue can be delayed until the end of the current pass through the run loop or until the run loop is idle. Notifications delivery can be changed using posting style (NotificationQueue.PostingStyle
) and can be performed asynchronously.
public enum PostingStyle : UInt {
/// The notification is posted at the end of the current notification callout or timer.
case whenIdle
/// The notification is posted when the run loop is idle.
case asap
/// The notification is posted immediately after coalescing.
case now
}
Notification and Architecture
Observation patterns are used in various architectures. Notifications are commonly used as a communication protocol in Model-View-Controller (MVC) architecture.
Notification and UIViewController
class ViewController: UIViewController {
let container = URLContainer()
private var observer: AnyObject? override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(
forName: .URLContainerDidAddURL,
object: container,
queue: OperationQueue.main)
{ [weak self] notification in
guard let `self` = self else {
return
} self.updateUI()
}
} deinit {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
}
}
In this example ViewController
updates UI as the response to the URLContainerDidAddURL
notification. Because UI must be updated on main queue, OperationQueue.main
used when adding observer. Reference to self
is weak and converted to strong inside the closure.
There are still some things to consider. Handling notification may be costly operation. Updating user interface, formatting values, etc. Maybe you need to download an image from the server. For the best performance it is better to avoid unnecessary work.
One option is to subscribe on appearance and unsubscribe when view disappears. This will remove unnecessary updates. But, you still need to update UI on appearance. I prefer to keep track of needed update:
private var needsUpdateViewOnAppearance = true
private var isVisible: Bool {
return isViewLoaded && view.window != nil
}override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(
forName: .URLContainerDidAddURL,
object: container,
queue: OperationQueue.main)
{ [weak self] notification in
guard let `self` = self else {
return
} if self.isVisible {
self.updateUI()
}
else {
self.needsUpdateViewOnAppearance = true
}
}
}override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needsUpdateViewOnAppearance {
updateUI()
needsUpdateViewOnAppearance = false
}
}
In this example needsUpdateViewOnAppearance
property to indicate when UI must be updated on appearance (in viewWillAppear(_:)
). When URLContainerDidAddURL
notification occur, we check if view is visible (by checking if it is in the window). If view is not visible, we set needsUpdateViewOnAppearance
property.
A bit more work, but performance gain can be huge.
Final Notes
Notification is easy to use and powerful mechanism. It works well in Model-View-Controller architecture and can be used to respond to system events.
With great power comes great responsibility.
Notification may look as a simple solution to complicated problems. But overuse of notifications may lead to complex dependencies between objects in the app, sometimes even separate modules.
When considering adding an observer, check if you have reference to the object that posts the notification. If the object is not accessible, instead of observing every notification, it might be better to solve the problem by adjusting your architecture.
When constructing data flows consider dependency injection and delegation as alternative to notifications.
In many cases, when notification looks like the only solution for the problem — it is a symptom of more complex, architectural issue. Notification must be the best solution from a selection.
You can find documentation Notifications on Apple developer website under Notification Programming Topics.
Thank you for reading. If you like the article, please follow me for future posts.
Related topic: Delegate in Swift.
Show authors more ❤️ with 👏’s
And hope to see you next time 😊