Imagine a scenario in which multiple view controllers / views need to know about the current user’s authentication status, they have to show different information to a logged in user versus an anonymous user. To inform these observers one could use the existing NotificationCenter
, it’s been around for decades and using it rather straightforward. Any observer who wishes to listen to changes registers itself and when it’s done it removes itself from the observes. To pass information, however, one should be careful and know what types are passed around as userInfo
, cast this object from type Any?
to whatever the sender sends then use it, here is where I’m not a big fan. No need to mention that the developer should take care of naming the notifications and make sure sender and the receiver use the same name.
Here is a simple example of how it works:
/// Sender View Controller
func updateUserStatus(user: Models.User) {
let payload = ["user": user
NotificationCenter.default.post(name: Notification.Name("Notification.UserIsLoggedIn"), object: nil, userInfo: payload)
}
/// Receiver View Controller
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(handleMassage),
name: Notification.Name("Notification.UserIsLoggedIn"),
object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: Notification.Name("Notification.UserIsLoggedIn"), object: nil)
}
@objc func handleMassage(notification: NSNotification) {
if let userInfo = notification.userInfo as? NSDictionary {
if let user = userInfo["user"] as? Models.User {
/// Do something
}
}
}
Pretty big chunk of code for a simple purpose, right?
There is an alternative method, you can use protocols and adopt observers adopt to it. To me it’s much cleaner to do it with delegates rather than KVO notifications. There is one thing you should make sure to do and it’s unregistering the delegate when the observer is done, otherwise it will keep a reference to the observer in the memory and might cause unexpected behaviour in your program.
Let’s layout the architecture of our simple app, there is an Auth Service which handles login and logout and there are several view controllers who need to know about the status of authentication to update their information accordingly.
I’d like to call this middleware UserProfileServiceEvents
which informs the observers through a delegate called UserProfileServiceDelegate
. This delegate method will have three methods, userIsLoggedIn(user: Models.User)
for when user is logged in and the call to get user data from the backend is executed and returned, userIsLoggedOut()
for when user is logged out and finally another helpful one for authentication status is changed, when the observer is only interested to act on the change, userAuthStatusChanged()
.
protocol UserProfileServiceDelegate: NSObjectProtocol {
func userIsLoggedIn(user: Models.User?)
func userIsLoggedOut()
func userAuthStatusChanged()
}
Why NSObjectProtocol
? force of habit. Now that the protocol is ready, let’s have the service itself:
class UserProfileServiceEvents: NSObject {
override private init() {}
private var delegates = [UserProfileServiceDelegate]()
static let shared = UserProfileServiceEvents()
func userIsLoggedIn(user: Models.User?) {
for delegate in delegates {
delegate.userIsLoggedIn(user: user)
}
}
func userIsLoggedOut() {
for delegate in delegates {
delegate.userIsLoggedOut()
}
}
func userAuthStatusChanged() {
for delegate in delegates {
delegate.userAuthStatusChanged()
}
}
func addDelegate(_ delegate: UserProfileServiceDelegate) {
for item in delegates {
if item === delegate {
return
}
}
delegates.append(delegate)
}
func removeDelegate(_ delegate: UserProfileServiceDelegate) {
for (index, item) in delegates.enumerated() {
if item === delegate {
delegates.remove(at: index)
return
}
}
}
}
Yeah it’s singleton, otherwise you cannot keep track of delegates. Did you notice why delegates
is not a Set
? Because the way we declared our protocol it cannot conform to Hashable
hence no Set
for us here. The only thing worth mentioning in the code about is using ===
instead of ==
. That is used to indicating whether two references point to the same object instance, which is what we need. Those loops are basically doing what a Set
could have done for us, making sure there is only one instance of the object in the array.
Now that the service is ready, you can use it like this:
/// Observer View Controller
class ObserverViewController: UIViewController {
let userProfileServiceEvents = UserProfileServiceEvents.shared
override func viewDidLoad() {
super.viewDidLoad()
userProfileServiceEvents.addDelegate(self)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
userProfileServiceEvents.removeDelegate(self)
}
}
// MARK: - UserProfileService Events
extension ObserverViewController: UserProfileServiceDelegate {
func userIsLoggedOut() {
/// Do Something
}
func userAuthStatusChanged() {
/// Do Something
}
func userIsLoggedIn(user: StarModel.User?) {
/// Do Something
}
}
Have a great day or night or whatever!