// // SlideNavigationController.m // SlideMenu // // Created by Aryan Gh on 4/24/13. // Copyright (c) 2013 Aryan Ghassemi. All rights reserved. // // https://github.com/aryaxt/iOS-Slide-Menu // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #import "SlideNavigationController.h" #import "SlideNavigationContorllerAnimator.h" @interface SlideNavigationController() @property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; @property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer; @property (nonatomic, assign) CGPoint draggingPoint; @end @implementation SlideNavigationController #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending) #define MENU_SLIDE_ANIMATION_DURATION .3 #define MENU_QUICK_SLIDE_ANIMATION_DURATION .18 #define MENU_IMAGE @"menu-button" #define MENU_SHADOW_RADIUS 10 #define MENU_SHADOW_OPACITY 1 #define MENU_DEFAULT_SLIDE_OFFSET 60 #define MENU_FAST_VELOCITY_FOR_SWIPE_FOLLOW_DIRECTION 1200 #define STATUS_BAR_HEIGHT 20 static SlideNavigationController *singletonInstance; #pragma mark - Initialization - + (SlideNavigationController *)sharedInstance { return singletonInstance; } - (id)init { if (self = [super init]) { [self setup]; } return self; } - (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self setup]; } return self; } - (id)initWithRootViewController:(UIViewController *)rootViewController { if (self = [super initWithRootViewController:rootViewController]) { [self setup]; } return self; } - (void)setup { self.landscapeSlideOffset = MENU_DEFAULT_SLIDE_OFFSET; self.portraitSlideOffset = MENU_DEFAULT_SLIDE_OFFSET; self.avoidSwitchingToSameClassViewController = YES; singletonInstance = self; self.delegate = self; self.view.layer.shadowColor = [UIColor darkGrayColor].CGColor; self.view.layer.shadowRadius = MENU_SHADOW_RADIUS; self.view.layer.shadowOpacity = MENU_SHADOW_OPACITY; self.view.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.view.bounds].CGPath; self.view.layer.shouldRasterize = YES; self.view.layer.rasterizationScale = [UIScreen mainScreen].scale; [self setEnableSwipeGesture:YES]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; // Update shadow size self.view.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.view.bounds].CGPath; } - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; // Avoid an ugnly shadow in background while rotating self.view.layer.shadowOpacity = 0; } - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { [super didRotateFromInterfaceOrientation:fromInterfaceOrientation]; // Update rotation animation [self updateMenuFrameAndTransformAccordingToOrientation]; self.view.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.view.bounds].CGPath; // we set shadowOpacity to 0 in willRotateToInterfaceOrientation, after the rotation we want to add the shadow back self.view.layer.shadowOpacity = MENU_SHADOW_OPACITY; } - (void)updateMenuFrameAndTransformAccordingToOrientation { // Animate rotatation when menu is open and device rotates CGAffineTransform transform = self.view.transform; self.leftMenu.view.transform = transform; self.rightMenu.view.transform = transform; self.leftMenu.view.frame = [self initialRectForMenu]; self.rightMenu.view.frame = [self initialRectForMenu]; } #pragma mark - Public Methods - - (void)switchToViewController:(UIViewController *)viewController withCompletion:(void (^)())completion { if (self.avoidSwitchingToSameClassViewController && [self.topViewController isKindOfClass:viewController.class]) { [self closeMenuWithCompletion:completion]; return; } if ([self isMenuOpen]) { [UIView animateWithDuration:MENU_SLIDE_ANIMATION_DURATION delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ CGFloat width = self.horizontalSize; CGFloat moveLocation = (self.horizontalLocation> 0) ? width : -1*width; [self moveHorizontallyToLocation:moveLocation]; } completion:^(BOOL finished) { [super popToRootViewControllerAnimated:NO]; [super pushViewController:viewController animated:NO]; [self closeMenuWithCompletion:^{ if (completion) completion(); }]; }]; } else { [super popToRootViewControllerAnimated:NO]; [super pushViewController:viewController animated:YES]; if (completion) completion(); } } - (void)closeMenuWithCompletion:(void (^)())completion { [self closeMenuWithDuration:MENU_SLIDE_ANIMATION_DURATION andCompletion:completion]; } - (void)openMenu:(Menu)menu withCompletion:(void (^)())completion { [self openMenu:menu withDuration:MENU_SLIDE_ANIMATION_DURATION andCompletion:completion]; } - (void)toggleLeftMenu { [self toggleMenu:MenuLeft withCompletion:nil]; } - (void)toggleRightMenu { [self toggleMenu:MenuRight withCompletion:nil]; } - (BOOL)isMenuOpen { return (self.horizontalLocation == 0) ? NO : YES; } #pragma mark - Override Methods - - (NSArray *)popToRootViewControllerAnimated:(BOOL)animated { if ([self isMenuOpen]) { [self closeMenuWithCompletion:^{ [super popToRootViewControllerAnimated:animated]; }]; } else { return [super popToRootViewControllerAnimated:animated]; } return nil; } - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { if ([self isMenuOpen]) { [self closeMenuWithCompletion:^{ [super pushViewController:viewController animated:animated]; }]; } else { [super pushViewController:viewController animated:animated]; } } - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated { if ([self isMenuOpen]) { [self closeMenuWithCompletion:^{ [super popToViewController:viewController animated:animated]; }]; } else { return [super popToViewController:viewController animated:animated]; } return nil; } #pragma mark - Private Methods - - (void)toggleMenu:(Menu)menu withCompletion:(void (^)())completion { if ([self isMenuOpen]) [self closeMenuWithCompletion:completion]; else [self openMenu:menu withCompletion:completion]; } - (UIBarButtonItem *)barButtonItemForMenu:(Menu)menu { SEL selector = (menu == MenuLeft) ? @selector(leftMenuSelected:) : @selector(righttMenuSelected:); UIBarButtonItem *customButton = (menu == MenuLeft) ? self.leftBarButtonItem : self.rightBarButtonItem; if (customButton) { customButton.action = selector; customButton.target = self; return customButton; } else { UIImage *image = [UIImage imageNamed:MENU_IMAGE]; return [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:self action:selector]; } } - (BOOL)shouldDisplayMenu:(Menu)menu forViewController:(UIViewController *)vc { if (menu == MenuRight) { if ([vc respondsToSelector:@selector(slideNavigationControllerShouldDisplayRightMenu)] && [(UIViewController *)vc slideNavigationControllerShouldDisplayRightMenu]) { return YES; } } if (menu == MenuLeft) { if ([vc respondsToSelector:@selector(slideNavigationControllerShouldDisplayLeftMenu)] && [(UIViewController *)vc slideNavigationControllerShouldDisplayLeftMenu]) { return YES; } } return NO; } - (void)openMenu:(Menu)menu withDuration:(float)duration andCompletion:(void (^)())completion { self.topViewController.view.userInteractionEnabled = NO; [self.topViewController.navigationController.view addGestureRecognizer:self.tapRecognizer]; [self prepareMenuForReveal:menu forcePrepare:NO]; [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ CGRect rect = self.view.frame; CGFloat width = self.horizontalSize; rect.origin.x = (menu == MenuLeft) ? (width - self.slideOffset) : ((width - self.slideOffset )* -1); [self moveHorizontallyToLocation:rect.origin.x]; } completion:^(BOOL finished) { if (completion) completion(); }]; } - (void)closeMenuWithDuration:(float)duration andCompletion:(void (^)())completion { self.topViewController.view.userInteractionEnabled = YES; [self.topViewController.navigationController.view removeGestureRecognizer:self.tapRecognizer]; [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ CGRect rect = self.view.frame; rect.origin.x = 0; [self moveHorizontallyToLocation:rect.origin.x]; } completion:^(BOOL finished) { if (completion) completion(); }]; } - (void)moveHorizontallyToLocation:(CGFloat)location { CGRect rect = self.view.frame; UIInterfaceOrientation orientation = self.interfaceOrientation; Menu menu = (self.horizontalLocation >= 0 && location >= 0) ? MenuLeft : MenuRight; if (UIInterfaceOrientationIsLandscape(orientation)) { rect.origin.x = 0; rect.origin.y = (orientation == UIInterfaceOrientationLandscapeRight) ? location : location*-1; } else { rect.origin.x = (orientation == UIInterfaceOrientationPortrait) ? location : location*-1; rect.origin.y = 0; } self.view.frame = rect; [self updateMenuAnimation:menu]; } - (void)updateMenuAnimation:(Menu)menu { CGFloat progress = (menu == MenuLeft) ? (self.horizontalLocation / (self.horizontalSize - self.slideOffset)) : (self.horizontalLocation / ((self.horizontalSize - self.slideOffset) * -1)); [self.menuRevealAnimator animateMenu:menu withProgress:progress]; } - (CGRect)initialRectForMenu { CGRect rect = self.view.frame; rect.origin.x = 0; rect.origin.y = 0; BOOL isIos7 = SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0"); if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) { if (!isIos7) { // For some reasons in landscape belos the status bar is considered y=0, but in portrait it's considered y=20 rect.origin.x = (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight) ? 0 : STATUS_BAR_HEIGHT; rect.size.width = self.view.frame.size.width-STATUS_BAR_HEIGHT; } } else { if (!isIos7) { // For some reasons in landscape belos the status bar is considered y=0, but in portrait it's considered y=20 rect.origin.y = (self.interfaceOrientation == UIInterfaceOrientationPortrait) ? STATUS_BAR_HEIGHT : 0; rect.size.height = self.view.frame.size.height-STATUS_BAR_HEIGHT; } } return rect; } - (void)prepareMenuForReveal:(Menu)menu forcePrepare:(BOOL)forcePrepare { UIViewController *menuViewController = (menu == MenuLeft) ? self.leftMenu : self.rightMenu; UIViewController *removingMenuViewController = (menu == MenuLeft) ? self.rightMenu : self.leftMenu; // If menu is already open don't prepare, unless forcePrepare is set to true if ([self isMenuOpen] && !forcePrepare) return; [removingMenuViewController.view removeFromSuperview]; [self.view.window insertSubview:menuViewController.view atIndex:0]; [self updateMenuFrameAndTransformAccordingToOrientation]; [self.menuRevealAnimator prepareMenuForAnimation:menu]; } - (CGFloat)horizontalLocation { CGRect rect = self.view.frame; UIInterfaceOrientation orientation = self.interfaceOrientation; if (UIInterfaceOrientationIsLandscape(orientation)) { return (orientation == UIInterfaceOrientationLandscapeRight) ? rect.origin.y : rect.origin.y*-1; } else { return (orientation == UIInterfaceOrientationPortrait) ? rect.origin.x : rect.origin.x*-1; } } - (CGFloat)horizontalSize { CGRect rect = self.view.frame; UIInterfaceOrientation orientation = self.interfaceOrientation; if (UIInterfaceOrientationIsLandscape(orientation)) { return rect.size.height; } else { return rect.size.width; } } #pragma mark - UINavigationControllerDelegate Methods - - (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated { if ([self shouldDisplayMenu:MenuLeft forViewController:viewController]) viewController.navigationItem.leftBarButtonItem = [self barButtonItemForMenu:MenuLeft]; if ([self shouldDisplayMenu:MenuRight forViewController:viewController]) viewController.navigationItem.rightBarButtonItem = [self barButtonItemForMenu:MenuRight]; } - (CGFloat)slideOffset { return (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) ? self.landscapeSlideOffset : self.portraitSlideOffset; } #pragma mark - IBActions - - (void)leftMenuSelected:(id)sender { if ([self isMenuOpen]) [self closeMenuWithCompletion:nil]; else [self openMenu:MenuLeft withCompletion:nil]; } - (void)righttMenuSelected:(id)sender { if ([self isMenuOpen]) [self closeMenuWithCompletion:nil]; else [self openMenu:MenuRight withCompletion:nil]; } #pragma mark - Gesture Recognizing - - (void)tapDetected:(UITapGestureRecognizer *)tapRecognizer { [self closeMenuWithCompletion:nil]; } - (void)panDetected:(UIPanGestureRecognizer *)aPanRecognizer { CGPoint translation = [aPanRecognizer translationInView:aPanRecognizer.view]; CGPoint velocity = [aPanRecognizer velocityInView:aPanRecognizer.view]; NSInteger movement = translation.x - self.draggingPoint.x; static Menu lastMenu; Menu menu = (self.horizontalLocation > 0 || (self.horizontalLocation == 0 && movement > 0) ) ? MenuLeft : MenuRight; if (aPanRecognizer.state == UIGestureRecognizerStateBegan) { if (![self isMenuOpen]) [self prepareMenuForReveal:menu forcePrepare:YES]; self.draggingPoint = translation; lastMenu = menu; } else if (aPanRecognizer.state == UIGestureRecognizerStateChanged) { static CGFloat lastHorizontalLocation = 0; CGFloat newHorizontalLocation = [self horizontalLocation]; // Force prepare menu when slides quickly between left and right menu if (lastMenu != menu) [self prepareMenuForReveal:menu forcePrepare:YES]; lastHorizontalLocation = newHorizontalLocation; newHorizontalLocation += movement; if (newHorizontalLocation >= self.minXForDragging && newHorizontalLocation <= self.maxXForDragging) [self moveHorizontallyToLocation:newHorizontalLocation]; self.draggingPoint = translation; lastMenu = menu; } else if (aPanRecognizer.state == UIGestureRecognizerStateEnded) { NSInteger currentX = [self horizontalLocation]; NSInteger currentXOffset = (currentX > 0) ? currentX : currentX * -1; NSInteger positiveVelocity = (velocity.x > 0) ? velocity.x : velocity.x * -1; // If the speed is high enough follow direction if (positiveVelocity >= MENU_FAST_VELOCITY_FOR_SWIPE_FOLLOW_DIRECTION) { Menu menu = (velocity.x > 0) ? MenuLeft : MenuRight; // Moving Right if (velocity.x > 0) { if (currentX > 0) { if ([self shouldDisplayMenu:menu forViewController:self.visibleViewController]) [self openMenu:(velocity.x > 0) ? MenuLeft : MenuRight withDuration:MENU_QUICK_SLIDE_ANIMATION_DURATION andCompletion:nil]; } else { [self closeMenuWithDuration:MENU_QUICK_SLIDE_ANIMATION_DURATION andCompletion:nil]; } } // Moving Left else { if (currentX > 0) { [self closeMenuWithDuration:MENU_QUICK_SLIDE_ANIMATION_DURATION andCompletion:nil]; } else { if ([self shouldDisplayMenu:menu forViewController:self.visibleViewController]) [self openMenu:(velocity.x > 0) ? MenuLeft : MenuRight withDuration:MENU_QUICK_SLIDE_ANIMATION_DURATION andCompletion:nil]; } } } else { if (currentXOffset < (self.horizontalSize - self.slideOffset)/2) [self closeMenuWithCompletion:nil]; else [self openMenu:(currentX > 0) ? MenuLeft : MenuRight withCompletion:nil]; } lastMenu = -1; } } - (NSInteger)minXForDragging { if ([self shouldDisplayMenu:MenuRight forViewController:self.topViewController]) { return (self.horizontalSize - self.slideOffset) * -1; } return 0; } - (NSInteger)maxXForDragging { if ([self shouldDisplayMenu:MenuLeft forViewController:self.topViewController]) { return self.horizontalSize - self.slideOffset; } return 0; } #pragma mark - Setter & Getter - - (UITapGestureRecognizer *)tapRecognizer { if (!_tapRecognizer) { _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapDetected:)]; } return _tapRecognizer; } - (UIPanGestureRecognizer *)panRecognizer { if (!_panRecognizer) { _panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panDetected:)]; } return _panRecognizer; } - (void)setEnableSwipeGesture:(BOOL)markEnableSwipeGesture { _enableSwipeGesture = markEnableSwipeGesture; if (_enableSwipeGesture) { [self.view addGestureRecognizer:self.panRecognizer]; } else { [self.view removeGestureRecognizer:self.panRecognizer]; } } - (void)setMenuRevealAnimator:(id)menuRevealAnimator { [self.menuRevealAnimator clear]; _menuRevealAnimator = menuRevealAnimator; } @end