Nick Harris

iOS Slide-out Navigation Code

Ken Yarmosh has a great post entitled New iOS Design Pattern: Slide-out Navigation in which he does a great job describing this new design pattern and how different applications are using it.

I personally like the approach so I decided to figure out how I would implement it.  I have no idea if any of the apps mentioned in Ken’s post actually do it this way, but it works.

Slide-out Navigation

The idea here is use the Application Delegate as the controller between the current content view of the application and the menu view.  When a view in the application needs to show the navigation menu, it can call a method on the App Delegate that handles all the work of showing and restoring the menu and content views.

The “illusion” of the content view sliding off to reveal the menu view is done by first grabbing a screenshot of the current content view and passing that off to the MenuViewController.  The MenuViewController has a UIImageView that it populates with that screenshot.  Its this UIImageView that acts as the content view overlay. It makes animation smooth using a single flat image rather then trying to animate an actual view back and forth.

AppDelegate

The AppDelegate acts as the central controller whenever any views need to show the menu view. It gets two new properties:

@property (strong, nonatomic) ContentViewController *contentViewController;

@property (strong, nonatomic) MenuViewController *menuViewController;

 

The contentViewController gets used as a temp holding place while the menuViewController is visible.  Having the two of them in the app delegate makes switching the window.rootViewController easier.  I do this in two new methods on the AppDelegate:

-(void)showSideMenu

{

// before swaping the views, we'll take a "screenshot" of the current view

// by rendering its CALayer into the an ImageContext then saving that off to a UIImage

CGSize viewSize = self.contentViewController.view.bounds.size;

UIGraphicsBeginImageContextWithOptions(viewSize, NO, 1.0);

[self.contentViewController.view.layer renderInContext:UIGraphicsGetCurrentContext()];

 

// Read the UIImage object

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

 

// pass this image off to the MenuViewController then swap it in as the rootViewController

self.menuViewController.screenShotImage = image;

self.window.rootViewController = self.menuViewController;

}

 

-(void)hideSideMenu

{

// all animation takes place elsewhere. When this gets called just swap the contentViewController in

self.window.rootViewController = self.contentViewController;

}

 

MenuViewController

The MenuViewController handles all the animation and touch gestures that make the slide-out navigation feel real.  It has two important properties:

@property (strong, nonatomic) IBOutlet UIImageView *screenShotImageView;

@property (strong, nonatomic) UIImage *screenShotImage;

 

Using the viewWillAppear:animated method we can reset the menu view, covering the entire view with the screenShotImageView whose image has been set using the screenshot taken by the app delegate of the currentContentViewController.  It’s then just animated to the right.

-(void)viewWillAppear:(BOOL)animated

{

[super viewWillAppear:animated];

 

// when the menu view appears, it will create the illusion that the other view has slide to the side

// what its actually doing is sliding the screenShotImage passed in off to the side

// to start this, we always want the image to be the entire screen, so set it there

[screenShotImageView setImage:self.screenShotImage];

[screenShotImageView setFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];

 

// now we'll animate it across to the right over 0.2 seconds with an Ease In and Out curve

// this uses blocks to do the animation. Inside the block the frame of the UIImageView has its

// x value changed to where it will end up with the animation is complete.

// this animation doesn't require any action when completed so the block is left empty

[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{

[screenShotImageView setFrame:CGRectMake(265, 0, self.view.frame.size.width, self.view.frame.size.height)];

}

completion:^(BOOL finished){  }];

}

 

The easy part of the MenuViewController is if the user selects a new view.  Simply set the currentViewController on the AppDelegate with the new view, then call the slideThenHide method which animates the screenshot back over the entire screen completing the illusion.

-(IBAction)showLogoExpandingViewController

{

// this sets the currentViewController on the app_delegate to the expanding view controller

// then slides the screenshot back over

[app_delegate setContentViewController:[[LogoExpandingViewController alloc] initWithNibName:@"LogoExpandingViewController" bundle:nil]];

[self slideThenHide];

}

 

-(void) slideThenHide

{

// this animates the screenshot back to the left before telling the app delegate to swap out the MenuViewController

// it tells the app delegate using the completion block of the animation

[UIView animateWithDuration:0.15 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{

[screenShotImageView setFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];

}

completion:^(BOOL finished){ [app_delegate hideSideMenu]; }];

}

 

The more difficult part is detecting when the user interacts with the screenshot.  If they touch it, the end result should be the menu hiding and the content view becoming active again.  So it needs a UITapGestureRecognizer.

It also needs to be user interactive.  If they touch and drag the screenshot it should respond by moving. So it also needs a UIPanGestureRecognizer.

I wasn’t up to speed with dragging using Gestures so after a little searching I found this post by Soo How Ng which made it pretty simple:

- (void)viewDidLoad

{

[super viewDidLoad];

 

// create a UITapGestureRecognizer to detect when the screenshot recieves a single tap

tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTapScreenShot:)];

[screenShotImageView addGestureRecognizer:tapGesture];

 

// create a UIPanGestureRecognizer to detect when the screenshot is touched and dragged

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

[panGesture setMaximumNumberOfTouches:2];

[panGesture setDelegate:self];

[screenShotImageView addGestureRecognizer:panGesture];

}

 

- (void)viewDidUnload

{

[super viewDidUnload];

 

// remove the gesture recognizers

[self.screenShotImageView removeGestureRecognizer:self.tapGesture];

[self.screenShotImageView removeGestureRecognizer:self.panGesture];

}

 

- (void)singleTapScreenShot:(UITapGestureRecognizer *)gestureRecognizer

{

// on a single tap of the screenshot, assume the user is done viewing the menu

// and call the slideThenHide function

[self slideThenHide];

}

 

/* The following is from http://blog.shoguniphicus.com/2011/06/15/working-with-uigesturerecognizers-uipangesturerecognizer-uipinchgesturerecognizer/ */

-(void)panGestureMoveAround:(UIPanGestureRecognizer *)gesture;

{

UIView *piece = [gesture view];

[self adjustAnchorPointForGestureRecognizer:gesture];

 

if ([gesture state] == UIGestureRecognizerStateBegan || [gesture state] == UIGestureRecognizerStateChanged) {

 

CGPoint translation = [gesture translationInView:[piece superview]];

 

// I edited this line so that the image view cannont move vertically

[piece setCenter:CGPointMake([piece center].x + translation.x, [piece center].y)];

[gesture setTranslation:CGPointZero inView:[piece superview]];

}

else if ([gesture state] == UIGestureRecognizerStateEnded)

[self slideThenHide];

}

 

- (void)adjustAnchorPointForGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {

if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {

UIView *piece = gestureRecognizer.view;

CGPoint locationInView = [gestureRecognizer locationInView:piece];

CGPoint locationInSuperview = [gestureRecognizer locationInView:piece.superview];

 

piece.layer.anchorPoint = CGPointMake(locationInView.x / piece.bounds.size.width, locationInView.y / piece.bounds.size.height);

piece.center = locationInSuperview;

}

}

 

Sample

 

Source Code

https://bitbucket.org/nh129096/slideoutnavigationsample/src

 

About these ads

Written by Nick Harris

February 5, 2012 at 3:53 am

Posted in Uncategorized

10 Responses

Subscribe to comments with RSS.

  1. It would be even more effective if you paused the animations/movies/etc. Although, I understand that it helps show what’s going on in the example.

    Shaun Lewis

    February 14, 2012 at 1:36 am

  2. By the way, I wasn’t harping on it at all. I like it a lot!

    Shaun Lewis

    February 14, 2012 at 1:37 am

  3. Thanks! You have me curious though… does Core Animation not pause the animation on its own when the view gets hidden? I’ll dig around on that.

    Nick Harris

    February 14, 2012 at 1:41 am

  4. Thanks for this example. It’s very useful.

    One thing regarding the image – you write that “It makes animation smooth using a single flat image rather then trying to animate an actual view back and forth.”

    I think that CoreAnimation just animates the backing layer, which would qualify as a single flat image anyway. Has the performance been compared?

    I can see it would make sense to unload the original View Controller and associated controls to save memory though.

    It would be interesting to try implementing this with the new nested View Controller structure.

    Michael Spears

    February 14, 2012 at 5:43 pm

  5. the image of the view is blurred once in slide-out position, meaning, when the menu is show and the “view” is slide to the right side. is there any reason/fix for that?

    tw3141

    May 9, 2012 at 1:40 am

    • Hi! Incredibly late reply frome me, but I would try UIGraphicsBeginImageContextWithOptions(viewSize, NO, 0.0) and let UIKit take care of the render scale. Makes sense, anyone…?

      John Willsund

      January 24, 2013 at 3:18 pm

  6. Simple and clear. Thanks Nick!
    By the way, how can i implement for both sides left and right?
    What do i need to add in your code?

    Randy

    July 11, 2012 at 3:23 am

  7. Works nicely.

    Agree with mr. Spears that combining it with nested view controllers would be interesting.

    Compared to the new ios facebook-app (which was launched several months after this article) it misses the panning gesture to slide the menu into view.
    Any tips on how this best could be implemented with your pattern?

  8. Just a quick update. Great tutorial, but iOS6 seems to have changed how a few things work. The animation for the menu slide should be moved to the viewDidAppear method instead of viewWillAppear as the auto layer feature that is new with IOS6 likes to rearrange views all the way up until viewWillAppear is called.

    Tim M

    December 7, 2012 at 9:40 pm

  9. *auto layout feature, I mean.

    Tim M

    December 7, 2012 at 9:40 pm


Comments are closed.

Follow

Get every new post delivered to your Inbox.

Join 480 other followers

%d bloggers like this: