NotificationCenter is an object that lets you register observers, so that those objects are notified when particular events (notifications) are emitted. When posting a notification to a notification center, only observers registered with that particular notification center will be notified.
For example, on the iOS platform, if you want to be notified when your application will go in the background, you can do it as follows:
self.resignObserver = NotificationCenter.default.addObserver(
forName: NSApplication.willResignActiveNotification,
object: nil,
queue: nil) { (notification) in
print("Application will resign active")
}
Don't forget to retain the observer, as you will need to remove it from the notification center once you're done listening for notifications:
NotificationCenter.default.removeObserver(self.resignObserver)
Now that we're somewhat familiar with the NotificationCenter API, let's dig deeper and implement a notification-based application that decouples the model layer from the UI layer through notifications.
First, let's define our custom centers:
extension NotificationCenter {
static let ui = NotificationCenter()
static let model = NotificationCenter()
}
As recommended by Apple documentation, we'll make extensive use of notifications, so let's instantiate custom notification centers to improve performance right away. In the code, we will be able to access those centers with NotificationCenter.ui and NotificationCenter.model.
The UI center will be responsible for sending view-related notifications, such as a button pressed, or text typed.
The model center will be responsible for sending all model updates. Whether the model is loaded from the database, or was updated after the network call, the model notification center will send notifications of those events.
We'll use two straightforward notification names—one for notifying model changes, and the other for notifying that the user wants to save the object:
let modelChangedNotification = Notification.Name(rawValue: "ModelChangedNotification")
let saveTappedNotification = Notification.Name(rawValue: "SaveTappedNotification")
We'll use three objects:
- Model, which contains a few properties as a struct
- ModelController, responsible for loading, and saving the object
- ViewController, responsible for interacting with the user
struct Model {
var title: String = ""
var description: String = ""
}
class ModelController {
private var uiObserver: NSObjectProtocol!
init() {
// Setup the observation
uiObserver = NotificationCenter.ui.addObserver(
forName: saveTappedNotification,
object: nil,
queue: nil) { [unowned self] (notification) in
guard let model = notification.object as? Model else { return }
self.save(model: model)
}
}
func loadModel() {
// Load the model from somewhere and emit it
let model = Model()
NotificationCenter.model.post(name: modelChangedNotification, object: model)
}
func save(model: Model) {
var model = model
// Ensure the title length is never > 20 chars
model.title = String(model.title.prefix(20))
// Save the model...
// Then emit back so we can update the UI
NotificationCenter.model.post(name: modelChangedNotification, object: model)
}
deinit {
// Do not forget to remove the observer
NotificationCenter.ui.removeObserver(uiObserver)
}
}
class ViewController {
private var modelObserver: NSObjectProtocol!
var model: Model? = nil
init() {
modelObserver = NotificationCenter.model.addObserver(
forName: modelChangedNotification,
object: nil,
queue: nil) { [unowned self] (notification) in
guard let model = notification.object as? Model else { return }
self.handle(model: model)
}
}
private func handle(model: Model) {
self.model = model
// Print it, our display is the console
print("\(model)")
}
func saveButtonTapped() {
NotificationCenter.ui.post(
name: saveTappedNotification,
object: self.model)
}
deinit {
NotificationCenter.model.removeObserver(modelObserver)
}
}
As you can see in the preceding code, ViewController and ModelController are not in direct connection. The display layer is driven by events sent by the model layer, and this helps when decoupling them.
You can test the UI code without connecting the model layer, and vice versa:
let modelControler = ModelController()
let viewController = ViewController()
// We have our two instances
// Load the model, this will emit the event to the UI Layer
modelControler.loadModel()
// In the UI, update the title
uiController.model?.title = "Hi! There"
// Tap the saveButton
uiController.saveButtonTapped()
// Attempt to update the title again
uiController.model?.title = "When gone am I, the last of the Jedi will you be. The Force runs strong in your family. Pass on what you have learned."
uiController.saveButtonTapped()
This program will print the following in the console:
Model(title: "", description: "")
Model(title: "Hi! There", description: "")
Model(title: "When gone am I, the ", description: "")
The first line is logged when calling modelControler.loadModel(), the second and the third when pressing the Save button. In any case, all the calls to print() were indirect and the result of mutations or events from the model layer.
NotificationCenter is quite powerful, but can be heavy and also lead to complex debugging scenarios. It is well-suited, however, to separating layers from your programs and yet allowing them to communicate loosely.
Keep an eye out for the following issues:
- Always remember to remove observers properly, as otherwise this can lead to crashes.
- Calling addObserver with the same objects will register multiple observers.
- Race conditions and thread safety—in our example, all calls are synchronous, but notifications can be dispatched on particular queues asynchronously.