Custom Components - Notification Center

In order to proceed open Xcode and do the following:

 

CMD (⌘) + SHIFT + N  (new single view project)

CMD (⌘) + N (new class, subclass of UIViewController which will be our NotificationCenterViewController)

 

Once the class is created, define isSlideActionAvailable as property in order to be aware when should we enable user interaction on our custom NotificationCenterViewController.

Also for private use we need to define another flag isMoving in order to be aware what is the state of the view. Place this flag in the auto-generated extension of our class between the curly brackets {}.

 

In order to move the view up and down we need to add an UIPanGestureRecognizer. This gesture recognizer will detect the pan action and will provide a series of events/states which will be useful for us in order to create the desired behavior. We can add the gesture recognizer right on the initialization method for our NotificationCenterViewController.

 



#import <UIKit/UIKit.h>

@interface DBNotificationCenterViewController : UIViewController

@property (nonatomic, assign) BOOL      *isSlideActionAvailable;

@end

 

@interface DBNotificationCenterViewController (){

    BOOL isMoving;

}

@end

 

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil

{

    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];

    if (self) {

        // Custom initialization

        UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePan:)];

        [self.view addGestureRecognizer:panGesture];

        [panGesture release];

    }

    return self;

}

 

Since we have declared the target as being self, and for the action we’ve specified the handlePan: method, this will be the place where all the magic will happen. Easy right :) ?

 

Let’s proceed with the implementation of handlePan: method.

Right before the end of your class (@end) place the following lines of code, which represents the skeleton for this method:

 



#pragma mark -

#pragma mark UIGestureRecognizer Methods

- (void)handlePan:(UIPanGestureRecognizer *)recognizer{

    

    if (_isSlideActionAvailable) {

       

        if (recognizer.state == UIGestureRecognizerStateBegan) {

            

        }

        

        if (recognizer.state == UIGestureRecognizerStateChanged) {

            

        }

        

        if (recognizer.state == UIGestureRecognizerStateEnded) {

           

        }

    }

}

 

The decisional structure presented above is checking if user’s interaction should be considered or not ( if (_isSlideActionAvailable) ), and if the result is TRUE we should handle each recognizer state separately.

Before any other changes to this method, we should define a touchable area for our view, and also a top offset in order to differentiate it from the NotificationCenter present in the OS. Place the following two lines at the top of your class right after the comments.

 

#define TOUCHABLE_AREA 80.0

#define TOP_OFFSET 40

 

Since we’ve defined our touchable area, the next step is to validate the start location of the pan gesture. We can do that by comparing the y coordinate of the touch location with the difference between the view’s frame height and our TOUCHABLE_AREA value. If the y value is greater, that means that we have started to move the view. Yay :) !

 

This is how our logic should look like:

 



if (recognizer.state == UIGestureRecognizerStateBegan) {

    CGPoint startPoint = [recognizer locationInView:self.view];

    if (startPoint.y > self.view.frame.size.height - TOUCHABLE_AREA){

        isMoving = YES;

    }

}

 

Until now we have managed to detect when we should move the view, but the actual translation take place only in the UIGestureRecognizerStateChange branch.

 

In order to move the view up and down we can make use of translationInView: method form UIPanGestureRecognizer class. This way we can get the translation of the pan gesture in our’s view coordinate system, and increase the y value of our notification view center with the y value of the translation in order to move it around.

 



 

if (recognizer.state == UIGestureRecognizerStateChanged) {

    if (isMoving){

        CGPoint translation = [recognizer translationInView:self.view];

        if (recognizer.view.center.y + translation.yself.view.frame.size.height/2) {

            recognizer.view.center = CGPointMake(recognizer.view.center.xrecognizer.view.center.y + translation.y);

            [recognizer setTranslation:CGPointMake(0, 0) inView:self.view];

        }

    }

}

 

The last step is to adjust the final position of the screen. This means the notification view can be fully displayed or closed. To achieve this behavior we must compare the current position of our notification view with a certain limit, in order to know which of the final states described above we must choose.

 



 

if (isMoving) {

    CGPoint finalPoint;

    CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;

    if ((screenHeight + self.view.frame.origin.y) > TOUCHABLE_AREA) {

        finalPoint = CGPointMake(recognizer.view.center.x,recognizer.view.frame.size.height/2);

    }else{

        finalPoint = CGPointMake(recognizer.view.center.x,-screenHeight/2 + TOP_OFFSET);

    }            

    recognizer.view.center = finalPoint;

    isMoving = NO;

}

 

In order to test what we have already build until now, create a new class which will be a subclass of our NotificationCenterViewcontroller and make sure the checkbox in front of “With XIB for user interface” is selected. Then change the background color of it’s view in order to spot the difference while sliding.

Now go back into your ViewController class, and add the following piece of code right before the end of the method.

 



 

NotificationViewController *notif = [[NotificationViewController alloc] initWithNibName:@"NotificationViewController" bundle:nil];

[notif setIsSlideActionAvailable:YES];

[self.view addSubview:notif.view];

 

Just run the project and slide that view. What a mess :). It’s sliding pretty well but when you release it, the jump to the final position is instant and it looks annoying. Let’s fix that.

 

In order to fix the translation to the final place we will use UIView animation, but to make things more exiting we will also consider the pan velocity and the remaining distance to the final point. This way we will not only slide the view. In fact we  will ad an impulse to it’s sliding velocity. Isn’t that cool?

 

The fist step for this customization is to create an enum which will contain the slide directions, which will be useful when we need to track the last sliding direction. In order to do that add those lines of code right after the #define directive from NotificationCenterViewController.m, and then create a variable named direction right after the BOOLEAN isMoving from our class extension .

 



 

typedef enum{

    NotificationCenterMovingDirectionUp,

    NotificationCenterMovingDirectionDown

} NotificationCenterMovingDirection;

 

NotificationCenterMovingDirection   direction;

 

Right after we need to detect this direction changes, and for that we will update the handlePan: method.

 



 

if (recognizer.state == UIGestureRecognizerStateChanged) {

    if (isMoving){

        CGPoint translation = [recognizer translationInView:self.view];

        if (recognizer.view.center.y + translation.yself.view.frame.size.height/2) {

            recognizer.view.center = CGPointMake(recognizer.view.center.xrecognizer.view.center.y + translation.y);

            [recognizer setTranslation:CGPointMake(0, 0) inView:self.view];

                    

            if (translation.y != 0) {

                (translation.y > 0) ? (directionNotificationCenterMovingDirectionDown) : (directionNotificationCenterMovingDirectionUp);

            }

        }

    }

}

 

We are pretty close to the final result now. All we have to do is a little more math.

 



if (recognizer.state == UIGestureRecognizerStateEnded) {

    if (isMoving) {            

        CGPoint velocity = [recognizer velocityInView:self.view];

        CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));

        CGFloat distanceLeft;

        CGPoint finalPoint;       

        CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;

 

        if ((((screenHeight + self.view.frame.origin.y) > TOUCHABLE_AREA) && direction == NotificationCenterMovingDirectionDown)

                    || (direction == NotificationCenterMovingDirectionDown  && magnitude > 500)) {

            finalPoint = CGPointMake(recognizer.view.center.x,recognizer.view.frame.size.height/2);

            distanceLeft = 0 - self.view.frame.origin.y;

        }else{

            finalPoint = CGPointMake(recognizer.view.center.x,-screenHeight/2 + TOP_OFFSET);

            distanceLeft = screenHeight + self.view.frame.origin.y;

        }

                

        CGFloat duration = MIN(distanceLeft / sqrtf(velocity.y * velocity.y), 0.35);

        [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{

            recognizer.view.center = finalPoint;

        } completion:nil];

                

        isMoving = NO;

    }

}

 

The final project can be downloaded from here.

Thanks for following me!

Posted by dorindanciu 07/10/2012 at 09:25PM


Comments

Leave a response

Leave a comment