diff --git a/e2e/SetRoot.test.js b/e2e/SetRoot.test.js index f4f2d472a8..b46310ac35 100644 --- a/e2e/SetRoot.test.js +++ b/e2e/SetRoot.test.js @@ -51,3 +51,14 @@ describe('SetRoot', () => { await elementById(TestIDs.OK_BUTTON).tap(); }); }); + + it.e2e(':ios: set root with left and right side menus - menu visibility', async () => { + await elementById(TestIDs.SET_ROOT_WITH_MENUS).tap(); + await elementById(TestIDs.TOGGLE_SIDE_MENU_OPEN_MODE_BTN).tap(); + await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); + await elementById(TestIDs.CLOSE_LEFT_SIDE_MENU_BTN).tap(); + await expect(elementById(TestIDs.CLOSE_LEFT_SIDE_MENU_BTN)).toBeNotVisible(); + await elementById(TestIDs.OPEN_RIGHT_SIDE_MENU_BTN).tap(); + await elementById(TestIDs.CLOSE_RIGHT_SIDE_MENU_BTN).tap(); + await expect(elementById(TestIDs.CLOSE_RIGHT_SIDE_MENU_BTN)).toBeNotVisible(); + }); diff --git a/e2e/SideMenu.test.js b/e2e/SideMenu.test.js index 6199e25823..8e86ff9587 100644 --- a/e2e/SideMenu.test.js +++ b/e2e/SideMenu.test.js @@ -1,78 +1,91 @@ import Utils from './Utils'; import TestIDs from '../playground/src/testIDs'; -const { elementByLabel, elementById } = Utils; +const {elementByLabel, elementById, expectImagesToBeEqual} = Utils; -describe('SideMenu', () => { +describe.each(['aboveContent', 'pushContent'])('SideMenu', (openMode) => { beforeEach(async () => { - await device.launchApp({ newInstance: true }); + await device.launchApp({newInstance: true}); await elementById(TestIDs.SIDE_MENU_BTN).tap(); - }); - it('close SideMenu and push to stack with static id', async () => { - await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); - await elementById(TestIDs.LEFT_SIDE_MENU_PUSH_BTN).tap(); - await elementById(TestIDs.CLOSE_LEFT_SIDE_MENU_BTN).tap(); - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); - await elementById(TestIDs.POP_BTN).tap(); - await expect(elementById(TestIDs.CENTER_SCREEN_HEADER)).toBeVisible(); + if (openMode === 'aboveContent') { + await elementById(TestIDs.TOGGLE_SIDE_MENU_OPEN_MODE_BTN).tap(); + } }); - it('Push to stack with static id and close SideMenu with screen options', async () => { - await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); - await elementById(TestIDs.LEFT_SIDE_MENU_PUSH_AND_CLOSE_BTN).tap(); - await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); - await elementById(TestIDs.POP_BTN).tap(); - await expect(elementById(TestIDs.CENTER_SCREEN_HEADER)).toBeVisible(); - }); + describe(`Open mode '${openMode}'`, () => { + it('close SideMenu and push to stack with static id', async () => { + await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); + await elementById(TestIDs.LEFT_SIDE_MENU_PUSH_BTN).tap(); + await elementById(TestIDs.CLOSE_LEFT_SIDE_MENU_BTN).tap(); + await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + await elementById(TestIDs.POP_BTN).tap(); + await expect(elementById(TestIDs.CENTER_SCREEN_HEADER)).toBeVisible(); + }); - it('side menu visibility - left', async () => { - await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); - await elementById(TestIDs.CLOSE_LEFT_SIDE_MENU_BTN).tap(); - await expect(elementById(TestIDs.CLOSE_LEFT_SIDE_MENU_BTN)).toBeNotVisible(); - }); + it('Push to stack with static id and close SideMenu with screen options', async () => { + await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); + await elementById(TestIDs.LEFT_SIDE_MENU_PUSH_AND_CLOSE_BTN).tap(); + await expect(elementById(TestIDs.PUSHED_SCREEN_HEADER)).toBeVisible(); + await elementById(TestIDs.POP_BTN).tap(); + await expect(elementById(TestIDs.CENTER_SCREEN_HEADER)).toBeVisible(); + }); - it('side menu visibility - right', async () => { - await elementById(TestIDs.OPEN_RIGHT_SIDE_MENU_BTN).tap(); - await elementById(TestIDs.CLOSE_RIGHT_SIDE_MENU_BTN).tap(); - await expect(elementById(TestIDs.CLOSE_RIGHT_SIDE_MENU_BTN)).toBeNotVisible(); - }); + it('side menu visibility - left', async () => { + await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); + await elementById(TestIDs.CLOSE_LEFT_SIDE_MENU_BTN).tap(); + await expect(elementById(TestIDs.CLOSE_LEFT_SIDE_MENU_BTN)).toBeNotVisible(); + }); - it.e2e('should rotate', async () => { - await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); - await device.setOrientation('landscape'); - await expect(elementById(TestIDs.LEFT_SIDE_MENU_PUSH_BTN)).toBeVisible(); - }); + it('side menu visibility - right', async () => { + await elementById(TestIDs.OPEN_RIGHT_SIDE_MENU_BTN).tap(); + await elementById(TestIDs.CLOSE_RIGHT_SIDE_MENU_BTN).tap(); + await expect(elementById(TestIDs.CLOSE_RIGHT_SIDE_MENU_BTN)).toBeNotVisible(); + }); - it.e2e(':ios: rotation should update drawer height', async () => { - await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); - await expect(elementByLabel('left drawer height: 869')).toBeVisible(); - await device.setOrientation('landscape'); - await expect(elementByLabel('left drawer height: 428')).toBeVisible(); - await device.setOrientation('portrait'); - await expect(elementByLabel('left drawer height: 869')).toBeVisible(); - }); + it.e2e('should rotate', async () => { + await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); + await device.setOrientation('landscape'); + await expect(elementById(TestIDs.LEFT_SIDE_MENU_PUSH_BTN)).toBeVisible(); + }); - it.e2e('should set left drawer width', async () => { - await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); - await expect(elementById(TestIDs.SIDE_MENU_LEFT_DRAWER_HEIGHT_TEXT)).toBeVisible(); - await expect(elementByLabel('left drawer width: 250')).toBeVisible(); - }); + it.e2e(':ios: rotation should update drawer height', async () => { + await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); + await expect(elementByLabel('left drawer height: 869')).toBeVisible(); + await device.setOrientation('landscape'); + await expect(elementByLabel('left drawer height: 428')).toBeVisible(); + await device.setOrientation('portrait'); + await expect(elementByLabel('left drawer height: 869')).toBeVisible(); + }); - it.e2e('should change left drawer width', async () => { - await elementById(TestIDs.CHANGE_LEFT_SIDE_MENU_WIDTH_BTN).tap(); - await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); - await expect(elementByLabel('left drawer width: 50')).toBeVisible(); - }); + it.e2e('should set left drawer width', async () => { + await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); + await expect(elementById(TestIDs.SIDE_MENU_LEFT_DRAWER_HEIGHT_TEXT)).toBeVisible(); + await expect(elementByLabel('left drawer width: 250')).toBeVisible(); + }); - it.e2e('should set right drawer width', async () => { - await elementById(TestIDs.OPEN_RIGHT_SIDE_MENU_BTN).tap(); - await expect(elementByLabel('right drawer width: 250')).toBeVisible(); - }); + it.e2e('should change left drawer width', async () => { + await elementById(TestIDs.CHANGE_LEFT_SIDE_MENU_WIDTH_BTN).tap(); + await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); + await expect(elementByLabel('left drawer width: 50')).toBeVisible(); + }); + + it.e2e('should set right drawer width', async () => { + await elementById(TestIDs.OPEN_RIGHT_SIDE_MENU_BTN).tap(); + await expect(elementByLabel('right drawer width: 250')).toBeVisible(); + }); + + it.e2e('should change right drawer width', async () => { + await elementById(TestIDs.CHANGE_RIGHT_SIDE_MENU_WIDTH_BTN).tap(); + await elementById(TestIDs.OPEN_RIGHT_SIDE_MENU_BTN).tap(); + await expect(elementByLabel('right drawer width: 50')).toBeVisible(); + }); - it.e2e('should change right drawer width', async () => { - await elementById(TestIDs.CHANGE_RIGHT_SIDE_MENU_WIDTH_BTN).tap(); - await elementById(TestIDs.OPEN_RIGHT_SIDE_MENU_BTN).tap(); - await expect(elementByLabel('right drawer width: 50')).toBeVisible(); + it.e2e(':ios: should render side menu correctly', async () => { + await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap(); + const snapshottedImagePath = `./e2e/assets/side_menu.${openMode}.png`; + const actual = await elementById('SideMenuContainer').takeScreenshot(`side_menu_${openMode}`); + expectImagesToBeEqual(actual, snapshottedImagePath); + }); }); }); diff --git a/e2e/assets/side_menu.aboveContent.png b/e2e/assets/side_menu.aboveContent.png new file mode 100644 index 0000000000..f6a1542fa7 Binary files /dev/null and b/e2e/assets/side_menu.aboveContent.png differ diff --git a/e2e/assets/side_menu.pushContent.png b/e2e/assets/side_menu.pushContent.png new file mode 100644 index 0000000000..5a8b71de73 Binary files /dev/null and b/e2e/assets/side_menu.pushContent.png differ diff --git a/lib/ios/RNNSideMenu/MMDrawerController/MMDrawerController.h b/lib/ios/RNNSideMenu/MMDrawerController/MMDrawerController.h index b874677960..127572b6e9 100644 --- a/lib/ios/RNNSideMenu/MMDrawerController/MMDrawerController.h +++ b/lib/ios/RNNSideMenu/MMDrawerController/MMDrawerController.h @@ -127,6 +127,14 @@ typedef void (^MMDrawerControllerDrawerVisualStateBlock)(MMDrawerController *dra @interface MMDrawerController : UIViewController +/** + Enum defining how the drawer opens + */ + typedef NS_ENUM(NSInteger, MMDrawerOpenMode) { + MMDrawerOpenModePushContent = 0, // Original behavior - pushes content aside + MMDrawerOpenModeAboveContent = 1, // Overlay behavior - opens above content +}; + ///--------------------------------------- /// @name Accessing Drawer Container View Controller Properties ///--------------------------------------- @@ -213,6 +221,18 @@ typedef void (^MMDrawerControllerDrawerVisualStateBlock)(MMDrawerController *dra @property(nonatomic, assign) BOOL shouldStretchLeftDrawer; @property(nonatomic, assign) BOOL shouldStretchRightDrawer; +/** + * Specifies how the drawer should open relative to the center content. + * + * Possible values: + * - MMDrawerOpenModePushContent: The drawer will push the center content aside when opening (traditional behavior). + * - MMDrawerOpenModeAboveContent: The drawer will open above the center content with a semi-transparent overlay. + * + * By default, this value is set to MMDrawerOpenModePushContent. + */ +@property(nonatomic, assign) MMDrawerOpenMode leftDrawerOpenMode; +@property(nonatomic, assign) MMDrawerOpenMode rightDrawerOpenMode; + /** The current open side of the drawer. @@ -600,4 +620,6 @@ typedef void (^MMDrawerControllerDrawerVisualStateBlock)(MMDrawerController *dra (BOOL (^)(MMDrawerController *drawerController, UIGestureRecognizer *gesture, UITouch *touch))gestureShouldRecognizeTouchBlock; +- (void)side:(MMDrawerSide)drawerSide openMode:(MMDrawerOpenMode)openMode; + @end diff --git a/lib/ios/RNNSideMenu/MMDrawerController/MMDrawerController.m b/lib/ios/RNNSideMenu/MMDrawerController/MMDrawerController.m index 4981b2cb91..c9b1b8b291 100644 --- a/lib/ios/RNNSideMenu/MMDrawerController/MMDrawerController.m +++ b/lib/ios/RNNSideMenu/MMDrawerController/MMDrawerController.m @@ -152,6 +152,8 @@ @interface MMDrawerController () { CGFloat _maximumRightDrawerWidth; CGFloat _maximumLeftDrawerWidth; UIColor *_statusBarViewBackgroundColor; + MMDrawerOpenMode _leftDrawerOpenMode; + MMDrawerOpenMode _rightDrawerOpenMode; } @property(nonatomic, assign, readwrite) MMDrawerSide openSide; @@ -160,13 +162,15 @@ @interface MMDrawerController () { @property(nonatomic, strong) MMDrawerCenterContainerView *centerContainerView; @property(nonatomic, strong) UIView *dummyStatusBarView; -@property(nonatomic, assign) CGRect startingPanRect; @property(nonatomic, copy) MMDrawerControllerDrawerVisualStateBlock drawerVisualState; @property(nonatomic, copy) MMDrawerGestureShouldRecognizeTouchBlock gestureShouldRecognizeTouch; @property(nonatomic, copy) MMDrawerGestureCompletionBlock gestureStart; @property(nonatomic, copy) MMDrawerGestureCompletionBlock gestureCompletion; @property(nonatomic, assign, getter=isAnimatingDrawer) BOOL animatingDrawer; @property(nonatomic, strong) UIGestureRecognizer *pan; +@property (nonatomic, strong) UIView *centerContentOverlay; +@property(nonatomic, assign) CGRect startingPanRect; +@property(nonatomic, assign) MMDrawerSide panDrawerSide; @end @@ -227,6 +231,8 @@ - (void)commonSetup { [self setShowsShadow:YES]; [self setShouldStretchLeftDrawer:YES]; [self setShouldStretchRightDrawer:YES]; + [self side:MMDrawerSideRight openMode:MMDrawerOpenModePushContent]; + [self side:MMDrawerSideLeft openMode:MMDrawerOpenModePushContent]; [self setOpenDrawerGestureModeMask:MMOpenDrawerGestureModeNone]; [self setCloseDrawerGestureModeMask:MMCloseDrawerGestureModeNone]; @@ -323,52 +329,126 @@ - (void)closeDrawerAnimated:(BOOL)animated } } else { [self setAnimatingDrawer:animated]; - CGRect newFrame = self.childControllerContainerView.bounds; + MMDrawerSide visibleSide = self.openSide; - CGFloat distance = ABS(CGRectGetMinX(self.centerContainerView.frame)); - NSTimeInterval duration = MAX(distance / ABS(velocity), MMDrawerMinimumAnimationDuration); + if (visibleSide == MMDrawerSideNone) { + [self setAnimatingDrawer:NO]; + if (completion) { + completion(NO); + } + return; + } - BOOL leftDrawerVisible = CGRectGetMinX(self.centerContainerView.frame) > 0; - BOOL rightDrawerVisible = CGRectGetMinX(self.centerContainerView.frame) < 0; + UIViewController *sideDrawerViewController = [self sideDrawerViewControllerForSide:visibleSide]; + [sideDrawerViewController beginAppearanceTransition:NO animated:animated]; - MMDrawerSide visibleSide = MMDrawerSideNone; - CGFloat percentVisble = 0.0; + MMDrawerOpenMode openMode = [self getDrawerOpenMode:visibleSide]; - if (leftDrawerVisible) { - CGFloat visibleDrawerPoints = CGRectGetMinX(self.centerContainerView.frame); - percentVisble = MAX(0.0, visibleDrawerPoints / self.maximumLeftDrawerWidth); - visibleSide = MMDrawerSideLeft; - } else if (rightDrawerVisible) { - CGFloat visibleDrawerPoints = CGRectGetWidth(self.centerContainerView.frame) - - CGRectGetMaxX(self.centerContainerView.frame); - percentVisble = MAX(0.0, visibleDrawerPoints / self.maximumRightDrawerWidth); - visibleSide = MMDrawerSideRight; + if (openMode == MMDrawerOpenModeAboveContent) { + [self closeDrawerInOverlayMode:sideDrawerViewController + visibleSide:visibleSide + animated:animated + velocity:velocity + animationOptions:options + completion:completion]; + } else { + [self closeDrawerInPushMode:sideDrawerViewController + visibleSide:visibleSide + animated:animated + velocity:velocity + animationOptions:options + completion:completion]; } + } +} - UIViewController *sideDrawerViewController = - [self sideDrawerViewControllerForSide:visibleSide]; +- (void)closeDrawerInOverlayMode:(UIViewController *)sideDrawerViewController + visibleSide:(MMDrawerSide)visibleSide + animated:(BOOL)animated + velocity:(CGFloat)velocity + animationOptions:(UIViewAnimationOptions)options + completion:(void (^)(BOOL finished))completion { + CGFloat maximumDrawerWidth = [self maximumDrawerWidthForSide:visibleSide]; - [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:percentVisble]; + CGRect currentFrame = sideDrawerViewController.view.frame; + CGRect finalFrame = currentFrame; - [sideDrawerViewController beginAppearanceTransition:NO animated:animated]; + // Set final position based on side + if (visibleSide == MMDrawerSideLeft) { + finalFrame.origin.x = -maximumDrawerWidth; // Off-screen left + } else { // MMDrawerSideRight + finalFrame.origin.x = self.view.bounds.size.width; // Off-screen right + } - [UIView animateWithDuration:(animated ? duration : 0.0) - delay:0.0 - options:options - animations:^{ - [self setNeedsStatusBarAppearanceUpdateIfSupported]; - [self.centerContainerView setFrame:newFrame withLayoutAlpha:0.0]; - [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:0.0]; - } - completion:^(BOOL finished) { - [sideDrawerViewController endAppearanceTransition]; - [self setOpenSide:MMDrawerSideNone]; - [self resetDrawerVisualStateForDrawerSide:visibleSide]; - [self setAnimatingDrawer:NO]; - if (completion) { - completion(finished); - } - }]; + // Ensure overlay is in view hierarchy + if (self.centerContentOverlay && self.centerContentOverlay.superview == nil) { + [self.centerContainerView addSubview:self.centerContentOverlay]; + [self.centerContainerView bringSubviewToFront:self.centerContentOverlay]; + self.centerContentOverlay.alpha = 0.5; + } + + CGFloat distance = ABS(currentFrame.origin.x - finalFrame.origin.x); + NSTimeInterval duration = [self animationDurationForDistance:distance velocity:velocity]; + + [UIView animateWithDuration:(animated ? duration : 0.0) + delay:0.0 + options:options + animations:^{ + [self setNeedsStatusBarAppearanceUpdateIfSupported]; + [sideDrawerViewController.view setFrame:finalFrame]; + self.centerContentOverlay.alpha = 0.0; + [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:0.0]; + } + completion:^(BOOL finished) { + [self completeDrawerClosingForSide:visibleSide + sideDrawerViewController:sideDrawerViewController + finished:finished + completion:completion]; + [self.centerContentOverlay removeFromSuperview]; + }]; +} + +- (void)closeDrawerInPushMode:(UIViewController *)sideDrawerViewController + visibleSide:(MMDrawerSide)visibleSide + animated:(BOOL)animated + velocity:(CGFloat)velocity + animationOptions:(UIViewAnimationOptions)options + completion:(void (^)(BOOL finished))completion { + + CGRect newFrame = self.childControllerContainerView.bounds; + + CGFloat distance = ABS(CGRectGetMinX(self.centerContainerView.frame)); + NSTimeInterval duration = [self animationDurationForDistance:distance velocity:velocity]; + sideDrawerViewController.view.hidden = NO; + + [UIView animateWithDuration:(animated ? duration : 0.0) + delay:0.0 + options:options + animations:^{ + [self setNeedsStatusBarAppearanceUpdateIfSupported]; + [self.centerContainerView setFrame:newFrame]; + [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:0.0]; + } + completion:^(BOOL finished) { + [self completeDrawerClosingForSide:visibleSide + sideDrawerViewController:sideDrawerViewController + finished:finished + completion:completion]; + }]; +} + +- (void)completeDrawerClosingForSide:(MMDrawerSide)visibleSide + sideDrawerViewController:(UIViewController *)sideDrawerViewController + finished:(BOOL)finished + completion:(void (^)(BOOL finished))completion { + [sideDrawerViewController endAppearanceTransition]; + + [self setOpenSide:MMDrawerSideNone]; + [self resetDrawerVisualStateForDrawerSide:visibleSide]; + [self setAnimatingDrawer:NO]; + + if (completion) { + completion(finished); } } @@ -397,50 +477,207 @@ - (void)openDrawerSide:(MMDrawerSide)drawerSide } } else { [self setAnimatingDrawer:animated]; - UIViewController *sideDrawerViewController = - [self sideDrawerViewControllerForSide:drawerSide]; + UIViewController *sideDrawerViewController = [self sideDrawerViewControllerForSide:drawerSide]; + if (self.openSide != drawerSide) { [self prepareToPresentDrawer:drawerSide animated:animated]; } if (sideDrawerViewController) { - CGRect newFrame; - CGRect oldFrame = self.centerContainerView.frame; - if (drawerSide == MMDrawerSideLeft) { - newFrame = self.centerContainerView.frame; - newFrame.origin.x = self.maximumLeftDrawerWidth; + MMDrawerOpenMode openMode = [self getDrawerOpenMode:drawerSide]; + + if (openMode == MMDrawerOpenModeAboveContent) { + [self openDrawerInOverlayMode:sideDrawerViewController + drawerSide:drawerSide + animated:animated + velocity:velocity + animationOptions:options + completion:completion]; } else { - newFrame = self.centerContainerView.frame; - newFrame.origin.x = 0 - self.maximumRightDrawerWidth; + [self openDrawerInPushMode:sideDrawerViewController + drawerSide:drawerSide + animated:animated + velocity:velocity + animationOptions:options + completion:completion]; } + } + } +} - CGFloat distance = ABS(CGRectGetMinX(oldFrame) - newFrame.origin.x); - NSTimeInterval duration = - MAX(distance / ABS(velocity), MMDrawerMinimumAnimationDuration); - - [UIView animateWithDuration:(animated ? duration : 0.0) - delay:0.0 - options:options - animations:^{ - [self setNeedsStatusBarAppearanceUpdateIfSupported]; - [self.centerContainerView setFrame:newFrame withLayoutAlpha:1.0]; - [self updateDrawerVisualStateForDrawerSide:drawerSide percentVisible:1.0]; - } - completion:^(BOOL finished) { - // End the appearance transition if it already wasn't open. - if (drawerSide != self.openSide) { - [sideDrawerViewController endAppearanceTransition]; - } - [self setOpenSide:drawerSide]; +- (void)openDrawerInOverlayMode:(UIViewController *)sideDrawerViewController + drawerSide:(MMDrawerSide)drawerSide + animated:(BOOL)animated + velocity:(CGFloat)velocity + animationOptions:(UIViewAnimationOptions)options + completion:(void (^)(BOOL finished))completion { - [self resetDrawerVisualStateForDrawerSide:drawerSide]; - [self setAnimatingDrawer:NO]; - if (completion) { - completion(finished); - } - }]; + CGFloat maximumDrawerWidth = [self maximumDrawerWidthForSide:drawerSide]; + + CGRect drawerFrame = sideDrawerViewController.view.frame; + CGRect initialFrame = sideDrawerViewController.view.frame; + + drawerFrame.size.width = maximumDrawerWidth; + initialFrame.size.width = maximumDrawerWidth; + + if (drawerSide == MMDrawerSideLeft) { + drawerFrame.origin.x = 0; // Final position + + if (self.openSide != drawerSide) { + initialFrame.origin.x = -maximumDrawerWidth; // Start off-screen + [sideDrawerViewController.view setFrame:initialFrame]; + } + } else { // MMDrawerSideRight + CGFloat screenWidth = self.view.bounds.size.width; + drawerFrame.origin.x = screenWidth - maximumDrawerWidth; // Final position + + if (self.openSide != drawerSide) { + initialFrame.origin.x = screenWidth; // Start off-screen + [sideDrawerViewController.view setFrame:initialFrame]; } } + + [self setupCenterContentOverlay]; + [self.centerContainerView addSubview:self.centerContentOverlay]; + [self.centerContainerView bringSubviewToFront:self.centerContentOverlay]; + self.centerContentOverlay.alpha = 0.0; // Start transparent + + // Make sure drawer is visible and in front + sideDrawerViewController.view.hidden = NO; + [self.childControllerContainerView bringSubviewToFront:sideDrawerViewController.view]; + + CGFloat distance = ABS(initialFrame.origin.x - drawerFrame.origin.x); + NSTimeInterval duration = [self animationDurationForDistance:distance velocity:velocity]; + + [UIView animateWithDuration:(animated ? duration : 0.0) + delay:0.0 + options:options + animations:^{ + [self setNeedsStatusBarAppearanceUpdateIfSupported]; + [sideDrawerViewController.view setFrame:drawerFrame]; // Move to final position + self.centerContentOverlay.alpha = 0.5; + [self updateDrawerVisualStateForDrawerSide:drawerSide percentVisible:1.0]; + } + completion:^(BOOL finished) { + [self completeDrawerOpeningForSide:drawerSide + sideDrawerViewController:sideDrawerViewController + finished:finished + completion:completion]; + }]; +} + +- (void)openDrawerInPushMode:(UIViewController *)sideDrawerViewController + drawerSide:(MMDrawerSide)drawerSide + animated:(BOOL)animated + velocity:(CGFloat)velocity + animationOptions:(UIViewAnimationOptions)options + completion:(void (^)(BOOL finished))completion { + + CGRect oldFrame = self.centerContainerView.frame; + CGRect newFrame = self.centerContainerView.frame; + + if (drawerSide == MMDrawerSideLeft) { + newFrame.origin.x = self.maximumLeftDrawerWidth; + } else { + newFrame.origin.x = 0 - self.maximumRightDrawerWidth; + } + + CGFloat distance = ABS(CGRectGetMinX(oldFrame) - newFrame.origin.x); + NSTimeInterval duration = MAX(distance / ABS(velocity), MMDrawerMinimumAnimationDuration); + BOOL isGestureOpen = !CGRectIsNull(self.startingPanRect) && [self shouldStretchForSide:drawerSide]; + sideDrawerViewController.view.hidden = NO; + + [UIView animateWithDuration:(animated ? duration : 0.0) + delay:0.0 + options:options + animations:^{ + [self setNeedsStatusBarAppearanceUpdateIfSupported]; + [self.centerContainerView setFrame:newFrame withLayoutAlpha:1.0]; + [self updateDrawerVisualStateForDrawerSide:drawerSide percentVisible:1.0]; + } + completion:^(BOOL finished) { + [self completeDrawerOpeningForSide:drawerSide + sideDrawerViewController:sideDrawerViewController + finished:finished + completion:^(BOOL innerFinished) { + + if (isGestureOpen) { + [self applyBounceForDrawerSide:drawerSide]; + } + + if (completion) { + completion(innerFinished); + } + }]; + }]; +} + +- (void)applyBounceForDrawerSide:(MMDrawerSide)drawerSide { + UIViewController *sideDrawerViewController = [self sideDrawerViewControllerForSide:drawerSide]; + if (!sideDrawerViewController) return; + + CGRect centerFrame = self.centerContainerView.frame; + CGRect bounceFrame = centerFrame; + CGFloat bounceAmount = 15.0; + + if (drawerSide == MMDrawerSideLeft) { + bounceFrame.origin.x += bounceAmount; + } else { + bounceFrame.origin.x -= bounceAmount; + } + + CATransform3D originalTransform = sideDrawerViewController.view.layer.transform; + CGFloat scale = 1.0 + (bounceAmount / (drawerSide == MMDrawerSideLeft ? + self.maximumLeftDrawerWidth : + self.maximumRightDrawerWidth)); + + CATransform3D bounceTransform = CATransform3DMakeScale(scale, 1.0, 1.0); + + if (drawerSide == MMDrawerSideLeft) { + bounceTransform = CATransform3DTranslate(bounceTransform, + self.maximumLeftDrawerWidth * (scale - 1.0) / 2.0, + 0, 0); + } else { + bounceTransform = CATransform3DTranslate(bounceTransform, + -self.maximumRightDrawerWidth * (scale - 1.0) / 2.0, + 0, 0); + } + [UIView animateWithDuration:0.15 + delay:0.0 + options:UIViewAnimationOptionCurveEaseOut + animations:^{ + self.centerContainerView.frame = bounceFrame; + sideDrawerViewController.view.layer.transform = bounceTransform; + } + completion:^(BOOL bounceFinished) { + [UIView animateWithDuration:0.15 + delay:0.0 + options:UIViewAnimationOptionCurveEaseIn + animations:^{ + self.centerContainerView.frame = centerFrame; + sideDrawerViewController.view.layer.transform = originalTransform; + } + completion:nil]; + }]; +} + +- (void)completeDrawerOpeningForSide:(MMDrawerSide)drawerSide + sideDrawerViewController:(UIViewController *)sideDrawerViewController + finished:(BOOL)finished + completion:(void (^)(BOOL finished))completion { + + // End the appearance transition if it already wasn't open. + if (drawerSide != self.openSide) { + [sideDrawerViewController endAppearanceTransition]; + } + + [self setOpenSide:drawerSide]; + [self resetDrawerVisualStateForDrawerSide:drawerSide]; + [self setAnimatingDrawer:NO]; + + if (completion) { + completion(finished); + } } #pragma mark - Updating the Center View Controller @@ -801,7 +1038,7 @@ - (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:[UIColor blackColor]]; - + [self.view setAccessibilityIdentifier:@"SideMenuContainer"]; [self setupGestureRecognizers]; } @@ -1070,6 +1307,16 @@ - (void)setStatusBarViewBackgroundColor:(UIColor *)dummyStatusBarColor { [self.dummyStatusBarView setBackgroundColor:_statusBarViewBackgroundColor]; } +- (void)setLeftDrawerOpenMode:(MMDrawerOpenMode)openMode { + if (self.openSide == MMDrawerSideLeft) return; + _leftDrawerOpenMode = openMode; +} + +- (void)setRightDrawerOpenMode:(MMDrawerOpenMode)openMode { + if (self.openSide == MMDrawerSideRight) return; + _rightDrawerOpenMode = openMode; +} + - (void)setAnimatingDrawer:(BOOL)animatingDrawer { _animatingDrawer = animatingDrawer; [self.view setUserInteractionEnabled:!animatingDrawer]; @@ -1085,6 +1332,7 @@ - (void)setRightSideEnabled:(BOOL)rightSideEnabled { [self updatePanHandlersState]; } + #pragma mark - Getters - (CGFloat)maximumLeftDrawerWidth { if (self.leftDrawerViewController) { @@ -1155,107 +1403,420 @@ - (UIColor *)statusBarViewBackgroundColor { return _statusBarViewBackgroundColor; } +- (MMDrawerOpenMode)rightDrawerOpenMode { + return _rightDrawerOpenMode; +} + +- (MMDrawerOpenMode)leftDrawerOpenMode { + return _leftDrawerOpenMode; +} + #pragma mark - Gesture Handlers - (void)tapGestureCallback:(UITapGestureRecognizer *)tapGesture { if (self.openSide != MMDrawerSideNone && self.isAnimatingDrawer == NO) { - [self closeDrawerAnimated:YES - completion:^(BOOL finished) { - if (self.gestureCompletion) { - self.gestureCompletion(self, tapGesture); - } - }]; + // Get the tap location + CGPoint tapLocation = [tapGesture locationInView:self.childControllerContainerView]; + + // Get the open drawer view controller + UIViewController *sideDrawerViewController = [self sideDrawerViewControllerForSide:self.openSide]; + + // Check if we are in above content mode + MMDrawerOpenMode openMode = [self getDrawerOpenMode:self.openSide]; + + // Only close if not tapping on the drawer itself in above content mode + BOOL shouldClose = YES; + + if (openMode == MMDrawerOpenModeAboveContent && sideDrawerViewController) { + // Check if tap is inside the drawer view + CGPoint tapInDrawerView = [tapGesture locationInView:sideDrawerViewController.view]; + if (CGRectContainsPoint(sideDrawerViewController.view.bounds, tapInDrawerView)) { + shouldClose = NO; + } + } + + if (shouldClose) { + [self closeDrawerAnimated:YES + completion:^(BOOL finished) { + if (self.gestureCompletion) { + self.gestureCompletion(self, tapGesture); + } + }]; + } } } - (void)panGestureCallback:(UIPanGestureRecognizer *)panGesture { switch (panGesture.state) { - case UIGestureRecognizerStateBegan: { - if (self.gestureStart) { - self.gestureStart(self, panGesture); + case UIGestureRecognizerStateBegan: + [self handlePanGestureBegan:panGesture]; + break; + case UIGestureRecognizerStateChanged: + [self handlePanGestureChanged:panGesture]; + break; + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + [self handlePanGestureEnded:panGesture]; + break; + default: + break; + } +} + +- (void)handlePanGestureBegan:(UIPanGestureRecognizer *)panGesture { + if (self.gestureStart) { + self.gestureStart(self, panGesture); + } + + if (self.animatingDrawer) { + [panGesture setEnabled:NO]; + return; + } + + MMDrawerSide drawerSide = [self determineDrawerSideForPanGesture:panGesture]; + if (drawerSide == MMDrawerSideNone) { + return; + } + + MMDrawerOpenMode openMode = [self getDrawerOpenMode:drawerSide]; + if (openMode == MMDrawerOpenModeAboveContent) { + [self setupPanningStartFrameOverlayMode:drawerSide]; + } else { + [self setupPanningStartFramePushMode]; + } +} + +- (MMDrawerSide)determineDrawerSideForPanGesture:(UIPanGestureRecognizer *)panGesture { + MMDrawerSide drawerSide = self.openSide; + + if (drawerSide == MMDrawerSideNone) { + CGPoint velocity = [panGesture velocityInView:self.view]; + drawerSide = (velocity.x > 0) ? MMDrawerSideLeft : MMDrawerSideRight; + + if ((drawerSide == MMDrawerSideLeft && !_leftSideEnabled) || + (drawerSide == MMDrawerSideRight && !_rightSideEnabled)) { + drawerSide = (drawerSide == MMDrawerSideLeft) ? MMDrawerSideRight : MMDrawerSideLeft; + + if ((drawerSide == MMDrawerSideLeft && !_leftSideEnabled) || + (drawerSide == MMDrawerSideRight && !_rightSideEnabled)) { + return MMDrawerSideNone; + } } - if (self.animatingDrawer) { - [panGesture setEnabled:NO]; - break; + } + return drawerSide; +} + +- (void)setupPanningStartFrameOverlayMode:(MMDrawerSide)drawerSide { + UIViewController *drawerViewController = [self sideDrawerViewControllerForSide:drawerSide]; + + // If drawer is closed, set up initial position + if (self.openSide == MMDrawerSideNone) { + self.startingPanRect = + [self calcClosedDrawerPanStartFrameInOverlay:drawerSide + drawerViewController:(UIViewController *)drawerViewController]; + + [drawerViewController.view setFrame:self.startingPanRect]; + + // Ensure drawer is visible and in front + drawerViewController.view.hidden = NO; + [self.childControllerContainerView bringSubviewToFront:drawerViewController.view]; + } else { + self.startingPanRect = drawerViewController.view.frame; + } + + // Save for upcoming gesture updates in above-content mode + self.panDrawerSide = drawerSide; +} + +- (void)setupPanningStartFramePushMode { + self.startingPanRect = self.centerContainerView.frame; +} + +- (CGRect)calcClosedDrawerPanStartFrameInOverlay:(MMDrawerSide)drawerSide + drawerViewController:(UIViewController *)drawerViewController { + CGRect drawerFrame = drawerViewController.view.frame; + CGFloat maximumDrawerWidth = [self maximumDrawerWidthForSide:drawerSide]; + + drawerFrame.size.width = maximumDrawerWidth; + + // Position off-screen based on side + if (drawerSide == MMDrawerSideLeft) { + drawerFrame.origin.x = -maximumDrawerWidth; + } else { + drawerFrame.origin.x = self.view.bounds.size.width; + } + return drawerFrame; +} + +- (void)handlePanGestureChanged:(UIPanGestureRecognizer *)panGesture { + self.view.userInteractionEnabled = NO; + + MMDrawerSide drawerSide = self.panDrawerSide; + MMDrawerOpenMode openMode = [self getDrawerOpenMode:drawerSide]; + if (openMode == MMDrawerOpenModeAboveContent) { + [self handlePanGestureChangedOverlayMode:panGesture forDrawerSide:drawerSide]; + } else { + [self handlePanGestureChangedPushMode:panGesture]; + } +} + +- (void)handlePanGestureChangedOverlayMode:(UIPanGestureRecognizer *)panGesture + forDrawerSide:(MMDrawerSide)drawerSide { + + UIViewController *drawerViewController = [self sideDrawerViewControllerForSide:drawerSide]; + if (!drawerViewController) { + return; + } + + CGFloat maximumDrawerWidth = [self maximumDrawerWidthForSide:drawerSide]; + CGPoint translatedPoint = [panGesture translationInView:self.view]; + + CGRect newFrame = [self calculateNewFrameForOverlayDrawer:drawerViewController + withTranslation:translatedPoint + forDrawerSide:drawerSide + maximumDrawerWidth:maximumDrawerWidth]; + + CGFloat percentVisible = [self calculatePercentVisibleForOverlayDrawer:drawerSide + withFrame:newFrame + maximumDrawerWidth:maximumDrawerWidth]; + + // Handle overlay + [self updateOverlayWithPercentVisible:percentVisible]; + + MMDrawerSide visibleSide = (percentVisible > 0.01) ? drawerSide : MMDrawerSideNone; + + [self updateAppearanceTransitions:self.openSide toSide:visibleSide]; + + // Apply new frame + drawerViewController.view.frame = newFrame; + [self.childControllerContainerView bringSubviewToFront:drawerViewController.view]; +} + +- (CGRect)calculateNewFrameForOverlayDrawer:(UIViewController *)drawerViewController + withTranslation:(CGPoint)translatedPoint + forDrawerSide:(MMDrawerSide)drawerSide + maximumDrawerWidth:(CGFloat)maximumDrawerWidth { + CGRect newFrame = drawerViewController.view.frame; + if (self.openSide == drawerSide) { + // If drawer is already open, adjust from starting position + newFrame.origin.x = self.startingPanRect.origin.x + translatedPoint.x; + } else { + // If drawer is closed, calculate from off-screen position + if (drawerSide == MMDrawerSideLeft) { + newFrame.origin.x = -maximumDrawerWidth + translatedPoint.x; } else { - self.startingPanRect = self.centerContainerView.frame; - } - } - case UIGestureRecognizerStateChanged: { - self.view.userInteractionEnabled = NO; - CGRect newFrame = self.startingPanRect; - CGPoint translatedPoint = [panGesture translationInView:self.centerContainerView]; - newFrame.origin.x = - [self roundedOriginXForDrawerConstriants:CGRectGetMinX(self.startingPanRect) + - translatedPoint.x]; - newFrame = CGRectIntegral(newFrame); - CGFloat xOffset = newFrame.origin.x; - - MMDrawerSide visibleSide = MMDrawerSideNone; - CGFloat percentVisible = 0.0; - if (xOffset > 0) { - visibleSide = MMDrawerSideLeft; - percentVisible = xOffset / self.maximumLeftDrawerWidth; - } else if (xOffset < 0) { - visibleSide = MMDrawerSideRight; - percentVisible = ABS(xOffset) / self.maximumRightDrawerWidth; - } - - if ((!_leftSideEnabled && visibleSide == MMDrawerSideLeft) || - (!_rightSideEnabled && visibleSide == MMDrawerSideRight)) { - return; + CGFloat screenWidth = self.view.bounds.size.width; + newFrame.origin.x = screenWidth + translatedPoint.x; } + } - UIViewController *visibleSideDrawerViewController = - [self sideDrawerViewControllerForSide:visibleSide]; + // Apply constraints based on drawer side + if (drawerSide == MMDrawerSideLeft) { + newFrame.origin.x = MIN(0, newFrame.origin.x); + newFrame.origin.x = MAX(-maximumDrawerWidth, newFrame.origin.x); + } else { + CGFloat screenWidth = self.view.bounds.size.width; + newFrame.origin.x = MAX(screenWidth - maximumDrawerWidth, newFrame.origin.x); + newFrame.origin.x = MIN(screenWidth, newFrame.origin.x); + } + + return newFrame; +} - if (self.openSide != visibleSide) { - // Handle disappearing the visible drawer - UIViewController *sideDrawerViewController = - [self sideDrawerViewControllerForSide:self.openSide]; - [sideDrawerViewController beginAppearanceTransition:NO animated:NO]; - [sideDrawerViewController endAppearanceTransition]; +- (CGFloat)calculatePercentVisibleForOverlayDrawer:(MMDrawerSide)drawerSide + withFrame:(CGRect)frame + maximumDrawerWidth:(CGFloat)maximumDrawerWidth { + CGFloat percentVisible; + if (drawerSide == MMDrawerSideLeft) { + percentVisible = (maximumDrawerWidth + frame.origin.x) / maximumDrawerWidth; + } else { + CGFloat rightEdge = self.view.bounds.size.width; + percentVisible = (rightEdge - frame.origin.x) / maximumDrawerWidth; + } + return MAX(0, MIN(1.0, percentVisible)); +} - // Drawer is about to become visible - [self prepareToPresentDrawer:visibleSide animated:NO]; - [visibleSideDrawerViewController endAppearanceTransition]; - [self setOpenSide:visibleSide]; - } else if (visibleSide == MMDrawerSideNone) { - [self setOpenSide:MMDrawerSideNone]; +- (void)updateOverlayWithPercentVisible:(CGFloat)percentVisible { + [self setupCenterContentOverlay]; + if (self.centerContentOverlay.superview != self.centerContainerView) { + [self.centerContainerView addSubview:self.centerContentOverlay]; + [self.centerContainerView bringSubviewToFront:self.centerContentOverlay]; + } + self.centerContentOverlay.alpha = percentVisible * 0.5; +} + +- (void)updateAppearanceTransitions:(MMDrawerSide)fromSide toSide:(MMDrawerSide)toSide { + if (fromSide != toSide) { + if (fromSide != MMDrawerSideNone) { + UIViewController *sideDrawerVC = [self sideDrawerViewControllerForSide:fromSide]; + [sideDrawerVC beginAppearanceTransition:NO animated:NO]; + [sideDrawerVC endAppearanceTransition]; } - [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:percentVisible]; + if (toSide != MMDrawerSideNone) { + [self prepareToPresentDrawer:toSide animated:NO]; + UIViewController *visibleDrawerVC = [self sideDrawerViewControllerForSide:toSide]; + [visibleDrawerVC endAppearanceTransition]; + } - [self.centerContainerView - setCenter:CGPointMake(CGRectGetMidX(newFrame), CGRectGetMidY(newFrame))]; + [self setOpenSide:toSide]; + } +} - newFrame = self.centerContainerView.frame; - newFrame.origin.x = floor(newFrame.origin.x); - newFrame.origin.y = floor(newFrame.origin.y); - self.centerContainerView.frame = newFrame; +- (void)handlePanGestureChangedPushMode:(UIPanGestureRecognizer *)panGesture { + CGRect newFrame = self.startingPanRect; + CGPoint translatedPoint = [panGesture translationInView:self.centerContainerView]; - [self.centerContainerView addSubview:self.centerContainerView.overlayView]; - [self.centerContainerView bringSubviewToFront:self.centerContainerView.overlayView]; - self.centerContainerView.overlayView.alpha = percentVisible; + newFrame.origin.x = [self + roundedOriginXForDrawerConstriants:CGRectGetMinX(self.startingPanRect) + translatedPoint.x]; + newFrame = CGRectIntegral(newFrame); + CGFloat xOffset = newFrame.origin.x; - break; + MMDrawerSide visibleSide = MMDrawerSideNone; + CGFloat percentVisible = 0.0; + if (xOffset > 0) { + visibleSide = MMDrawerSideLeft; + percentVisible = xOffset / self.maximumLeftDrawerWidth; + } else if (xOffset < 0) { + visibleSide = MMDrawerSideRight; + percentVisible = ABS(xOffset) / self.maximumRightDrawerWidth; } - case UIGestureRecognizerStateEnded: - case UIGestureRecognizerStateCancelled: { - self.startingPanRect = CGRectNull; - CGPoint velocity = [panGesture velocityInView:self.childControllerContainerView]; - [self finishAnimationForPanGestureWithXVelocity:velocity.x - completion:^(BOOL finished) { - if (self.gestureCompletion) { - self.gestureCompletion(self, panGesture); - } - }]; - self.view.userInteractionEnabled = YES; - break; + + if ((!_leftSideEnabled && visibleSide == MMDrawerSideLeft) || + (!_rightSideEnabled && visibleSide == MMDrawerSideRight)) { + return; } - default: - break; + + [self handleAppearanceTransitionPushMode:visibleSide]; + [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:percentVisible]; + [self.centerContainerView + setCenter:CGPointMake(CGRectGetMidX(newFrame), CGRectGetMidY(newFrame))]; + + newFrame = self.centerContainerView.frame; + newFrame.origin.x = floor(newFrame.origin.x); + newFrame.origin.y = floor(newFrame.origin.y); + self.centerContainerView.frame = newFrame; + + [self.centerContainerView addSubview:self.centerContainerView.overlayView]; + [self.centerContainerView bringSubviewToFront:self.centerContainerView.overlayView]; + self.centerContainerView.overlayView.alpha = percentVisible; +} + +- (void)handleAppearanceTransitionPushMode:(MMDrawerSide)visibleSide { + UIViewController *visibleSideDrawerViewController = [self sideDrawerViewControllerForSide:visibleSide]; + + if (self.openSide != visibleSide) { + // Handle disappearing the visible drawer + UIViewController *sideDrawerVC = [self sideDrawerViewControllerForSide:self.openSide]; + [sideDrawerVC beginAppearanceTransition:NO animated:NO]; + [sideDrawerVC endAppearanceTransition]; + + // Drawer is about to become visible + [self prepareToPresentDrawer:visibleSide animated:NO]; + [visibleSideDrawerViewController endAppearanceTransition]; + [self setOpenSide:visibleSide]; + } else if (visibleSide == MMDrawerSideNone) { + [self setOpenSide:MMDrawerSideNone]; + } +} + +- (void)handlePanGestureEnded:(UIPanGestureRecognizer *)panGesture { + MMDrawerSide drawerSide = self.panDrawerSide; + MMDrawerOpenMode openMode = [self getDrawerOpenMode:drawerSide]; + + if (openMode == MMDrawerOpenModeAboveContent) { + [self completePanningOverlayMode:drawerSide withPanGesture:panGesture]; + } else { + [self completePanningPushMode:panGesture]; } + + self.startingPanRect = CGRectNull; + self.view.userInteractionEnabled = YES; +} + +- (void)completePanningOverlayMode:(MMDrawerSide)drawerSide + withPanGesture:(UIPanGestureRecognizer *)panGesture { + // Get drawer view controller + UIViewController *drawerVC = [self sideDrawerViewControllerForSide:drawerSide]; + if (!drawerVC) { + self.panDrawerSide = MMDrawerSideNone; + return; + } + + BOOL shouldOpen = [self shouldOpenDrawerForGestureEnd:panGesture withDrawerSide:drawerSide andCurrentPosition:drawerVC.view.frame.origin.x]; + [self animateOverlayDrawerToFinalState:shouldOpen forDrawerSide:drawerSide withDrawerVC:drawerVC andPanGesture:panGesture]; +} + +- (BOOL)shouldOpenDrawerForGestureEnd:(UIPanGestureRecognizer *)panGesture withDrawerSide:(MMDrawerSide)drawerSide andCurrentPosition:(CGFloat)currentX { + CGPoint velocity = [panGesture velocityInView:self.view]; + CGFloat maximumDrawerWidth = [self maximumDrawerWidthForSide:drawerSide]; + CGFloat screenWidth = self.view.bounds.size.width; + BOOL shouldOpen = NO; + + if (drawerSide == MMDrawerSideLeft) { + if (velocity.x > 500) shouldOpen = YES; // Fast right swipe + else if (velocity.x < -500) shouldOpen = NO; // Fast left swipe + else shouldOpen = (currentX > -maximumDrawerWidth/2.0); // Based on position + } else { + if (velocity.x < -500) shouldOpen = YES; // Fast left swipe + else if (velocity.x > 500) shouldOpen = NO; // Fast right swipe + else shouldOpen = (currentX < screenWidth - maximumDrawerWidth/2.0); // Based on position + } + + return shouldOpen; +} + +- (void)animateOverlayDrawerToFinalState:(BOOL)shouldOpen forDrawerSide:(MMDrawerSide)drawerSide withDrawerVC:(UIViewController *)drawerVC andPanGesture:(UIPanGestureRecognizer *)panGesture { + CGFloat maximumDrawerWidth = [self maximumDrawerWidthForSide:drawerSide]; + CGFloat screenWidth = self.view.bounds.size.width; + + [UIView animateWithDuration:0.25 + animations:^{ + CGRect frame = drawerVC.view.frame; + + if (shouldOpen) { + if (drawerSide == MMDrawerSideLeft) { + frame.origin.x = 0; + } else { + frame.origin.x = screenWidth - maximumDrawerWidth; + } + + self.centerContentOverlay.alpha = 0.5; + } else { + if (drawerSide == MMDrawerSideLeft) { + frame.origin.x = -maximumDrawerWidth; + } else { + frame.origin.x = screenWidth; + } + + self.centerContentOverlay.alpha = 0.0; + } + + drawerVC.view.frame = frame; + } completion:^(BOOL finished) { + if (shouldOpen) { + [self setOpenSide:drawerSide]; + } else { + [self setOpenSide:MMDrawerSideNone]; + [self.centerContentOverlay removeFromSuperview]; + } + + self.panDrawerSide = MMDrawerSideNone; + + if (self.gestureCompletion) { + self.gestureCompletion(self, panGesture); + } + }]; +} + +- (void)completePanningPushMode:(UIPanGestureRecognizer *)panGesture { + CGPoint velocity = [panGesture velocityInView:self.childControllerContainerView]; + [self finishAnimationForPanGestureWithXVelocity:velocity.x + completion:^(BOOL finished) { + if (self.gestureCompletion) { + self.gestureCompletion(self, panGesture); + } + }]; } - (void)updatePanHandlersState { @@ -1270,6 +1831,29 @@ - (void)updatePanHandlersState { } } +- (void)setupCenterContentOverlay { + if (!self.centerContentOverlay) { + // Create overlay view if it doesn't exist + self.centerContentOverlay = [[UIView alloc] initWithFrame:self.centerContainerView.bounds]; + self.centerContentOverlay.backgroundColor = [UIColor blackColor]; + self.centerContentOverlay.alpha = 0.0; // Start fully transparent + self.centerContentOverlay.userInteractionEnabled = YES; + + // Add tap gesture to close drawer when tapping overlay + UITapGestureRecognizer *tapGesture = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(overlayTapped:)]; + [self.centerContentOverlay addGestureRecognizer:tapGesture]; + } + + // Update frame to match current center container bounds + self.centerContentOverlay.frame = self.centerContainerView.bounds; +} + +- (void)overlayTapped:(UITapGestureRecognizer *)tapGesture { + // Close drawer when overlay is tapped + [self closeDrawerAnimated:YES completion:nil]; +} + #pragma mark - iOS 7 Status Bar Helpers - (UIViewController *)childViewControllerForStatusBarStyle { return [self childViewControllerForSide:self.openSide]; @@ -1367,23 +1951,46 @@ - (BOOL)shouldStretchForSide:(MMDrawerSide)drawerSide { } } +- (void)side:(MMDrawerSide)drawerSide openMode:(MMDrawerOpenMode)openMode { + if (drawerSide == MMDrawerSideLeft) { + self.leftDrawerOpenMode = openMode; + } else if (drawerSide == MMDrawerSideRight) { + self.rightDrawerOpenMode = openMode; + } +} + - (void)applyOvershootScaleTransformForDrawerSide:(MMDrawerSide)drawerSide percentVisible:(CGFloat)percentVisible { - if (percentVisible >= 1.f) { CATransform3D transform = CATransform3DIdentity; - UIViewController *sideDrawerViewController = - [self sideDrawerViewControllerForSide:drawerSide]; - if (drawerSide == MMDrawerSideLeft) { - transform = CATransform3DMakeScale(percentVisible, 1.f, 1.f); - transform = CATransform3DTranslate( - transform, self.maximumLeftDrawerWidth * (percentVisible - 1.f) / 2, 0.f, 0.f); - } else if (drawerSide == MMDrawerSideRight) { - transform = CATransform3DMakeScale(percentVisible, 1.f, 1.f); - transform = CATransform3DTranslate( - transform, -self.maximumRightDrawerWidth * (percentVisible - 1.f) / 2, 0.f, 0.f); + + MMDrawerOpenMode openMode = [self getDrawerOpenMode:drawerSide]; + UIViewController *sideDrawerViewController = [self sideDrawerViewControllerForSide:drawerSide]; + + if (openMode == MMDrawerOpenModePushContent) { + CGFloat stretchFactor = MIN(percentVisible, 1.1); + + if (drawerSide == MMDrawerSideLeft) { + transform = CATransform3DMakeScale(stretchFactor, 1.f, 1.f); + transform = CATransform3DTranslate(transform, self.maximumLeftDrawerWidth * (stretchFactor - 1.f) / 2, 0.f, 0.f); + } else if (drawerSide == MMDrawerSideRight) { + transform = CATransform3DMakeScale(stretchFactor, 1.f, 1.f); + transform = CATransform3DTranslate(transform, -self.maximumRightDrawerWidth * (stretchFactor - 1.f) / 2, 0.f, 0.f); + } + + sideDrawerViewController.view.layer.transform = transform; + } + else if (openMode == MMDrawerOpenModeAboveContent) { + if (drawerSide == MMDrawerSideLeft) { + transform = CATransform3DMakeScale(percentVisible, 1.f, 1.f); + transform = CATransform3DTranslate(transform, self.maximumLeftDrawerWidth * (percentVisible - 1.f) / 2, 0.f, 0.f); + } else if (drawerSide == MMDrawerSideRight) { + transform = CATransform3DMakeScale(percentVisible, 1.f, 1.f); + transform = CATransform3DTranslate(transform, -self.maximumRightDrawerWidth * (percentVisible - 1.f) / 2, 0.f, 0.f); + } + + sideDrawerViewController.view.layer.transform = transform; } - sideDrawerViewController.view.layer.transform = transform; } } @@ -1679,4 +2286,18 @@ - (BOOL)isPointContainedWithinRightBezelRect:(CGPoint)point { return (CGRectContainsPoint(rightBezelRect, point) && [self isPointContainedWithinCenterViewContentRect:point]); } + +- (MMDrawerOpenMode)getDrawerOpenMode:(MMDrawerSide)drawerSide { + return (drawerSide == MMDrawerSideLeft) ? self.leftDrawerOpenMode : self.rightDrawerOpenMode; +} + +- (CGFloat)maximumDrawerWidthForSide:(MMDrawerSide)drawerSide { + return (drawerSide == MMDrawerSideLeft) ? self.maximumLeftDrawerWidth : self.maximumRightDrawerWidth; +} + +// Helper method to calculate animation duration based on distance and velocity +- (NSTimeInterval)animationDurationForDistance:(CGFloat)distance velocity:(CGFloat)velocity { + return MAX(distance / ABS(velocity), MMDrawerMinimumAnimationDuration); +} + @end diff --git a/lib/ios/RNNSideMenuPresenter.m b/lib/ios/RNNSideMenuPresenter.m index 09ec475d8e..4419ba621d 100644 --- a/lib/ios/RNNSideMenuPresenter.m +++ b/lib/ios/RNNSideMenuPresenter.m @@ -1,5 +1,6 @@ #import "RNNSideMenuPresenter.h" #import "RNNSideMenuController.h" +#import "RNNSideMenuSideOptions.h" @implementation RNNSideMenuPresenter @@ -52,6 +53,20 @@ - (void)applyOptions:(RNNNavigationOptions *)options { [self.sideMenuController.view setBackgroundColor:[withDefault.layout.backgroundColor withDefault:nil]]; + + MMDrawerOpenMode openModeLeft = MMDrawerOpenModePushContent; // Default value + if (withDefault.sideMenu.left.openMode.hasValue) { + NSString *openModeString = withDefault.sideMenu.left.openMode.get; + openModeLeft = MMDrawerOpenModeFromString(openModeString); + } + [self.sideMenuController side:MMDrawerSideLeft openMode:openModeLeft]; + + MMDrawerOpenMode openModeRight = MMDrawerOpenModePushContent; // Default value + if (withDefault.sideMenu.right.openMode.hasValue) { + NSString *openModeString = withDefault.sideMenu.right.openMode.get; + openModeRight = MMDrawerOpenModeFromString(openModeString); + } + [self.sideMenuController side:MMDrawerSideRight openMode:openModeRight]; } - (void)applyOptionsOnInit:(RNNNavigationOptions *)initialOptions { @@ -112,6 +127,18 @@ - (void)mergeOptions:(RNNNavigationOptions *)options options.sideMenu.right.shouldStretchDrawer.get; } + if (options.sideMenu.left.openMode.hasValue) { + NSString *openModeString = options.sideMenu.left.openMode.get; + MMDrawerOpenMode openMode = MMDrawerOpenModeFromString(openModeString); + [self.sideMenuController side:MMDrawerSideLeft openMode:openMode]; + } + + if (options.sideMenu.right.openMode.hasValue) { + NSString *openModeString = options.sideMenu.right.openMode.get; + MMDrawerOpenMode openMode = MMDrawerOpenModeFromString(openModeString); + [self.sideMenuController side:MMDrawerSideRight openMode:openMode]; + } + if (options.sideMenu.left.animationVelocity.hasValue) { self.sideMenuController.animationVelocityLeft = options.sideMenu.left.animationVelocity.get; } diff --git a/lib/ios/RNNSideMenuSideOptions.h b/lib/ios/RNNSideMenuSideOptions.h index c19512939d..8024b7c62b 100644 --- a/lib/ios/RNNSideMenuSideOptions.h +++ b/lib/ios/RNNSideMenuSideOptions.h @@ -8,5 +8,11 @@ @property(nonatomic, strong) Double *width; @property(nonatomic, strong) Bool *shouldStretchDrawer; @property(nonatomic, strong) Double *animationVelocity; +@property (nonatomic, strong) Text *openMode; +/** + * Converts a string open mode to the equivalent MMDrawerOpenMode enum value + */ + MMDrawerOpenMode MMDrawerOpenModeFromString(NSString *openModeString); + @end diff --git a/lib/ios/RNNSideMenuSideOptions.m b/lib/ios/RNNSideMenuSideOptions.m index 1f21afdd8d..1fe33c18ec 100644 --- a/lib/ios/RNNSideMenuSideOptions.m +++ b/lib/ios/RNNSideMenuSideOptions.m @@ -10,7 +10,7 @@ - (instancetype)initWithDict:(NSDictionary *)dict { self.width = [DoubleParser parse:dict key:@"width"]; self.shouldStretchDrawer = [BoolParser parse:dict key:@"shouldStretchDrawer"]; self.animationVelocity = [DoubleParser parse:dict key:@"animationVelocity"]; - + self.openMode = [TextParser parse:dict key:@"openMode"]; return self; } @@ -25,6 +25,18 @@ - (void)mergeOptions:(RNNSideMenuSideOptions *)options { self.shouldStretchDrawer = options.shouldStretchDrawer; if (options.animationVelocity.hasValue) self.animationVelocity = options.animationVelocity; + if (options.openMode.hasValue) + self.openMode = options.openMode; +} + +/** + Converts a string open mode to the equivalent MMDrawerOpenMode enum value + */ +MMDrawerOpenMode MMDrawerOpenModeFromString(NSString *openModeString) { + if ([openModeString isEqualToString:@"aboveContent"]) { + return MMDrawerOpenModeAboveContent; + } + return MMDrawerOpenModePushContent; } @end diff --git a/lib/src/interfaces/Options.ts b/lib/src/interfaces/Options.ts index f0923edfb8..5346587bcd 100644 --- a/lib/src/interfaces/Options.ts +++ b/lib/src/interfaces/Options.ts @@ -1078,10 +1078,19 @@ export interface SideMenuSide { height?: number; /** * Stretch sideMenu contents when opened past the width + * + * **Not applicable when `openMode` is `aboveContent`** + * * #### (iOS specific) * @default true */ shouldStretchDrawer?: boolean; + /** + * Configure the opening mode of the side menu + * #### (iOS specific) + * @default 'pushContent' + */ + openMode?: 'pushContent' | 'aboveContent'; } export interface OptionsSideMenu { diff --git a/package-lock.json b/package-lock.json index 7aa3614f78..ed0e47d12b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23907,4 +23907,4 @@ } } } -} +} \ No newline at end of file diff --git a/playground/ios/NavigationTests/RNNSideMenuPresenterTest.m b/playground/ios/NavigationTests/RNNSideMenuPresenterTest.m index efa4794818..83817d1cd5 100644 --- a/playground/ios/NavigationTests/RNNSideMenuPresenterTest.m +++ b/playground/ios/NavigationTests/RNNSideMenuPresenterTest.m @@ -47,6 +47,8 @@ - (void)testApplyOptionsShouldSetDefaultValues { [[self.boundViewController expect] side:MMDrawerSideRight enabled:YES]; [[self.boundViewController expect] setShouldStretchLeftDrawer:YES]; [[self.boundViewController expect] setShouldStretchRightDrawer:YES]; + [[self.boundViewController expect] side:MMDrawerSideLeft openMode:MMDrawerOpenModePushContent]; + [[self.boundViewController expect] side:MMDrawerSideRight openMode:MMDrawerOpenModePushContent]; [[self.boundViewController expect] setAnimationVelocityLeft:840.0f]; [[self.boundViewController expect] setAnimationVelocityRight:840.0f]; [[self.boundViewController reject] side:MMDrawerSideLeft width:0]; @@ -63,6 +65,8 @@ - (void)testApplyOptionsShouldSetInitialValues { self.options.sideMenu.right.enabled = [[Bool alloc] initWithBOOL:NO]; self.options.sideMenu.left.shouldStretchDrawer = [[Bool alloc] initWithBOOL:NO]; self.options.sideMenu.right.shouldStretchDrawer = [[Bool alloc] initWithBOOL:NO]; + self.options.sideMenu.left.openMode = [[Text alloc] initWithValue:@"aboveContent"]; + self.options.sideMenu.right.openMode = [[Text alloc] initWithValue:@"aboveContent"]; self.options.sideMenu.right.animationVelocity = [[Double alloc] initWithValue:@(100.0f)]; self.options.sideMenu.left.animationVelocity = [[Double alloc] initWithValue:@(100.0f)]; @@ -70,6 +74,8 @@ - (void)testApplyOptionsShouldSetInitialValues { [[self.boundViewController expect] side:MMDrawerSideRight enabled:NO]; [[self.boundViewController expect] setShouldStretchLeftDrawer:NO]; [[self.boundViewController expect] setShouldStretchRightDrawer:NO]; + [[self.boundViewController expect] side:MMDrawerSideLeft openMode:MMDrawerOpenModeAboveContent]; + [[self.boundViewController expect] side:MMDrawerSideRight openMode:MMDrawerOpenModeAboveContent]; [[self.boundViewController expect] setAnimationVelocityLeft:100.0f]; [[self.boundViewController expect] setAnimationVelocityRight:100.0f]; diff --git a/playground/src/screens/SetRootScreen.tsx b/playground/src/screens/SetRootScreen.tsx index 43bfbe6aa3..c9690c806f 100644 --- a/playground/src/screens/SetRootScreen.tsx +++ b/playground/src/screens/SetRootScreen.tsx @@ -17,6 +17,7 @@ const { SET_ROOT_WITH_TWO_CHILDREN_HIDES_BOTTOM_TABS_BTN, SET_ROOT_WITHOUT_STACK_HIDES_BOTTOM_TABS_BTN, SET_ROOT_WITH_BUTTONS, + SET_ROOT_WITH_MENUS, ROUND_BUTTON, } = testIDs; @@ -86,6 +87,11 @@ export default class SetRootScreen extends React.Component { testID={SET_ROOT_WITH_BUTTONS} onPress={this.setRootWithButtons} /> +