Dealing with NotificationCenter is daily task for all iOS developers. Either it’s a system notification such as KeyboardShow/KeyboardHide or CustomNotification to post some information app wide, we all have used Notifications.

Yeah, So. Do you have something interesting to say?

Well, answer to that question is, Yes. Today, we are gonna discuss notification programming in such a way, that it will become as easy as eating an apple pie. (I don’t think anyone can deny it, eating an apple pie is just so satisfying, obviously if you love apple pie in first place ;p )

Old way of handling notification

In Swift we focus on writing all the APIs in strongly typed manner, then why not same with Notifications? Let’s see an example of default way of doing notification handling.

// Registering system notification for keyboardShow and keyboardHide
func registerForKeyboardNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}

// Action on keyboardShow notification receive
func keyboardWillShow(_ notification: Notification) {
    let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
    let keyboardDuration = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? Double
      //do something with these value
    }
}

// Action on keyboardShow notification receive
func keyboardWillHide(_ notification: Notification) {
  let keyboardDuration = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? Double
}

//Removing notifies on viewController deinit
func deregisterFromKeyboardNotifications(){
    NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
    NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}

We write this same code again and again or we create a BaseViewController and have these defined or maybe a UIViewController extension. But all of these are still not efficient as you have to dig into notification.userInfo dictionary to get relevant information.

Typed Notificaion

Strongly Typed (Sorry, about the art being not so good. Tried to do my best. ;))

We want to define notifications in such a way, where we can definitively get the data from our notification observer (strongly typed), not a userInfo dictionary. This way we will enforce swift type system to help us writing type safe and bug free code. In this process we will also redefine how we observe our notification to make our code DRY(don’t repeat yourself).

To create typed notification we can first define an NotificationDescriptor struct. Our NotificationDescriptor will have two properties,

  1. name defining unique notification name
  2. convert closure, which help us extract out data from Notification.userInfo
struct NotificationDescriptor<A> {
    //Name of the notification
    let name: Notification.Name

    //Method to convert notification.userInfo to desired type
    let convert: (Notification) -> A
}

With help of Swift’s Generic type we can use above Struct for all type of notifications and describe our notifications using this.

Next we can extend NotificationCenter to provide us a convenience method over default addObserver method.

extension NotificationCenter {
    func addObserver<A>(forDescriptor d: NotificationDescriptor<A>, using block: @escaping (A) -> ()) {
        addObserver(forName: d.name, object: nil, queue: nil, using: { notification in
            block(d.convert(notification))
        })
    }
}

But there is one thing missing in above implementation. We have added the observer but we are ignoring the token received from addObserver method. Without this token we won’t be able to deregister the notification. For this purpose we can define a Token class.

class Token {
    let center: NotificationCenter
    let token: NSObjectProtocol

    init(token: NSObjectProtocol, center: NotificationCenter) {
        self.token = token
        self.center = center
    }

    deinit {
        center.removeObserver(token)
    }
}

And redefine our addObserver method this way:-

extension NotificationCenter {
    func addObserver<A>(forDescriptor d: NotificationDescriptor<A>, using block: @escaping (A) -> ()) -> Token {
        let t = addObserver(forName: d.name, object: nil, queue: nil, using: { notification in
            block(d.convert(notification))
        })
        return Token(token: t, center: self)
    }
}

We can store the Token returned by above function as a ViewController property, when ViewController’s deinit gets called, Token will also gets destroyed and notification gets deregistered.

How to use it

We will define KeyboardShow and KeyboardHide notification using our NotificationDescriptor.

let keyboardShowNotification = NotificationDescriptor<A>(name: Notification.Name.UIKeyboardWillShow, convert: { notification in
  // Here we can parse the userInfo into our desired type
})

let keyboardHideNotification = NotificationDescriptor<KeyboardPayload>(name: Notification.Name.UIKeyboardWillHide, convert: convert: { notification in
  // Here we can parse the userInfo into our desired type
})

Let’s define a struct with info we require from keyboard notification using KeyboardNotificationKeys.

struct KeyboardData {
    let beginFrame: CGRect
    let endFrame: CGRect
    let curve: UIViewAnimationCurve? //If we want to use it to synchronize our animation
    let duration: TimeInterval
}

We can also define an initializer to our KeyboardData which take notification and instantiate KeyboardData. This can be used as our convert function and we don’t have to redefine convert for KeyboardShow and KeyboardHide.

This is also the most important part of our Type-Safe Notification. We will have strictly defined data in our application and all the parsing logic is separated out for easy debugging and testing.

extension KeyboardData {
    init(note: Notification) {
        guard let userInfo = note.userInfo else {
            self.beginFrame = CGRect.zero
            self.endFrame = CGRect.zero
            self.curve = nil
            self.duration = 0

            return
        }

        self.beginFrame = userInfo[UIKeyboardFrameBeginUserInfoKey] as! CGRect
        self.endFrame = userInfo[UIKeyboardFrameEndUserInfoKey] as! CGRect
        self.curve = UIViewAnimationCurve(rawValue: (userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int) ?? 0)
        self.duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! TimeInterval
    }
}

KeboardShowDescriptor and KeyboardHideDescriptor will become

struct SystemNotification {
    static let keyboardShowNotification = NotificationDescriptor<KeyboardData>(name: Notification.Name.UIKeyboardWillShow, convert: KeyboardData.init)
    static let keyboardHideNotification = NotificationDescriptor<KeyboardData>(name: Notification.Name.UIKeyboardWillHide, convert: KeyboardData.init)
}

We have also encapsulated these properties in SystemNotification struct, so that our global namespace doesn’t get polluted with all different types of notification descriptors.

UIViewController usage

Having different notification with respective descriptor defined at a central place, using these in a UIViewController is very simple and we don’t have to track the notification for deregister.

private var keyboardShowToken: Token?
private var keyboardHideToken: Token?

override func viewDidLoad() {
    super.viewDidLoad()

    registerForKeyboardNotifications()
}

// MARK: Register keyboardShow and keyboardHide notification for this UIViewController keeping reference to respective tokens
func registerForKeyboardNotifications() {
        keyboardShowToken = NotificationCenter.default.addObserver(descriptor: SystemNotification.keyboardShowNotification) { keyboardData in
          //Custom logic related to view only
        }

        keyboardHideToken = NotificationCenter.default.addObserver(descriptor: SystemNotification.keyboardHideNotification) { keyboardData in
            //Custom logic related to view only
        }
    }

This way we are also freeing up our UIViewController from other logic and only allowing it to handle views. This is also highly reusable, all the repetitive code is moved to a single place and this makes our code DRY.

For MVVM architecture above implementation fits perfect, a viewController should only handle view related logic only.

Inspiration

Above pattern is inspired from SwiftTalk Objc.io. I would highly recommend watching this episode. They also define a different pattern using Protocol for handling the Notification. But for our use case we are fine with above implementation, much simpler and easy to use.

Happy coding!

The moldedbits Team

comments powered by Disqus