An alternative method to replace NotificationCenter for broadcasting messages

coffee

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!

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.