Skip to content

Commit

Permalink
Support react-native modals (#7156)
Browse files Browse the repository at this point in the history
Currently, using react-native declarative modals can cause view controllers to be detached from the view hierarchy. For example, when presenting an RNN modal from react-native modal and then dismissing the react-native modal, the RNN modal on top of it remain detached as its holder is dismissed. It can also cause the new RNN modal to be dismissed.

In this PR we implemented `RCTModalHostViewManager.presentationBlock`  and `RCTModalHostViewManager.dismissalBlock` in order to have the same modals logic we have in RNN.
  • Loading branch information
yogevbd authored Jun 17, 2021
1 parent cf1fd7a commit ec60931
Show file tree
Hide file tree
Showing 14 changed files with 137 additions and 54 deletions.
7 changes: 7 additions & 0 deletions e2e/Modals.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,11 @@ describe('modal', () => {
'dismissModal promise resolved with: UniqueStackId'
);
});

it('dismiss previous react-native modal', async () => {
await elementById(TestIDs.TOGGLE_REACT_NATIVE_MODAL).tap();
await elementById(TestIDs.SHOW_MODAL_AND_DISMISS_REACT_NATIVE_MODAL).tap();
await elementById(TestIDs.DISMISS_MODAL_BTN).tap();
await expect(elementById(TestIDs.MODAL_SCREEN_HEADER)).toBeVisible();
});
});
8 changes: 5 additions & 3 deletions lib/ios/RNNBridgeManager.mm
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
#import "RNNBridgeManager.h"

#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>

#import "RNNBridgeModule.h"
#import "RNNComponentViewCreator.h"
#import "RNNEventEmitter.h"
#import "RNNLayoutManager.h"
#import "RNNReactComponentRegistry.h"
#import "RNNReactRootViewCreator.h"
#import "RNNSplashScreen.h"
#import <React/RCTBridge.h>
#import <React/RCTModalHostViewManager.h>
#import <React/RCTUIManager.h>

@interface RNNBridgeManager ()

Expand Down Expand Up @@ -105,6 +105,8 @@ - (void)onJavaScriptWillLoad {

- (void)onJavaScriptLoaded {
[_commandsHandler setReadyToReceiveCommands:true];
[_modalManager
connectModalHostViewManager:[self.bridge moduleForClass:RCTModalHostViewManager.class]];
[[_bridge moduleForClass:[RNNEventEmitter class]] sendOnAppLaunched];
}

Expand Down
22 changes: 16 additions & 6 deletions lib/ios/RNNCommandsHandler.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "RNNCommandsHandler.h"
#import "RNNAssert.h"
#import "RNNComponentViewController.h"
#import "RNNConvert.h"
#import "RNNDefaultOptionsHelper.h"
#import "RNNErrorHandler.h"
#import "React/RCTI18nUtil.h"
Expand Down Expand Up @@ -368,6 +369,12 @@ - (void)showModal:(NSDictionary *)layout
__weak UIViewController *weakNewVC = newVc;
newVc.waitForRender =
[newVc.resolveOptionsWithDefault.animations.showModal shouldWaitForRender];
newVc.modalPresentationStyle =
[RNNConvert UIModalPresentationStyle:[newVc.resolveOptionsWithDefault.modalPresentationStyle
withDefault:@"default"]];
newVc.modalTransitionStyle =
[RNNConvert UIModalTransitionStyle:[newVc.resolveOptionsWithDefault.modalTransitionStyle
withDefault:@"coverVertical"]];
[newVc setReactViewReadyCallback:^{
[self->_modalManager
showModal:weakNewVC
Expand Down Expand Up @@ -403,12 +410,15 @@ - (void)dismissModal:(NSString *)componentId
RNNNavigationOptions *options = [[RNNNavigationOptions alloc] initWithDict:mergeOptions];
[modalToDismiss.presentedComponentViewController mergeOptions:options];

[_modalManager dismissModal:modalToDismiss
completion:^{
[self->_eventEmitter sendOnNavigationCommandCompletion:dismissModal
commandId:commandId];
completion(modalToDismiss.topMostViewController.layoutInfo.componentId);
}];
[_modalManager
dismissModal:modalToDismiss
animated:[modalToDismiss.resolveOptionsWithDefault.animations.dismissModal.exit.enable
withDefault:YES]
completion:^{
[self->_eventEmitter sendOnNavigationCommandCompletion:dismissModal
commandId:commandId];
completion(modalToDismiss.topMostViewController.layoutInfo.componentId);
}];
}

- (void)dismissAllModals:(NSDictionary *)mergeOptions
Expand Down
4 changes: 2 additions & 2 deletions lib/ios/RNNEnterExitAnimation.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ - (instancetype)initWithDict:(NSDictionary *)dict {
}

- (void)mergeOptions:(RNNEnterExitAnimation *)options {
if (options.enter.hasAnimation)
if (options.enter.hasValue)
self.enter = options.enter;
if (options.exit.hasAnimation)
if (options.exit.hasValue)
self.exit = options.exit;
}

Expand Down
6 changes: 4 additions & 2 deletions lib/ios/RNNEventEmitter.m
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ - (void)sendOnPreviewCompleted:(NSString *)componentId

- (void)sendModalsDismissedEvent:(NSString *)componentId
numberOfModalsDismissed:(NSNumber *)modalsDismissed {
[self send:ModalDismissed
body:@{@"componentId" : componentId, @"modalsDismissed" : modalsDismissed}];
if (componentId) {
[self send:ModalDismissed
body:@{@"componentId" : componentId, @"modalsDismissed" : modalsDismissed}];
}
}

- (void)sendModalAttemptedToDismissEvent:(NSString *)componentId {
Expand Down
3 changes: 3 additions & 0 deletions lib/ios/RNNModalManager.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "RNNModalManagerEventHandler.h"
#import <Foundation/Foundation.h>
#import <React/RCTBridge.h>
#import <React/RCTModalHostViewManager.h>
#import <UIKit/UIKit.h>

typedef void (^RNNTransitionCompletionBlock)(void);
Expand All @@ -12,11 +13,13 @@ typedef void (^RNNTransitionRejectionBlock)(NSString *_Nonnull code, NSString *_

- (instancetype _Nonnull)initWithBridge:(RCTBridge *_Nonnull)bridge
eventHandler:(RNNModalManagerEventHandler *_Nonnull)eventHandler;
- (void)connectModalHostViewManager:(RCTModalHostViewManager *_Nonnull)modalHostViewManager;

- (void)showModal:(UIViewController *_Nonnull)viewController
animated:(BOOL)animated
completion:(RNNTransitionWithComponentIdCompletionBlock _Nullable)completion;
- (void)dismissModal:(UIViewController *_Nullable)viewController
animated:(BOOL)animated
completion:(RNNTransitionCompletionBlock _Nullable)completion;
- (void)dismissAllModalsAnimated:(BOOL)animated completion:(void (^__nullable)(void))completion;
- (void)dismissAllModalsSynchronosly;
Expand Down
51 changes: 33 additions & 18 deletions lib/ios/RNNModalManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
return self;
}

- (void)connectModalHostViewManager:(RCTModalHostViewManager *)modalHostViewManager {
modalHostViewManager.presentationBlock =
^(UIViewController *reactViewController, UIViewController *viewController, BOOL animated,
dispatch_block_t completionBlock) {
[self showModal:viewController
animated:animated
completion:^(NSString *_Nonnull componentId) {
if (completionBlock)
completionBlock();
}];
};

modalHostViewManager.dismissalBlock =
^(UIViewController *reactViewController, UIViewController *viewController, BOOL animated,
dispatch_block_t completionBlock) {
[self dismissModal:viewController
animated:animated
completion:^{
if (completionBlock)
completionBlock();
}];
};
}

- (void)showModal:(UIViewController<RNNLayoutProtocol> *)viewController
animated:(BOOL)animated
completion:(RNNTransitionWithComponentIdCompletionBlock)completion {
Expand All @@ -44,13 +68,6 @@ - (void)showModal:(UIViewController<RNNLayoutProtocol> *)viewController

UIViewController *topVC = [self topPresentedVC];

viewController.modalPresentationStyle = [RNNConvert
UIModalPresentationStyle:[viewController.resolveOptionsWithDefault.modalPresentationStyle
withDefault:@"default"]];
viewController.modalTransitionStyle = [RNNConvert
UIModalTransitionStyle:[viewController.resolveOptionsWithDefault.modalTransitionStyle
withDefault:@"coverVertical"]];

if (viewController.presentationController) {
viewController.presentationController.delegate = self;
}
Expand Down Expand Up @@ -80,10 +97,11 @@ - (void)showModal:(UIViewController<RNNLayoutProtocol> *)viewController
}

- (void)dismissModal:(UIViewController *)viewController
animated:(BOOL)animated
completion:(RNNTransitionCompletionBlock)completion {
if (viewController) {
[_pendingModalIdsToDismiss addObject:viewController];
[self removePendingNextModalIfOnTop:completion];
[self removePendingNextModalIfOnTop:completion animated:animated];
}
}

Expand Down Expand Up @@ -128,7 +146,8 @@ - (void)dismissAllModalsSynchronosly {

#pragma mark - private

- (void)removePendingNextModalIfOnTop:(RNNTransitionCompletionBlock)completion {
- (void)removePendingNextModalIfOnTop:(RNNTransitionCompletionBlock)completion
animated:(BOOL)animated {
UIViewController<RNNLayoutProtocol> *modalToDismiss = [_pendingModalIdsToDismiss lastObject];
RNNNavigationOptions *optionsWithDefault = modalToDismiss.resolveOptionsWithDefault;

Expand All @@ -152,12 +171,11 @@ - (void)removePendingNextModalIfOnTop:(RNNTransitionCompletionBlock)completion {
_dismissModalTransitionDelegate;
}

if (modalToDismiss == topPresentedVC ||
[[topPresentedVC childViewControllers] containsObject:modalToDismiss]) {
if ((modalToDismiss == topPresentedVC ||
[[topPresentedVC childViewControllers] containsObject:modalToDismiss])) {
[self dismissSearchController:modalToDismiss];
[modalToDismiss
dismissViewControllerAnimated:[optionsWithDefault.animations.dismissModal.exit.enable
withDefault:YES]
dismissViewControllerAnimated:animated
completion:^{
[self->_pendingModalIdsToDismiss removeObject:modalToDismiss];
if (modalToDismiss.view) {
Expand All @@ -168,18 +186,15 @@ - (void)removePendingNextModalIfOnTop:(RNNTransitionCompletionBlock)completion {
completion();
}

[self removePendingNextModalIfOnTop:nil];
[self removePendingNextModalIfOnTop:nil animated:NO];
}];
} else {
[modalToDismiss.view removeFromSuperview];
modalToDismiss.view = nil;
modalToDismiss.getCurrentChild.resolveOptions.animations.dismissModal.exit.enable =
[[Bool alloc] initWithBOOL:NO];
[self dismissedModal:modalToDismiss];

if (completion) {
if (completion)
completion();
}
}
}

Expand Down
1 change: 1 addition & 0 deletions lib/ios/TransitionOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@

- (NSTimeInterval)maxDuration;
- (BOOL)hasAnimation;
- (BOOL)hasValue;

@end
6 changes: 6 additions & 0 deletions lib/ios/TransitionOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ - (void)mergeOptions:(TransitionOptions *)options {
}

- (BOOL)hasAnimation {
return self.x.hasAnimation || self.y.hasAnimation || self.alpha.hasAnimation ||
self.translationX.hasAnimation || self.translationY.hasAnimation ||
self.rotationX.hasAnimation || self.rotationY.hasAnimation;
}

- (BOOL)hasValue {
return self.x.hasAnimation || self.y.hasAnimation || self.alpha.hasAnimation ||
self.translationX.hasAnimation || self.translationY.hasAnimation ||
self.rotationX.hasAnimation || self.rotationY.hasAnimation ||
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,13 @@
"binaryPath": "playground/android/app/build/outputs/apk/debug/app-debug.apk",
"build": "cd playground/android && ./gradlew app:assembleDebug app:assembleAndroidTest -DtestBuildType=debug",
"type": "android.emulator",
"name": "Pixel_API_28"
"name": "Pixel_3A_API_29"
},
"android.emu.release": {
"binaryPath": "playground/android/app/build/outputs/apk/release/app-release.apk",
"build": "cd playground/android && ./gradlew app:assembleRelease app:assembleAndroidTest -DtestBuildType=release",
"type": "android.emulator",
"name": "Pixel_API_28"
"name": "Pixel_3A_API_29"
}
}
}
Expand Down
23 changes: 21 additions & 2 deletions playground/ios/NavigationTests/RNNCommandsHandlerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ - (void)testDismissModal_shouldResolveTopMostComponentId {
eventEmitter:nil
childViewControllers:@[ child ]];

OCMStub([self.modalManager dismissModal:OCMArg.any completion:OCMArg.invokeBlock]);
OCMStub([self.modalManager dismissModal:OCMArg.any animated:NO completion:OCMArg.invokeBlock]);
OCMStub(child.isModal).andReturn(YES);
OCMStub([self.layoutManager findComponentForId:@"child"]).andReturn(child);

Expand Down Expand Up @@ -787,11 +787,12 @@ - (void)testDismissModal_shouldMergeOptions {
dismissModal:[OCMArg checkWithBlock:^BOOL(UIViewController *modalToDismiss) {
return modalToDismiss.options.animations.dismissModal.exit.enable.get == NO;
}]
animated:NO
completion:OCMArg.any];

[self.uut dismissModal:@"child"
commandId:@"commandId"
mergeOptions:@{@"animations" : @{@"dismissModal" : @{@"enabled" : @(0)}}}
mergeOptions:@{@"animations" : @{@"dismissModal" : @{@"exit" : @{@"enabled" : @(0)}}}}
completion:^(NSString *_Nonnull componentId) {
XCTAssertTrue([componentId isEqualToString:@"stack"]);
}
Expand All @@ -802,6 +803,24 @@ - (void)testDismissModal_shouldMergeOptions {
[self.modalManager verify];
}

- (void)testShowModal_withPresentationStyle {
[self.uut setReadyToReceiveCommands:true];
OCMStub([self.controllerFactory createLayout:[OCMArg any]]).andReturn(_vc1);
_vc1.options = [RNNNavigationOptions emptyOptions];
_vc1.options.modalPresentationStyle = [Text withValue:@"overCurrentContext"];
[self.uut showModal:@{} commandId:@"" completion:nil];
XCTAssertEqual(_vc1.modalPresentationStyle, UIModalPresentationOverCurrentContext);
}

- (void)testApplyOptionsOnInit_shouldShowModalWithTransitionStyle {
[self.uut setReadyToReceiveCommands:true];
OCMStub([self.controllerFactory createLayout:[OCMArg any]]).andReturn(_vc1);
_vc1.options = [RNNNavigationOptions emptyOptions];
_vc1.options.modalTransitionStyle = [Text withValue:@"crossDissolve"];
[self.uut showModal:@{} commandId:@"" completion:nil];
XCTAssertEqual(_vc1.modalTransitionStyle, UIModalTransitionStyleCrossDissolve);
}

- (void)testPush_shouldResolvePromiseAndSendCommandCompletionWithPushedComponentId {
[self.uut setReadyToReceiveCommands:true];
NSString *expectedComponentId = @"pushedComponent";
Expand Down
22 changes: 4 additions & 18 deletions playground/ios/NavigationTests/RNNModalManagerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ - (void)testDismissModal_InvokeDelegateWithCorrectParameters {
[_modalManager showModal:_vc3 animated:NO completion:nil];

[[_modalManagerEventHandler expect] dismissedModal:_vc3];
[_modalManager dismissModal:_vc3 completion:nil];
[_modalManager dismissModal:_vc3 animated:NO completion:nil];
[_modalManagerEventHandler verify];
}

Expand All @@ -99,7 +99,7 @@ - (void)testDismissPreviousModal_InvokeDelegateWithCorrectParameters {
[_modalManager showModal:_vc3 animated:NO completion:nil];

[[_modalManagerEventHandler expect] dismissedModal:_vc2];
[_modalManager dismissModal:_vc2 completion:nil];
[_modalManager dismissModal:_vc2 animated:NO completion:nil];
[_modalManagerEventHandler verify];
}

Expand All @@ -113,7 +113,7 @@ - (void)testDismissAllModals_AfterDismissingPreviousModal_InvokeDelegateWithCorr
[_modalManager showModal:_vc3 animated:NO completion:nil];

[[_modalManagerEventHandler expect] dismissedModal:_vc2];
[_modalManager dismissModal:_vc2 completion:nil];
[_modalManager dismissModal:_vc2 animated:NO completion:nil];
[_modalManagerEventHandler verify];

[[_modalManagerEventHandler expect] dismissedMultipleModals:@[ _vc1, _vc3 ]];
Expand All @@ -123,7 +123,7 @@ - (void)testDismissAllModals_AfterDismissingPreviousModal_InvokeDelegateWithCorr

- (void)testDismissModal_DismissNilModalDoesntCrash {
[[_modalManagerEventHandler reject] dismissedModal:OCMArg.any];
[_modalManager dismissModal:nil completion:nil];
[_modalManager dismissModal:nil animated:NO completion:nil];
[_modalManagerEventHandler verify];
}

Expand Down Expand Up @@ -170,18 +170,4 @@ - (void)testApplyOptionsOnInit_shouldShowModalWithDefaultTransitionStyle {
XCTAssertEqual(_vc1.modalTransitionStyle, UIModalTransitionStyleCoverVertical);
}

- (void)testApplyOptionsOnInit_shouldShowModalWithPresentationStyle {
_vc1.options = [RNNNavigationOptions emptyOptions];
_vc1.options.modalPresentationStyle = [Text withValue:@"overCurrentContext"];
[_modalManager showModal:_vc1 animated:NO completion:nil];
XCTAssertEqual(_vc1.modalPresentationStyle, UIModalPresentationOverCurrentContext);
}

- (void)testApplyOptionsOnInit_shouldShowModalWithTransitionStyle {
_vc1.options = [RNNNavigationOptions emptyOptions];
_vc1.options.modalTransitionStyle = [Text withValue:@"crossDissolve"];
[_modalManager showModal:_vc1 animated:NO completion:nil];
XCTAssertEqual(_vc1.modalTransitionStyle, UIModalTransitionStyleCrossDissolve);
}

@end
Loading

0 comments on commit ec60931

Please sign in to comment.