From 0a1937ac5a044adfa4f326fe4f35eeb3dcc00cc4 Mon Sep 17 00:00:00 2001 From: Liat Netach <60575762+liatnetach@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:30:20 +0200 Subject: [PATCH 01/29] Support drawer menus opening as overlay instead of pushing content in ios (#7984) * add example in playground and some animations changes with Yogi * fix drag animations (open+close) in left drawer and add overlay on center * support all features in right drawer * supoprt both old and new opening mode using openAboveScreen param * connect the new parameter to rnn platform * update param from boolean to enum * clean * install * update lock * fix unit --- .../MMDrawerController/MMDrawerController.h | 22 + .../MMDrawerController/MMDrawerController.m | 742 ++++++++++++++---- lib/ios/RNNSideMenuPresenter.m | 30 + lib/ios/RNNSideMenuSideOptions.h | 6 + lib/ios/RNNSideMenuSideOptions.m | 20 +- lib/src/interfaces/Options.ts | 7 + package-lock.json | 2 +- .../RNNSideMenuPresenterTest.m | 6 + playground/src/screens/SetRootScreen.tsx | 92 +++ playground/src/testIDs.ts | 2 + 10 files changed, 789 insertions(+), 140 deletions(-) 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..f026ce1eae 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; @@ -167,6 +169,9 @@ @interface MMDrawerController () { @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) MMDrawerSide startingDrawerSide; + @end @@ -227,6 +232,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 +330,113 @@ - (void)closeDrawerAnimated:(BOOL)animated } } else { [self setAnimatingDrawer:animated]; - CGRect newFrame = self.childControllerContainerView.bounds; - - CGFloat distance = ABS(CGRectGetMinX(self.centerContainerView.frame)); - NSTimeInterval duration = MAX(distance / ABS(velocity), MMDrawerMinimumAnimationDuration); - - BOOL leftDrawerVisible = CGRectGetMinX(self.centerContainerView.frame) > 0; - BOOL rightDrawerVisible = CGRectGetMinX(self.centerContainerView.frame) < 0; - - MMDrawerSide visibleSide = MMDrawerSideNone; - CGFloat percentVisble = 0.0; - - 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; + MMDrawerSide visibleSide = self.openSide; + + if (visibleSide == MMDrawerSideNone) { + [self setAnimatingDrawer:NO]; + if (completion) { + completion(NO); + } + return; } - - UIViewController *sideDrawerViewController = - [self sideDrawerViewControllerForSide:visibleSide]; - - [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:percentVisble]; - + + UIViewController *sideDrawerViewController = [self sideDrawerViewControllerForSide:visibleSide]; [sideDrawerViewController beginAppearanceTransition:NO animated:animated]; - - [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]; + + MMDrawerOpenMode openMode = (visibleSide == MMDrawerSideLeft) ? + self.leftDrawerOpenMode : + self.rightDrawerOpenMode; + + if (openMode == MMDrawerOpenModeAboveContent) { + // OVERLAY MODE + // Get maximum drawer width + CGFloat maximumDrawerWidth = (visibleSide == MMDrawerSideLeft) ? + self.maximumLeftDrawerWidth : + self.maximumRightDrawerWidth; + + // Prepare drawer frames + CGRect currentFrame = sideDrawerViewController.view.frame; + CGRect finalFrame = currentFrame; + + // 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 } - 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; + } + + // Calculate animation duration + CGFloat distance = ABS(currentFrame.origin.x - finalFrame.origin.x); + NSTimeInterval duration = MAX(distance / ABS(velocity), MMDrawerMinimumAnimationDuration); + + // Animate closure + [UIView animateWithDuration:(animated ? duration : 0.0) + delay:0.0 + options:options + animations:^{ + [self setNeedsStatusBarAppearanceUpdateIfSupported]; + + // Move drawer off-screen + [sideDrawerViewController.view setFrame:finalFrame]; + + // Fade out overlay + self.centerContentOverlay.alpha = 0.0; + + // Update visual state + [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:0.0]; + } + completion:^(BOOL finished) { + // Complete appearance transition + [sideDrawerViewController endAppearanceTransition]; + + // Update state + [self setOpenSide:MMDrawerSideNone]; + [self resetDrawerVisualStateForDrawerSide:visibleSide]; + + // Remove overlay + [self.centerContentOverlay removeFromSuperview]; + + [self setAnimatingDrawer:NO]; + if (completion) { + completion(finished); + } + }]; + } else { + // ORIGINAL PUSH MODE + CGRect newFrame = self.childControllerContainerView.bounds; + + CGFloat distance = ABS(CGRectGetMinX(self.centerContainerView.frame)); + 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]; + [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:0.0]; + } + completion:^(BOOL finished) { + // Complete appearance transition + [sideDrawerViewController endAppearanceTransition]; + + // Update state + [self setOpenSide:MMDrawerSideNone]; + [self resetDrawerVisualStateForDrawerSide:visibleSide]; + [self setAnimatingDrawer:NO]; + + if (completion) { + completion(finished); + } + }]; + } } } @@ -397,48 +465,137 @@ - (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; + // Check if this drawer should use overlay mode + MMDrawerOpenMode openMode = (drawerSide == MMDrawerSideLeft) ? + self.leftDrawerOpenMode : + self.rightDrawerOpenMode; + + if (openMode == MMDrawerOpenModeAboveContent) { + // OVERLAY MODE + CGFloat maximumDrawerWidth = (drawerSide == MMDrawerSideLeft) ? + self.maximumLeftDrawerWidth : + self.maximumRightDrawerWidth; + + // Configure drawer frames + CGRect drawerFrame = sideDrawerViewController.view.frame; + CGRect initialFrame = sideDrawerViewController.view.frame; + + // Set proper width + drawerFrame.size.width = maximumDrawerWidth; + initialFrame.size.width = maximumDrawerWidth; + + // Set proper positions + 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]; + } + } + + // Setup overlay + [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]; + + // Calculate animation duration + CGFloat distance = ABS(initialFrame.origin.x - drawerFrame.origin.x); + NSTimeInterval duration = MAX(distance / ABS(velocity), MMDrawerMinimumAnimationDuration); + + // Animate opening + [UIView animateWithDuration:(animated ? duration : 0.0) + delay:0.0 + options:options + animations:^{ + [self setNeedsStatusBarAppearanceUpdateIfSupported]; + + // Move drawer to final position + [sideDrawerViewController.view setFrame:drawerFrame]; + + // Fade in overlay + self.centerContentOverlay.alpha = 0.5; + + // Update visual state + [self updateDrawerVisualStateForDrawerSide:drawerSide percentVisible:1.0]; + } + completion:^(BOOL finished) { + // Complete appearance transition + if (drawerSide != self.openSide) { + [sideDrawerViewController endAppearanceTransition]; + } + + // Update state + [self setOpenSide:drawerSide]; + [self resetDrawerVisualStateForDrawerSide:drawerSide]; + [self setAnimatingDrawer:NO]; + + if (completion) { + completion(finished); + } + }]; } else { - newFrame = self.centerContainerView.frame; - newFrame.origin.x = 0 - self.maximumRightDrawerWidth; - } - - 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]; + // ORIGINAL PUSH MODE + CGRect newFrame; + CGRect oldFrame = self.centerContainerView.frame; + + if (drawerSide == MMDrawerSideLeft) { + newFrame = self.centerContainerView.frame; + newFrame.origin.x = self.maximumLeftDrawerWidth; + } else { + newFrame = self.centerContainerView.frame; + newFrame.origin.x = 0 - self.maximumRightDrawerWidth; } - completion:^(BOOL finished) { - // 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); - } - }]; + + // Calculate animation duration + CGFloat distance = ABS(CGRectGetMinX(oldFrame) - newFrame.origin.x); + NSTimeInterval duration = MAX(distance / ABS(velocity), MMDrawerMinimumAnimationDuration); + + // Animate center container + [UIView animateWithDuration:(animated ? duration : 0.0) + delay:0.0 + options:options + animations:^{ + [self setNeedsStatusBarAppearanceUpdateIfSupported]; + [self.centerContainerView setFrame:newFrame]; + [self updateDrawerVisualStateForDrawerSide:drawerSide percentVisible:1.0]; + } + completion:^(BOOL finished) { + // Complete appearance transition + if (drawerSide != self.openSide) { + [sideDrawerViewController endAppearanceTransition]; + } + + // Update state + [self setOpenSide:drawerSide]; + [self resetDrawerVisualStateForDrawerSide:drawerSide]; + [self setAnimatingDrawer:NO]; + + if (completion) { + completion(finished); + } + }]; + } } } } @@ -1070,6 +1227,14 @@ - (void)setStatusBarViewBackgroundColor:(UIColor *)dummyStatusBarColor { [self.dummyStatusBarView setBackgroundColor:_statusBarViewBackgroundColor]; } +- (void)setLeftDrawerOpenMode:(MMDrawerOpenMode)openMode { + _leftDrawerOpenMode = openMode; +} + +- (void)setRightDrawerOpenMode:(MMDrawerOpenMode)openMode { + _rightDrawerOpenMode = openMode; +} + - (void)setAnimatingDrawer:(BOOL)animatingDrawer { _animatingDrawer = animatingDrawer; [self.view setUserInteractionEnabled:!animatingDrawer]; @@ -1085,6 +1250,7 @@ - (void)setRightSideEnabled:(BOOL)rightSideEnabled { [self updatePanHandlersState]; } + #pragma mark - Getters - (CGFloat)maximumLeftDrawerWidth { if (self.leftDrawerViewController) { @@ -1155,6 +1321,14 @@ - (UIColor *)statusBarViewBackgroundColor { return _statusBarViewBackgroundColor; } +- (MMDrawerOpenMode)rightDrawerOpenMode { + return _rightDrawerOpenMode; +} + +- (MMDrawerOpenMode)leftDrawerOpenMode { + return _leftDrawerOpenMode; +} + #pragma mark - Gesture Handlers - (void)tapGestureCallback:(UITapGestureRecognizer *)tapGesture { @@ -1171,85 +1345,345 @@ - (void)tapGestureCallback:(UITapGestureRecognizer *)tapGesture { - (void)panGestureCallback:(UIPanGestureRecognizer *)panGesture { switch (panGesture.state) { case UIGestureRecognizerStateBegan: { + // Call gesture start callback if (self.gestureStart) { self.gestureStart(self, panGesture); } + + // Don't proceed if drawer is currently animating if (self.animatingDrawer) { [panGesture setEnabled:NO]; break; + } + + // Determine which drawer to work with + CGPoint velocity = [panGesture velocityInView:self.view]; + MMDrawerSide drawerSide = self.openSide; + + if (drawerSide == MMDrawerSideNone) { + // Determine based on gesture direction + drawerSide = (velocity.x > 0) ? MMDrawerSideLeft : MMDrawerSideRight; + + // Check if the side is enabled + if ((drawerSide == MMDrawerSideLeft && !_leftSideEnabled) || + (drawerSide == MMDrawerSideRight && !_rightSideEnabled)) { + drawerSide = (drawerSide == MMDrawerSideLeft) ? MMDrawerSideRight : MMDrawerSideLeft; + + if ((drawerSide == MMDrawerSideLeft && !_leftSideEnabled) || + (drawerSide == MMDrawerSideRight && !_rightSideEnabled)) { + return; + } + } + } + + // Store which drawer we're working with for this gesture + self.startingDrawerSide = drawerSide; + + MMDrawerOpenMode openMode = (drawerSide == MMDrawerSideLeft) ? + self.leftDrawerOpenMode : + self.rightDrawerOpenMode; + + if (openMode == MMDrawerOpenModeAboveContent) { + // OVERLAY MODE + UIViewController *drawerViewController = [self sideDrawerViewControllerForSide:drawerSide]; + CGFloat maximumDrawerWidth = (drawerSide == MMDrawerSideLeft) ? + self.maximumLeftDrawerWidth : + self.maximumRightDrawerWidth; + + // Store current drawer frame + self.startingPanRect = drawerViewController.view.frame; + + // If drawer is closed, set up initial position + if (self.openSide == MMDrawerSideNone) { + CGRect drawerFrame = drawerViewController.view.frame; + + // Set proper width + drawerFrame.size.width = maximumDrawerWidth; + + // Position off-screen based on side + if (drawerSide == MMDrawerSideLeft) { + drawerFrame.origin.x = -maximumDrawerWidth; + } else { // MMDrawerSideRight + drawerFrame.origin.x = self.view.bounds.size.width; + } + + // Apply initial frame + [drawerViewController.view setFrame:drawerFrame]; + + // Ensure drawer is visible and in front + drawerViewController.view.hidden = NO; + [self.childControllerContainerView bringSubviewToFront:drawerViewController.view]; + + // Update starting rect + self.startingPanRect = drawerFrame; + } } else { + // Original push mode - get center container position as starting point self.startingPanRect = self.centerContainerView.frame; } + + break; } 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; + + // Get translation + CGPoint translatedPoint = [panGesture translationInView:self.view]; + + // Use the drawer side we're working with + MMDrawerSide drawerSide = self.startingDrawerSide; + if (drawerSide == MMDrawerSideNone) { + drawerSide = self.openSide; + if (drawerSide == MMDrawerSideNone) { + drawerSide = (translatedPoint.x > 0) ? MMDrawerSideLeft : MMDrawerSideRight; + } } - - UIViewController *visibleSideDrawerViewController = - [self sideDrawerViewControllerForSide:visibleSide]; - - if (self.openSide != visibleSide) { - // Handle disappearing the visible drawer - UIViewController *sideDrawerViewController = - [self sideDrawerViewControllerForSide:self.openSide]; - [sideDrawerViewController beginAppearanceTransition:NO animated:NO]; - [sideDrawerViewController endAppearanceTransition]; - - // Drawer is about to become visible - [self prepareToPresentDrawer:visibleSide animated:NO]; - [visibleSideDrawerViewController endAppearanceTransition]; - [self setOpenSide:visibleSide]; - } else if (visibleSide == MMDrawerSideNone) { - [self setOpenSide:MMDrawerSideNone]; + + MMDrawerOpenMode openMode = (drawerSide == MMDrawerSideLeft) ? + self.leftDrawerOpenMode : + self.rightDrawerOpenMode; + + if (openMode == MMDrawerOpenModeAboveContent) { + // OVERLAY MODE + UIViewController *drawerViewController = [self sideDrawerViewControllerForSide:drawerSide]; + if (!drawerViewController) { + return; + } + + CGFloat maximumDrawerWidth = (drawerSide == MMDrawerSideLeft) ? + self.maximumLeftDrawerWidth : + self.maximumRightDrawerWidth; + + // Calculate new drawer position + 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 { // MMDrawerSideRight + CGFloat screenWidth = self.view.bounds.size.width; + newFrame.origin.x = screenWidth + translatedPoint.x; + } + } + + // 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 { // MMDrawerSideRight + CGFloat screenWidth = self.view.bounds.size.width; + newFrame.origin.x = MAX(screenWidth - maximumDrawerWidth, newFrame.origin.x); + newFrame.origin.x = MIN(screenWidth, newFrame.origin.x); + } + + // Calculate visibility percentage + CGFloat percentVisible; + if (drawerSide == MMDrawerSideLeft) { + percentVisible = (maximumDrawerWidth + newFrame.origin.x) / maximumDrawerWidth; + } else { // MMDrawerSideRight + CGFloat rightEdge = self.view.bounds.size.width; + percentVisible = (rightEdge - newFrame.origin.x) / maximumDrawerWidth; + } + percentVisible = MAX(0, MIN(1.0, percentVisible)); + + // Handle overlay + [self setupCenterContentOverlay]; + if (self.centerContentOverlay.superview != self.centerContainerView) { + [self.centerContainerView addSubview:self.centerContentOverlay]; + [self.centerContainerView bringSubviewToFront:self.centerContentOverlay]; + } + self.centerContentOverlay.alpha = percentVisible * 0.5; + + // Determine visible side based on percentage + MMDrawerSide visibleSide = (percentVisible > 0.15) ? drawerSide : MMDrawerSideNone; + + // Update appearance transitions + if (self.openSide != visibleSide) { + if (self.openSide != MMDrawerSideNone) { + UIViewController *sideDrawerVC = [self sideDrawerViewControllerForSide:self.openSide]; + [sideDrawerVC beginAppearanceTransition:NO animated:NO]; + [sideDrawerVC endAppearanceTransition]; + } + + if (visibleSide != MMDrawerSideNone) { + [self prepareToPresentDrawer:visibleSide animated:NO]; + UIViewController *visibleDrawerVC = [self sideDrawerViewControllerForSide:visibleSide]; + [visibleDrawerVC endAppearanceTransition]; + } + + [self setOpenSide:visibleSide]; + } + + // Apply new frame + drawerViewController.view.frame = newFrame; + [self.childControllerContainerView bringSubviewToFront:drawerViewController.view]; + } else { + // ORIGINAL PUSH MODE + CGRect newFrame = self.startingPanRect; + + // Calculate new center container position + newFrame.origin.x = self.startingPanRect.origin.x + translatedPoint.x; + + // Apply constraints + CGFloat minX = -self.maximumRightDrawerWidth; + CGFloat maxX = self.maximumLeftDrawerWidth; + newFrame.origin.x = MAX(minX, MIN(maxX, newFrame.origin.x)); + + // Determine visible side and percentage + 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; + } + + // Check if side is enabled + if ((!_leftSideEnabled && visibleSide == MMDrawerSideLeft) || + (!_rightSideEnabled && visibleSide == MMDrawerSideRight)) { + return; + } + + // Handle appearance transitions + UIViewController *visibleSideDrawerViewController = [self sideDrawerViewControllerForSide:visibleSide]; + + if (self.openSide != visibleSide) { + // Handle existing drawer disappearing + UIViewController *sideDrawerVC = [self sideDrawerViewControllerForSide:self.openSide]; + [sideDrawerVC beginAppearanceTransition:NO animated:NO]; + [sideDrawerVC endAppearanceTransition]; + + // Handle new drawer appearing + [self prepareToPresentDrawer:visibleSide animated:NO]; + [visibleSideDrawerViewController endAppearanceTransition]; + + [self setOpenSide:visibleSide]; + } else if (visibleSide == MMDrawerSideNone) { + [self setOpenSide:MMDrawerSideNone]; + } + + // Update visual state and position center container + [self updateDrawerVisualStateForDrawerSide:visibleSide percentVisible:percentVisible]; + [self.centerContainerView setFrame:newFrame]; } - - [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; - + + self.view.userInteractionEnabled = YES; break; } case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: { + // Get tracked drawer side + MMDrawerSide drawerSide = self.startingDrawerSide; + + if (drawerSide == MMDrawerSideNone) { + drawerSide = self.openSide; + if (drawerSide == MMDrawerSideNone) { + CGPoint velocity = [panGesture velocityInView:self.view]; + drawerSide = (velocity.x > 0) ? MMDrawerSideLeft : MMDrawerSideRight; + } + } + + MMDrawerOpenMode openMode = (drawerSide == MMDrawerSideLeft) ? + self.leftDrawerOpenMode : + self.rightDrawerOpenMode; + + if (openMode == MMDrawerOpenModeAboveContent) { + // OVERLAY MODE + // Get drawer view controller + UIViewController *drawerVC = [self sideDrawerViewControllerForSide:drawerSide]; + if (!drawerVC) { + self.startingDrawerSide = MMDrawerSideNone; + self.startingPanRect = CGRectNull; + self.view.userInteractionEnabled = YES; + break; + } + + // Get position and velocity + CGFloat currentX = drawerVC.view.frame.origin.x; + CGPoint velocity = [panGesture velocityInView:self.view]; + CGFloat maximumDrawerWidth = (drawerSide == MMDrawerSideLeft) ? + self.maximumLeftDrawerWidth : + self.maximumRightDrawerWidth; + + // Determine if drawer should open or close + BOOL shouldOpen = NO; + CGFloat screenWidth = self.view.bounds.size.width; + + if (drawerSide == MMDrawerSideLeft) { + // Left drawer logic + 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 { // MMDrawerSideRight + // Right drawer logic + 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 + } + + // Animate to final position + [UIView animateWithDuration:0.25 + animations:^{ + CGRect frame = drawerVC.view.frame; + + if (shouldOpen) { + // Open drawer + if (drawerSide == MMDrawerSideLeft) { + frame.origin.x = 0; + } else { // MMDrawerSideRight + frame.origin.x = screenWidth - maximumDrawerWidth; + } + + // Show overlay + self.centerContentOverlay.alpha = 0.5; + } else { + // Close drawer + if (drawerSide == MMDrawerSideLeft) { + frame.origin.x = -maximumDrawerWidth; + } else { // MMDrawerSideRight + frame.origin.x = screenWidth; + } + + // Hide overlay + 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.startingDrawerSide = MMDrawerSideNone; + + if (self.gestureCompletion) { + self.gestureCompletion(self, panGesture); + } + }]; + } else { + // ORIGINAL PUSH MODE + // Use original finishAnimationForPanGesture method + CGPoint velocity = [panGesture velocityInView:self.childControllerContainerView]; + [self finishAnimationForPanGestureWithXVelocity:velocity.x + completion:^(BOOL finished) { + if (self.gestureCompletion) { + self.gestureCompletion(self, panGesture); + } + }]; + } + + // Reset tracking variables 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; } @@ -1270,6 +1704,30 @@ - (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,6 +1825,14 @@ - (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 { diff --git a/lib/ios/RNNSideMenuPresenter.m b/lib/ios/RNNSideMenuPresenter.m index 09ec475d8e..70c7f2d77f 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,23 @@ - (void)applyOptions:(RNNNavigationOptions *)options { [self.sideMenuController.view setBackgroundColor:[withDefault.layout.backgroundColor withDefault:nil]]; + + if (withDefault.sideMenu.left.openMode.hasValue) { + NSString *openModeString = withDefault.sideMenu.left.openMode.get; + MMDrawerOpenMode openMode = MMDrawerOpenModeFromString(openModeString); + [self.sideMenuController side:MMDrawerSideLeft openMode:openMode]; + } else { + [self.sideMenuController side:MMDrawerSideLeft openMode:MMDrawerOpenModePushContent]; + } + + if (withDefault.sideMenu.right.openMode.hasValue) { + NSString *openModeString = withDefault.sideMenu.right.openMode.get; + MMDrawerOpenMode openMode = MMDrawerOpenModeFromString(openModeString); + [self.sideMenuController side:MMDrawerSideRight openMode:openMode]; + } else { + [self.sideMenuController side:MMDrawerSideRight openMode:MMDrawerOpenModePushContent]; + } + } - (void)applyOptionsOnInit:(RNNNavigationOptions *)initialOptions { @@ -112,6 +130,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..df865c8f08 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,24 @@ - (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) { + return MMDrawerOpenModePushContent; // Default + } + + if ([openModeString isEqualToString:@"aboveContent"]) { + return MMDrawerOpenModeAboveContent; + } else { + // Default or explicit "pushContent" + return MMDrawerOpenModePushContent; + } } @end diff --git a/lib/src/interfaces/Options.ts b/lib/src/interfaces/Options.ts index f0923edfb8..bf0e0b8dd3 100644 --- a/lib/src/interfaces/Options.ts +++ b/lib/src/interfaces/Options.ts @@ -1082,6 +1082,13 @@ export interface SideMenuSide { * @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 e0df980ca2..a73dd563b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23119,4 +23119,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..3991caa98f 100644 --- a/playground/src/screens/SetRootScreen.tsx +++ b/playground/src/screens/SetRootScreen.tsx @@ -17,6 +17,8 @@ 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_RIGHT_MENU, + SET_ROOT_WITH_LEFT_MENU, ROUND_BUTTON, } = testIDs; @@ -86,6 +88,16 @@ export default class SetRootScreen extends React.Component { testID={SET_ROOT_WITH_BUTTONS} onPress={this.setRootWithButtons} /> +