Notification messages in iOS

Introduction

 

In this short tutorial I am going to present you solution that I created for one of my recent apps. I thought it might be very useful to have some kind of notifications presented to the end user without requiring him to make any actions. 

 

Implementation

 

So basically what I wanted was just one line of code that when called would show notification to the user no matter where my application was at the moment. To make this very simple for developers I wanted them to just pass the string to get notification shown on the screen.

So I came with following method:

+ (void)showString:(NSString *)string;

 

Simple, isn't it? Ok but what about styles and behaviours? I decided to make another method that would set notifications attributes that would be used throughout the whole application (aka UIApperance:D)

 

 

+ (void)setupNotificationsWithStyle:(TZNotifStyle)style
 			      delay:(NSTimeInterval)delay
		   heightPercentage:(CGFloat)heightPercentage
		  	  behaviour:(TZNotifBehavior)behavior
		           fontName:(NSString *)fontName;

Ok so here you can see there are 2 types TZNotifStyle and TZNotifBehavior which denote how it should look & behave. These are just customized inside class. In this blog I am only going to create one style and one behaviour but it is very simple to extend that class so If you do some cool looking notification let me know and I will add it here for others.

Besides these enums there are also arguments like delay which defines how long notification should be presented on screen. 

To make this notifications look similar on different screens I decided to create argument which will take notification height in percent. That value is meant to be between 0.0 (no notification) and 1.0 (full screen notificaton).

Additionally you can pass font name as last argument (e.g. "Helvetica")

Ok so here are enums that can be extended if new style or behavior is added to that class:

 

 

typedef enum {
	TZNotifBehaviorDefault,
	TZNotifBehaviorTopFromTopToTop,
	TZNotifBehaviorMax,
} TZNotifBehavior;

typedef enum {
	TZNotifStyleDefault,
	TZNotifStyleGrayAndWhite,
	TZNotifStyleMax,
} TZNotifStyle;

 

Ok so let me guide you through my implementation details now.

As you may have noticed both methods this class offers are class methods. setupNotificationsWithStyle is passing some attributes which will be used in all calls to showString method. This means I needed to keep it somewhere so I could use it any time need. So to accomplish that I thought of global variables:

 

 

static TZNotifStyle _style;
static NSTimeInterval _delay;
static CGFloat _heightInPoints;
static TZNotifBehavior _behavior;
static UIFont *_font;
static NSInteger _activeNotifs = 0;
static NSInteger _nextNotifPosition = 0;

I named them with _ to imitate they belong to class. 

Ok so to set them I made following implementation:

 

// This function should be therefore called from AppDelegate's didFinishLaunchingWithOptions
+ (void)setupNotificationsWithStyle:(TZNotifStyle)style
                              delay:(NSTimeInterval)delay
                   heightPercentage:(CGFloat)heightPercentage
                          behaviour:(TZNotifBehavior)behavior
                           fontName:(NSString *)fontName
{
    // this gives me thread safety here
    dispatch_async(dispatch_get_main_queue(), ^{
        _style = style;
        _delay = delay;
        _behavior = behavior;
        
        CGSize windowSize = [[TZNotif visibleWindow] frame].size;// always the same regardless of orientation
        _heightInPoints = heightPercentage * windowSize.height;
        _font = [UIFont fontWithName:fontName size:_heightInPoints / NOTIF_TO_STRING_RATIO];
    });
}

You can find here simple assignments for primitives and font creation. By default global objects like font are strong so there is noting to worry about since we use ARC.

All these setups are enclosed with dispatch_async which works on main queue. The fact this is working on main queue here doesn't really matter (it could be background queue) but later you will notice that this is in fact giving me some way to allow calling setup & showString from different threads without worries. You may notice I set font size here to be specified percentage of height of the screen. visibleWindow is a method that is included in private interface, I will come to it later.

Ok so once we setup our common values lets go to showString implementation:

 

// Shows notification for specified amount of time
+ (void)showString:(NSString *)string
{
 // all UIView manipulations must go from main thread, thread safety by the way
 dispatch_async(dispatch_get_main_queue(), ^{
  TZNotif *notification = [[TZNotif alloc] initWithString:string];
  [notification animate];
  // Add to subview because we want orientation, transform and translation events
  [[[[TZNotif visibleWindow] subviews] objectAtIndex:0] addSubview:notification];
 });
}

So here again I used dispatch_async to handle things, but here actually I need to use main thread. This is because all UIView animations need to go in main thread since UIView is not fully thread safe. By the way of using dispatch_async on the same queue as in setupNotificationsWithStyle I have guarantee that they won't be called in parallel which might cause some runtime problems including segmentation fault since you can imagine one thread might be changing font and the other reading it at the same time.

Ok so first thing I do in here is I create TZNotif object with provided string with the use of designated initializer initWithString. That initializer is creating object based on setup style and returning it. Here is how it looks like:

 

 

- (id)initWithString:(NSString *)string
{
 if (self = [super init])
 {
  CGSize stringSize = [string sizeWithFont:_font];
  // initial position
  self.frame = CGRectMake(0, 0, stringSize.width + 2 * (_heightInPoints - stringSize.height), _heightInPoints);
  
  // set label to be the same size, text will be smaller anyway
  UILabel *label = [[UILabel alloc] initWithFrame:self.frame];
  switch(_style)
  {
   case TZNotifStyleDefault:
   case TZNotifStyleGrayAndWhite:
    [self setBackgroundColor:[UIColor grayColor]];
    [self setAlpha:0.5f];
    [self.layer setCornerRadius:5.0f];
    [label setTextColor:[UIColor whiteColor]];
    break;
   default:
    break;
  };
  [label setBackgroundColor:[UIColor clearColor]];
  [label setFont:_font];
  [label setTextAlignment:NSTextAlignmentCenter];
  [label setText:string];
  [self addSubview:label];
 }
 return self;
}

NSString class has very convenient method sizeWithFont which returns size that will be consumed by given string on screen. Based on that size I can calculate frame size for custom UIView that will enclose our notification. Basically the size of the view is little bigger that text which will be presented.

Next thing to create is label that will be added to this custom view. There is a switch which based on your style will apply appropriate values so if you want to add your own style it's the right place to do so.

 

Next method is animate:

 

- (void)animate
{
 switch(_behavior)
 {
  case TZNotifBehaviorDefault:
  case TZNotifBehaviorTopFromTopToTop:
  {
   // initial position over window
   CGSize windowSize = [[[[TZNotif visibleWindow] subviews] objectAtIndex:0] bounds].size;// takes orientation into account
   [self setCenter:CGPointMake(windowSize.width/2, -_heightInPoints / 2)];
   
   // make it transformable to new orientation
   [self setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin|UIViewAutoresizingFlexibleRightMargin|UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleBottomMargin];
   
   // animations
   [UIView animateWithDuration:_delay / 3
         delay:0
        options:UIViewAnimationOptionCurveEaseOut
        animations:^{
         [self setCenter:CGPointMake(self.center.x, self.center.y + _heightInPoints * 1.2 + _heightInPoints * (CGFloat)_nextNotifPosition)];
         _activeNotifs++;
         _nextNotifPosition++;
        }
        completion:^(BOOL finished){
         //if (finished) { // in case of orientation change we do it anyway
         [UIView animateWithDuration:_delay / 3
              delay:_delay
              options:UIViewAnimationOptionCurveEaseIn
             animations:^{
              [self setCenter:CGPointMake(self.center.x, - _heightInPoints/2)]; //fully hidden
             }
             completion:^(BOOL finished){
              //if (finished) { // in case of orientation change we do it anyway
              _activeNotifs--;
              if (_activeNotifs == 0)
              {
               _nextNotifPosition = 0;
              }
              [self removeFromSuperview];
             }
         ];
        }
    ];
  }
   break;
  default:
  {
  }
   break;
 };
}

 

In this function we set the whole animation for previously created view. There is a great function to make any kind of animations on UIViews - animateWithDuration. I will not be describing it here, you can read about it in apple documentation but its worth to mention it is working on blocks and after it finishes animation it executes completion block. This allows developers to create chain of animations on a given UI element.  What is happening here is that I set initial center positon of our custom view then in animation block I move it down. In completion block I setup another animation to hide it but this time after specified delay.

There are 2 variables activeNotifs and nextNotifPosition. activeNotifs is updated when notification is added or removed so I am able to track how many notifications are there at the moment. nextNotifPosition is incremented until all notifications are gone then it is set to zero. Such approach allows me to display notifications one below the other so they never get displayed in the same place if you fire a couple of them at the same time.

Ok so last method is as follows:

 

+ (UIWindow *)visibleWindow
{
  // I assume there is only one window, this is not always true
  return [[UIApplication sharedApplication].windows objectAtIndex:0];
}

This is trying to find visible window, most of the time this should function properly, there were many discussions on stackoverflow how it should be done but this is the most dependable one I was able to find.

 

So basically this is it! Here is how it looks:

screenshoot

 

 

 

Room for improvement

1. Create new styles and behaviors - please share it with us!

 

GitHub sources

https://github.com/bloto/TZNotif

 

Leave a comment

I'd love to hear your thoughts!