Wednesday 13 January 2016

SBNotificationBar : A Custom Notification bar for iOS

Finally It's time for me to write a post on the platform I love the most. Always wanted to write a post on iOS, But was waiting for the right time and the content. I believe today my waiting is over. Hence writing this post.
                       
                              Someone rightly said, "necessity is the mother of invention" and who can understand it better than a developer  huh?? Working on a chat application, I recently came across the requirement to show a notification of new message on top of status bar and navigation bar. Just to give an idea of what I am talking about, here is what I needed exactly.


                             As usual I did what most of us would do first.  "FIND A THIRD PARTY LIBRARY!!!!".  Fortunately enough, found quite few of them. I must say many of them are quite brilliant. Integrated one of them and gave a build to my lead.
                         
                            What happened next??? You know what happens to all the builds right? You have been through this. Haven't you??? Yes!! You guessed it right. They started pouring out all new and weird requirements.

 "Notification bar should never disappear till user taps on it!!!" this one being the most funny, gives you a fair idea of what I had been through. Requirements were so strange that none of the existing framework could have satisfied their needs!!

                            Now left with no other way to go I decided to write a component of my own. SBNotificationBar is the invention of necessity to save my job I guess. 'SB' in the name of the framework stands for "Sandeep Bhandari" of course in case you haven't guessed it yet. I spent couple of hours to create it and I din wanted anyone else to waste theirs, so I have added a public repository in git hub and uploaded the project. Here is the link : SBNotificationBar
                   
                            How to use it is explained very much detail in the same page. This post is for the ones, who want to understand it and modify it to match the weird requirements of their  bosses :P

                            Lets dissect the the SBNotificationBar framework :). If you have downloaded the project and tested it, you must have seen, project files have a folder called SBNotificationBar.



SBNotificationBar has two pairs of .h and .m files:
1>SBNotificationViewController.h
    SBNotificationViewController.m

2>SBCustomWindow.h
    SBCustomWindow.m

and one SBNotificationBarView.xib. These five files are what you actually need. Most of the logic of notification bar has been placed in SBNotificationViewController and SBCustomWindow does a very little but yet a very tricky part here.

                     Lets see the code savvy?? SBNotificationViewController is a singleton class. Remember we used sharedManager to instantiate the instance of it?

Thats pretty much straight forward isn't it. Nothing to explain here I believe. Let's see the implementation of init.
Hmmmmm...... that's a lot of code there boy. Lets analyze them one by one.

NSArray *subviewArray = [[NSBundle mainBundle] loadNibNamed:@"SBNotificationBarView" owner:self options:nil];
UIView *mainView = [subviewArray objectAtIndex:0];
self.view =  mainView;

In these three statements am trying to load the xib and set the loaded view as view controller's view. Every day job huh?? Tricky part is yet to come :)

keyWindow= [[SBCustomWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
keyWindow.windowLevel = UIWindowLevelAlert;
keyWindow.rootViewController = self;
[keyWindow makeKeyAndVisible];

If you had ever tried loading a view on top of status bar and navigation bar before, you must have realized that adding a view as subview to navigation bar or even to the application window leads to hundreds of issues. I faced quite a few of them my self. Here are the few funny things I did.

Add a subview to navigation bar:


If you add view as a subview to navigation bar, your view will appear below status bar and most importantly your view will never appear on a view controller which does not have a navigation bar at all like the one presented modally. Thats not desirable isn't it? We want our notification to appear on top of all the windows no matter whether view has navigation bar or not.

If you are sneaky minded enough to think that you will always depend on applications rootviewcontroller navigationbar, your code will crash in case applications rootview controller does not have navigation bar at all :) like login or signup page of app?? :) 

Add a subview to application window:


For a quite sometime I was in an illusion that this has to be the obvious solution. "My notification view is appearing on top of navigation bar and is starting with status bar, with few more modification this approach should give me the desired output" I thought. I searched a little in google and found out that by setting applications window level to Alert level, my notification will not overlap with status bar instead will cover it completely as expected.

That's it!!!! I thought I was done with my framework.  Now I can show my notification window on top of all other view what else I want??? 

Issues with this approach:
1> User interactions will never reach to your notification bar : For some strange reasons, adding  view controller's view as subview to appdelegate.window, user interactions will never reach the view controller no matter how many times user taps the view.

I have noticed that correct way to add viewcontroller's view as subview is to add the viewcontroller as childview controller to parent first and then add viewcontrollers view as subview to parentview controller's view.

In case of appdelegate.window you cant use addChildViewController to add notification viewcontroller as a child to window. So you will have to fall back to appdelegate.window.rootviewcontroller, to add notification viewcontroller as a child. And then add notification viewcontroller's view as subview to either rootview or to window.  Even after so much of circus you might not get user interaction propagated to your view controller, but you will get the crash for sure if your appdelegate.window.rootviewcontroller is not UINavigation controller.

While writing a generic framework, I could not have just assumed that rootviewcontroller will always be UINavigation controller right??? I did everything I could, but did not manage to get the user interaction to my view controller, I felt as if appdelegate.window is completely blocking all the user interactions. 

Most obvious suggestion was to subclass the window and handle "-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event" or to handle "-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event" to propagate touch events to my view controller, It might have worked but I could not have asked the user to load subclass of my window as his application key window if he wants to use my framework. And that sounded little sneaky as well to me.


2>  Fails absolutely!! on loading the notification bar in viewDidLoad or viewWillAppear of initial view controller : If you still think that adding notification bar as a subview to appdelegate.window is the solution, you should consider loading your notification view in viewDidLoad or ViewWillAppear of initial view controller.

I have observed that trying to load notification bar view in viewDidload or ViewWillAppear of initial view controller fails because we are trying to add notification bar's view as subview to appdelegate.window but in viewDidLoad or viewWillAppear of initial view controller appdelegate.window will be nil.

My Solution :

keyWindow= [[SBCustomWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
keyWindow.windowLevel = UIWindowLevelAlert;
keyWindow.rootViewController = self;
[keyWindow makeKeyAndVisible];
    
[keyWindow.rootViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];

Adding a notification bars view as a subview to window was partially correct, because that yielded me desired visual output but failed to transmit user interactions to my view controller and also failed to load view in case of initial view controllers viewDidLoad and viewWillAppear as window itself was nil.

Continuing in the same path I decided to create a subclass of window and instantiate it inside the SBNotificationViewController so that I can handle touch events delegate on my own and need not ask the user to use my window as his application main window. This approach will load notification bar even in viewDidLoad or viewWillAppear of initial view controller because we are no longer dependent on appdelegate.window :)

Key here is to create a window and set its window level to UIWindowLevelAlert and its root view controller to notification bar view controller and call [window makeKeyAndVisible]; to make the changes effective and to bring the custom window on top of all other windows :)

Now you are done with major work. Now your custom window is on top of all other window, so whenever user taps, your window will receive the touch , your view controller is the root view controller of the window on top, so your view controller will be triggered with user interactions.

Code below ensures that your root view controller will expand and compress with key window. Key window changes its size automatically on screen rotations. All you have to do is to ensure that root view controller is doing the same with key window.

I have applied auto layout constraints here programmatically. 

[keyWindow.rootViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];
UIView *currentView = keyWindow.rootViewController.view;
NSDictionary *viewComponents = NSDictionaryOfVariableBindings(currentView);
    
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-(0)-[currentView]-(0)-|" options:0 metrics:nil views:viewComponents];
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(0)-[currentView(88)]-(>=8)-|" options:0 metrics:nil views:viewComponents];
    yAxisConstraint =  [verticalConstraints objectAtIndex:0];
    
[keyWindow addConstraints:horizontalConstraints];
[keyWindow addConstraints:verticalConstraints];
[keyWindow layoutIfNeeded];

Code is very much straight forward and easily understandable if you know VFL. 

Issue :
Because now our window is on top of all other window, all the user interactions will be captured by our window. As a result view controller which loaded our notification bar becomes completely inaccessible.

Solution:
Remember we subclassed our window :) We can now write touch delegates to decide whether to capture that touch or to pass it over to view stacked below. Thats all.
How to do it?? Have you ever heard of 

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event.

This method decides whether to pass the touch to views/view controllers stacked below or not. So what I did is to override it simple :)

What am I doing here? Nothing, just check if user tapped on the point, which belongs to my root view controller or not. If it belongs to my view controller return true else say false so that operating system will continue to ask the same question to view stacked below and thats how you transfer touch events back to parent view controller that loaded your notificationBar.

There is much more code in SBNotificationBarViewController.m but mostly it deals with animations of the view and timers to show and hide the view.  Most of the code is straight forward and can be understood easily by having a glance of it.

I believe SBNotificationBar is the simplest solution to show your custom notifications on top of all other views. It works fairly well on all kinds of view controllers no matter how view controller is displayed (may it be push or modal presentation). It also works absolutely fine when loaded in viewDidLoad or viewWillAppear of initial view controller. Its simple,easy and just drag n drop away.

Hope you enjoyed the post and the framework. Have fun with SBNotificationBar and don't forget to  reach me out in case you have any doubts or suggestions. Leave your comments below :)