From 71af55968db11315cd10aac2e64cb1e24f37c0e0 Mon Sep 17 00:00:00 2001 From: Yogev Ben David Date: Wed, 18 Dec 2019 15:23:02 +0200 Subject: [PATCH] Add screenPopped event (#5748) Called each time a screen is popped, either due to `Navigation.pop` or pop gesture. closes #3941 Navigation.events().registerScreenPoppedListener(({ componentId }) => { }); --- docs/docs/events.md | 13 ++ .../parse/LayoutFactory.java | 2 +- .../react/EventEmitter.java | 7 + .../stack/StackController.java | 6 +- .../stack/StackControllerBuilder.java | 11 +- .../com/reactnativenavigation/TestUtils.java | 3 +- .../viewcontrollers/OptionsApplyingTest.java | 15 +- .../stack/StackControllerTest.java | 54 ++++-- lib/ios/RNNCommandsHandler.m | 4 +- lib/ios/RNNComponentViewController.m | 2 - lib/ios/RNNEventEmitter.h | 3 +- lib/ios/RNNEventEmitter.m | 166 +++++++++--------- lib/ios/RNNNavigationStackManager.m | 2 - lib/ios/RNNStackController.m | 43 +++-- lib/src/adapters/NativeEventsReceiver.ts | 7 +- .../events/ComponentEventsObserver.test.tsx | 13 ++ lib/src/events/ComponentEventsObserver.ts | 9 +- lib/src/events/EventsRegistry.test.tsx | 7 + lib/src/events/EventsRegistry.ts | 8 +- lib/src/interfaces/ComponentEvents.ts | 4 + .../RNNNavigationControllerTest.m | 15 +- 21 files changed, 256 insertions(+), 138 deletions(-) diff --git a/docs/docs/events.md b/docs/docs/events.md index 33f6fee326d..9cd1f8676e3 100644 --- a/docs/docs/events.md +++ b/docs/docs/events.md @@ -145,6 +145,19 @@ const modalDismissedListener = Navigation.events().registerModalDismissedListene modalDismissedListener.remove(); ``` +## registerScreenPoppedListener +Invoked when screen is popped. + +```js +// Subscribe +const screenPoppedListener = Navigation.events().registerScreenPoppedListener(({ componentId }) => { + +}); +... +// Unsubscribe +screenPoppedListener.remove(); +``` + | Parameter | Description | |:--------------------:|:-----| |**componentId** | Id of the modal| diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java index 512fb5be19c..cf6687aee6b 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java @@ -176,7 +176,7 @@ private ViewController createExternalComponent(LayoutNode node) { } private ViewController createStack(LayoutNode node) { - return new StackControllerBuilder(activity) + return new StackControllerBuilder(activity, eventEmitter) .setChildren(createChildren(node.children)) .setChildRegistry(childRegistry) .setTopBarController(new TopBarController()) diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/react/EventEmitter.java b/lib/android/app/src/main/java/com/reactnativenavigation/react/EventEmitter.java index ae83e9a1414..5e7219c1897 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/react/EventEmitter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/react/EventEmitter.java @@ -19,6 +19,7 @@ public class EventEmitter { private static final String ComponentDidDisappear = "RNN.ComponentDidDisappear"; private static final String NavigationButtonPressed = "RNN.NavigationButtonPressed"; private static final String ModalDismissed = "RNN.ModalDismissed"; + private static final String ScreenPopped = "RNN.ScreenPopped"; @Nullable private ReactContext reactContext; @@ -73,6 +74,12 @@ public void emitModalDismissed(String id, int modalsDismissed) { emit(ModalDismissed, event); } + public void emitScreenPoppedEvent(String componentId) { + WritableMap event = Arguments.createMap(); + event.putString("componentId", componentId); + emit(ScreenPopped, event); + } + private void emit(String eventName) { emit(eventName, Arguments.createMap()); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java index 670cfb5870a..14e3663851a 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java @@ -9,6 +9,7 @@ import com.reactnativenavigation.presentation.Presenter; import com.reactnativenavigation.presentation.StackPresenter; import com.reactnativenavigation.react.Constants; +import com.reactnativenavigation.react.EventEmitter; import com.reactnativenavigation.utils.CommandListener; import com.reactnativenavigation.utils.CommandListenerAdapter; import com.reactnativenavigation.utils.CompatUtils; @@ -43,12 +44,14 @@ public class StackController extends ParentController { private IdStack stack = new IdStack<>(); private final NavigationAnimator animator; + private final EventEmitter eventEmitter; private TopBarController topBarController; private BackButtonHelper backButtonHelper; private final StackPresenter presenter; - public StackController(Activity activity, List children, ChildControllersRegistry childRegistry, TopBarController topBarController, NavigationAnimator animator, String id, Options initialOptions, BackButtonHelper backButtonHelper, StackPresenter stackPresenter, Presenter presenter) { + public StackController(Activity activity, List children, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, TopBarController topBarController, NavigationAnimator animator, String id, Options initialOptions, BackButtonHelper backButtonHelper, StackPresenter stackPresenter, Presenter presenter) { super(activity, childRegistry, id, presenter, initialOptions); + this.eventEmitter = eventEmitter; this.topBarController = topBarController; this.animator = animator; this.backButtonHelper = backButtonHelper; @@ -277,6 +280,7 @@ public void pop(Options mergeOptions, CommandListener listener) { private void finishPopping(ViewController disappearing, CommandListener listener) { disappearing.destroy(); listener.onSuccess(disappearing.getId()); + eventEmitter.emitScreenPoppedEvent(disappearing.getId()); } public void popTo(ViewController viewController, Options mergeOptions, CommandListener listener) { diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerBuilder.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerBuilder.java index e70baa423e0..bdba8c10660 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerBuilder.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerBuilder.java @@ -6,6 +6,7 @@ import com.reactnativenavigation.parse.Options; import com.reactnativenavigation.presentation.Presenter; import com.reactnativenavigation.presentation.StackPresenter; +import com.reactnativenavigation.react.EventEmitter; import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry; import com.reactnativenavigation.viewcontrollers.ViewController; import com.reactnativenavigation.viewcontrollers.topbar.TopBarController; @@ -26,13 +27,20 @@ public class StackControllerBuilder { private Presenter presenter; private StackPresenter stackPresenter; private List children = new ArrayList<>(); + private EventEmitter eventEmitter; - public StackControllerBuilder(Activity activity) { + public StackControllerBuilder(Activity activity, EventEmitter eventEmitter) { this.activity = activity; + this.eventEmitter = eventEmitter; presenter = new Presenter(activity, new Options()); animator = new NavigationAnimator(activity, new ElementTransitionManager()); } + public StackControllerBuilder setEventEmitter(EventEmitter eventEmitter) { + this.eventEmitter = eventEmitter; + return this; + } + public StackControllerBuilder setChildren(ViewController... children) { return setChildren(Arrays.asList(children)); } @@ -86,6 +94,7 @@ public StackController build() { return new StackController(activity, children, childRegistry, + eventEmitter, topBarController, animator, id, diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/TestUtils.java b/lib/android/app/src/test/java/com/reactnativenavigation/TestUtils.java index 7c457114dbd..47621dc56bb 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/TestUtils.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/TestUtils.java @@ -12,6 +12,7 @@ import com.reactnativenavigation.parse.params.Bool; import com.reactnativenavigation.presentation.RenderChecker; import com.reactnativenavigation.presentation.StackPresenter; +import com.reactnativenavigation.react.EventEmitter; import com.reactnativenavigation.utils.ImageLoader; import com.reactnativenavigation.utils.UiUtils; import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry; @@ -33,7 +34,7 @@ protected TopBar createTopBar(Context context, StackLayout stackLayout) { return topBar; } }; - return new StackControllerBuilder(activity) + return new StackControllerBuilder(activity, Mockito.mock(EventEmitter.class)) .setId("stack") .setChildRegistry(new ChildControllersRegistry()) .setTopBarController(topBarController) diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java index f7d52d3c5ef..882e268869d 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java @@ -11,21 +11,14 @@ import com.reactnativenavigation.TestUtils; import com.reactnativenavigation.mocks.TestComponentLayout; import com.reactnativenavigation.mocks.TestReactView; -import com.reactnativenavigation.mocks.TitleBarReactViewCreatorMock; -import com.reactnativenavigation.mocks.TopBarBackgroundViewCreatorMock; -import com.reactnativenavigation.mocks.TopBarButtonCreatorMock; import com.reactnativenavigation.parse.Options; import com.reactnativenavigation.parse.params.Bool; import com.reactnativenavigation.parse.params.Colour; import com.reactnativenavigation.parse.params.Text; import com.reactnativenavigation.presentation.ComponentPresenter; import com.reactnativenavigation.presentation.Presenter; -import com.reactnativenavigation.presentation.RenderChecker; -import com.reactnativenavigation.presentation.StackPresenter; import com.reactnativenavigation.utils.CommandListenerAdapter; -import com.reactnativenavigation.utils.ImageLoader; import com.reactnativenavigation.viewcontrollers.stack.StackController; -import com.reactnativenavigation.viewcontrollers.stack.StackControllerBuilder; import com.reactnativenavigation.viewcontrollers.topbar.TopBarController; import com.reactnativenavigation.views.StackLayout; import com.reactnativenavigation.views.topbar.TopBar; @@ -95,13 +88,7 @@ public void applyNavigationOptionsHandlesNoParentStack() { @Test public void initialOptionsAppliedOnAppear() { uut.options.topBar.title.text = new Text("the title"); - StackController stackController = - new StackControllerBuilder(activity) - .setTopBarController(new TopBarController()) - .setId("stackId") - .setInitialOptions(new Options()) - .setStackPresenter(new StackPresenter(activity, new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewCreatorMock(), new TopBarButtonCreatorMock(), new ImageLoader(), new RenderChecker(), new Options())) - .build(); + StackController stackController = TestUtils.newStackController(activity).build(); stackController.ensureViewIsCreated(); stackController.push(uut, new CommandListenerAdapter()); assertThat(stackController.getTopBar().getTitle()).isEmpty(); diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java index b0832421c4f..b0518197cfb 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java @@ -22,8 +22,8 @@ import com.reactnativenavigation.parse.params.Text; import com.reactnativenavigation.presentation.RenderChecker; import com.reactnativenavigation.presentation.StackPresenter; +import com.reactnativenavigation.react.EventEmitter; import com.reactnativenavigation.utils.CommandListenerAdapter; -import com.reactnativenavigation.utils.ImageLoader; import com.reactnativenavigation.utils.StatusBarUtils; import com.reactnativenavigation.utils.TitleBarHelper; import com.reactnativenavigation.utils.UiUtils; @@ -66,6 +66,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @LooperMode(LooperMode.Mode.PAUSED) @@ -83,10 +84,12 @@ public class StackControllerTest extends BaseTest { private TopBarController topBarController; private StackPresenter presenter; private BackButtonHelper backButtonHelper; + private EventEmitter eventEmitter; @Override public void beforeEach() { super.beforeEach(); + eventEmitter = Mockito.mock(EventEmitter.class); backButtonHelper = spy(new BackButtonHelper()); activity = newActivity(); StatusBarUtils.saveStatusBarHeight(63); @@ -383,12 +386,10 @@ public void onSuccess(String childId) { @Test public void pop_layoutHandlesChildWillDisappear() { TopBarController topBarController = new TopBarController(); - uut = new StackControllerBuilder(activity) - .setTopBarController(topBarController) - .setId("uut") - .setInitialOptions(new Options()) - .setStackPresenter(new StackPresenter(activity, new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewCreatorMock(), new TopBarButtonCreatorMock(), new ImageLoader(), new RenderChecker(), new Options())) - .build(); + uut = TestUtils.newStackController(activity) + .setTopBarController(topBarController) + .setId("uut") + .build(); uut.ensureViewIsCreated(); uut.push(child1, new CommandListenerAdapter()); uut.push(child2, new CommandListenerAdapter() { @@ -404,6 +405,30 @@ public void onSuccess(String childId) { }); } + @Test + public void pop_popEventIsEmitted() { + disablePushAnimation(child1, child2); + disablePopAnimation(child2); + uut.push(child1, new CommandListenerAdapter()); + uut.push(child2, new CommandListenerAdapter()); + + uut.pop(Options.EMPTY, new CommandListenerAdapter()); + verify(eventEmitter).emitScreenPoppedEvent(child2.getId()); + } + + @Test + public void popToRoot_popEventIsEmitted() { + disablePushAnimation(child1, child2, child3); + disablePopAnimation(child2, child3); + uut.push(child1, new CommandListenerAdapter()); + uut.push(child2, new CommandListenerAdapter()); + uut.push(child3, new CommandListenerAdapter()); + + uut.pop(Options.EMPTY, new CommandListenerAdapter()); + verify(eventEmitter).emitScreenPoppedEvent(child3.getId()); + verifyNoMoreInteractions(eventEmitter); + } + @Test public void stackOperations() { assertThat(uut.peek()).isNull(); @@ -957,12 +982,9 @@ public void buttonPressInvokedOnCurrentStack() { @Test public void mergeChildOptions_updatesViewWithNewOptions() { - StackController uut = spy(new StackControllerBuilder(activity) - .setTopBarController(new TopBarController()) - .setId("stack") - .setInitialOptions(new Options()) - .setStackPresenter(new StackPresenter(activity, new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewCreatorMock(), new TitleBarReactViewCreatorMock(), ImageLoaderMock.mock(), new RenderChecker(), Options.EMPTY)) - .build()); + StackController uut = spy(TestUtils.newStackController(activity) + .setId("stack") + .build()); Options optionsToMerge = new Options(); ViewController vc = mock(ViewController.class); uut.mergeChildOptions(optionsToMerge, vc); @@ -971,11 +993,8 @@ public void mergeChildOptions_updatesViewWithNewOptions() { @Test public void mergeChildOptions_updatesParentControllerWithNewOptions() { - StackController uut = new StackControllerBuilder(activity) - .setTopBarController(new TopBarController()) + StackController uut = TestUtils.newStackController(activity) .setId("stack") - .setInitialOptions(new Options()) - .setStackPresenter(new StackPresenter(activity, new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewCreatorMock(), new TitleBarReactViewCreatorMock(), ImageLoaderMock.mock(), new RenderChecker(), Options.EMPTY)) .build(); ParentController parentController = Mockito.mock(ParentController.class); uut.setParentController(parentController); @@ -1136,6 +1155,7 @@ private StackController createStack(List children) { private StackControllerBuilder createStackBuilder(String id, List children) { createTopBarController(); return TestUtils.newStackController(activity) + .setEventEmitter(eventEmitter) .setChildren(children) .setId(id) .setTopBarController(topBarController) diff --git a/lib/ios/RNNCommandsHandler.m b/lib/ios/RNNCommandsHandler.m index c3ea60d2118..0dc96fc4251 100644 --- a/lib/ios/RNNCommandsHandler.m +++ b/lib/ios/RNNCommandsHandler.m @@ -205,10 +205,8 @@ - (void)pop:(NSString*)componentId commandId:(NSString*)commandId mergeOptions:( UINavigationController *nvc = vc.navigationController; if ([nvc topViewController] == vc) { - if (vc.resolveOptionsWithDefault.animations.pop) { + if (vc.resolveOptions.animations.pop.hasCustomAnimation) { nvc.delegate = vc; - } else { - nvc.delegate = nil; } } else { NSMutableArray * vcs = nvc.viewControllers.mutableCopy; diff --git a/lib/ios/RNNComponentViewController.m b/lib/ios/RNNComponentViewController.m index b90e6664d9c..5cc1e21bab3 100644 --- a/lib/ios/RNNComponentViewController.m +++ b/lib/ios/RNNComponentViewController.m @@ -11,8 +11,6 @@ - (instancetype)initWithLayoutInfo:(RNNLayoutInfo *)layoutInfo rootViewCreator:( self.animator = [[RNNAnimator alloc] initWithTransitionOptions:self.resolveOptions.customTransition]; - self.navigationController.delegate = self; - return self; } diff --git a/lib/ios/RNNEventEmitter.h b/lib/ios/RNNEventEmitter.h index 1e8400d11f9..2d19f701b40 100644 --- a/lib/ios/RNNEventEmitter.h +++ b/lib/ios/RNNEventEmitter.h @@ -1,4 +1,3 @@ - #import #import @@ -26,5 +25,7 @@ - (void)sendModalsDismissedEvent:(NSString *)componentId numberOfModalsDismissed:(NSNumber *)modalsDismissed; +- (void)sendScreenPoppedEvent:(NSString *)componentId; + @end diff --git a/lib/ios/RNNEventEmitter.m b/lib/ios/RNNEventEmitter.m index 48cdbc149aa..ab1aba68d11 100644 --- a/lib/ios/RNNEventEmitter.m +++ b/lib/ios/RNNEventEmitter.m @@ -2,8 +2,8 @@ #import "RNNUtils.h" @implementation RNNEventEmitter { - NSInteger _appLaunchedListenerCount; - BOOL _appLaunchedEventDeferred; + NSInteger _appLaunchedListenerCount; + BOOL _appLaunchedEventDeferred; } RCT_EXPORT_MODULE(); @@ -18,115 +18,123 @@ @implementation RNNEventEmitter { static NSString* const SearchBarUpdated = @"RNN.SearchBarUpdated"; static NSString* const SearchBarCancelPressed = @"RNN.SearchBarCancelPressed"; static NSString* const PreviewCompleted = @"RNN.PreviewCompleted"; - --(NSArray *)supportedEvents { - return @[AppLaunched, - CommandCompleted, - BottomTabSelected, - ComponentDidAppear, - ComponentDidDisappear, - NavigationButtonPressed, - ModalDismissed, - SearchBarUpdated, - SearchBarCancelPressed, - PreviewCompleted]; +static NSString* const ScreenPopped = @"RNN.ScreenPopped"; + +- (NSArray *)supportedEvents { + return @[AppLaunched, + CommandCompleted, + BottomTabSelected, + ComponentDidAppear, + ComponentDidDisappear, + NavigationButtonPressed, + ModalDismissed, + SearchBarUpdated, + SearchBarCancelPressed, + PreviewCompleted, + ScreenPopped]; } # pragma mark public --(void)sendOnAppLaunched { - if (_appLaunchedListenerCount > 0) { - [self send:AppLaunched body:nil]; - } else { - _appLaunchedEventDeferred = TRUE; - } +- (void)sendOnAppLaunched { + if (_appLaunchedListenerCount > 0) { + [self send:AppLaunched body:nil]; + } else { + _appLaunchedEventDeferred = TRUE; + } } --(void)sendComponentDidAppear:(NSString *)componentId componentName:(NSString *)componentName { - [self send:ComponentDidAppear body:@{ - @"componentId":componentId, - @"componentName": componentName - }]; +- (void)sendComponentDidAppear:(NSString *)componentId componentName:(NSString *)componentName { + [self send:ComponentDidAppear body:@{ + @"componentId":componentId, + @"componentName": componentName + }]; } --(void)sendComponentDidDisappear:(NSString *)componentId componentName:(NSString *)componentName{ - [self send:ComponentDidDisappear body:@{ - @"componentId":componentId, - @"componentName": componentName - }]; +- (void)sendComponentDidDisappear:(NSString *)componentId componentName:(NSString *)componentName{ + [self send:ComponentDidDisappear body:@{ + @"componentId":componentId, + @"componentName": componentName + }]; } --(void)sendOnNavigationButtonPressed:(NSString *)componentId buttonId:(NSString*)buttonId { - [self send:NavigationButtonPressed body:@{ - @"componentId": componentId, - @"buttonId": buttonId - }]; +- (void)sendOnNavigationButtonPressed:(NSString *)componentId buttonId:(NSString*)buttonId { + [self send:NavigationButtonPressed body:@{ + @"componentId": componentId, + @"buttonId": buttonId + }]; } --(void)sendBottomTabSelected:(NSNumber *)selectedTabIndex unselected:(NSNumber*)unselectedTabIndex { - [self send:BottomTabSelected body:@{ - @"selectedTabIndex": selectedTabIndex, - @"unselectedTabIndex": unselectedTabIndex - }]; +- (void)sendBottomTabSelected:(NSNumber *)selectedTabIndex unselected:(NSNumber*)unselectedTabIndex { + [self send:BottomTabSelected body:@{ + @"selectedTabIndex": selectedTabIndex, + @"unselectedTabIndex": unselectedTabIndex + }]; } --(void)sendOnNavigationCommandCompletion:(NSString *)commandName commandId:(NSString *)commandId params:(NSDictionary*)params { - [self send:CommandCompleted body:@{ - @"commandId":commandId, - @"commandName":commandName, - @"params": params, - @"completionTime": [RNNUtils getCurrentTimestamp] - }]; +- (void)sendOnNavigationCommandCompletion:(NSString *)commandName commandId:(NSString *)commandId params:(NSDictionary*)params { + [self send:CommandCompleted body:@{ + @"commandId":commandId, + @"commandName":commandName, + @"params": params, + @"completionTime": [RNNUtils getCurrentTimestamp] + }]; } --(void)sendOnSearchBarUpdated:(NSString *)componentId - text:(NSString*)text - isFocused:(BOOL)isFocused { - [self send:SearchBarUpdated body:@{ - @"componentId": componentId, - @"text": text, - @"isFocused": @(isFocused) - }]; +- (void)sendOnSearchBarUpdated:(NSString *)componentId + text:(NSString*)text + isFocused:(BOOL)isFocused { + [self send:SearchBarUpdated body:@{ + @"componentId": componentId, + @"text": text, + @"isFocused": @(isFocused) + }]; } - (void)sendOnSearchBarCancelPressed:(NSString *)componentId { - [self send:SearchBarCancelPressed body:@{ - @"componentId": componentId - }]; + [self send:SearchBarCancelPressed body:@{ + @"componentId": componentId + }]; } - (void)sendOnPreviewCompleted:(NSString *)componentId previewComponentId:(NSString *)previewComponentId { - [self send:PreviewCompleted body:@{ - @"componentId": componentId, - @"previewComponentId": previewComponentId - }]; + [self send:PreviewCompleted body:@{ + @"componentId": componentId, + @"previewComponentId": previewComponentId + }]; } - (void)sendModalsDismissedEvent:(NSString *)componentId numberOfModalsDismissed:(NSNumber *)modalsDismissed { - [self send:ModalDismissed body:@{ - @"componentId": componentId, - @"modalsDismissed": modalsDismissed - }]; + [self send:ModalDismissed body:@{ + @"componentId": componentId, + @"modalsDismissed": modalsDismissed + }]; +} + +- (void)sendScreenPoppedEvent:(NSString *)componentId { + [self send:ScreenPopped body:@{ + @"componentId": componentId + }]; } - (void)addListener:(NSString *)eventName { - [super addListener:eventName]; - if ([eventName isEqualToString:AppLaunched]) { - _appLaunchedListenerCount++; - if (_appLaunchedEventDeferred) { - _appLaunchedEventDeferred = FALSE; - [self sendOnAppLaunched]; - } - } + [super addListener:eventName]; + if ([eventName isEqualToString:AppLaunched]) { + _appLaunchedListenerCount++; + if (_appLaunchedEventDeferred) { + _appLaunchedEventDeferred = FALSE; + [self sendOnAppLaunched]; + } + } } # pragma mark private --(void)send:(NSString *)eventName body:(id)body { - if (self.bridge == nil) { - return; - } - [self sendEventWithName:eventName body:body]; +- (void)send:(NSString *)eventName body:(id)body { + if (self.bridge == nil) { + return; + } + [self sendEventWithName:eventName body:body]; } @end diff --git a/lib/ios/RNNNavigationStackManager.m b/lib/ios/RNNNavigationStackManager.m index fac18dd6f41..57714ec8f4f 100644 --- a/lib/ios/RNNNavigationStackManager.m +++ b/lib/ios/RNNNavigationStackManager.m @@ -19,8 +19,6 @@ - (void)push:(UIViewController *)newTop onTop:(UIViewController *)onTopViewContr if (animationDelegate) { nvc.delegate = animationDelegate; - } else { - nvc.delegate = nil; } [self performAnimationBlock:^{ diff --git a/lib/ios/RNNStackController.m b/lib/ios/RNNStackController.m index 6874d5bf5f0..1d668f69399 100644 --- a/lib/ios/RNNStackController.m +++ b/lib/ios/RNNStackController.m @@ -1,9 +1,17 @@ #import "RNNStackController.h" #import "RNNComponentViewController.h" -@implementation RNNStackController +@implementation RNNStackController { + UIViewController* _presentedViewController; +} + +- (instancetype)init { + self = [super init]; + self.delegate = self; + return self; +} --(void)setDefaultOptions:(RNNNavigationOptions *)defaultOptions { +- (void)setDefaultOptions:(RNNNavigationOptions *)defaultOptions { [super setDefaultOptions:defaultOptions]; [self.presenter setDefaultOptions:defaultOptions]; } @@ -30,17 +38,32 @@ - (UIModalPresentationStyle)modalPresentationStyle { } - (UIViewController *)popViewControllerAnimated:(BOOL)animated { - if (self.viewControllers.count > 1) { - UIViewController *controller = self.viewControllers[self.viewControllers.count - 2]; - if ([controller isKindOfClass:[RNNComponentViewController class]]) { - RNNComponentViewController *rnnController = (RNNComponentViewController *)controller; - [self.presenter applyOptionsBeforePopping:rnnController.resolveOptions]; - } - } - + [self prepareForPop]; return [super popViewControllerAnimated:animated]; } +- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { + if ([self.viewControllers indexOfObject:_presentedViewController] < 0) { + [self sendScreenPoppedEvent:_presentedViewController]; + } + + _presentedViewController = viewController; +} + +- (void)sendScreenPoppedEvent:(UIViewController *)poppedScreen { + [self.eventEmitter sendScreenPoppedEvent:poppedScreen.layoutInfo.componentId]; +} + +- (void)prepareForPop { + if (self.viewControllers.count > 1) { + UIViewController *controller = self.viewControllers[self.viewControllers.count - 2]; + if ([controller isKindOfClass:[RNNComponentViewController class]]) { + RNNComponentViewController *rnnController = (RNNComponentViewController *)controller; + [self.presenter applyOptionsBeforePopping:rnnController.resolveOptions]; + } + } +} + - (UIViewController *)childViewControllerForStatusBarStyle { return self.topViewController; } diff --git a/lib/src/adapters/NativeEventsReceiver.ts b/lib/src/adapters/NativeEventsReceiver.ts index 29802c1ef7b..ba5b1a54158 100644 --- a/lib/src/adapters/NativeEventsReceiver.ts +++ b/lib/src/adapters/NativeEventsReceiver.ts @@ -6,7 +6,8 @@ import { SearchBarUpdatedEvent, SearchBarCancelPressedEvent, PreviewCompletedEvent, - ModalDismissedEvent + ModalDismissedEvent, + ScreenPoppedEvent } from '../interfaces/ComponentEvents'; import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Events'; @@ -67,4 +68,8 @@ export class NativeEventsReceiver { public registerBottomTabSelectedListener(callback: (data: BottomTabSelectedEvent) => void): EmitterSubscription { return this.emitter.addListener('RNN.BottomTabSelected', callback); } + + public registerScreenPoppedListener(callback: (event: ScreenPoppedEvent) => void): EmitterSubscription { + return this.emitter.addListener('RNN.ScreenPopped', callback); + } } diff --git a/lib/src/events/ComponentEventsObserver.test.tsx b/lib/src/events/ComponentEventsObserver.test.tsx index 368ef6bee05..510ed6fadbc 100644 --- a/lib/src/events/ComponentEventsObserver.test.tsx +++ b/lib/src/events/ComponentEventsObserver.test.tsx @@ -18,6 +18,7 @@ describe('ComponentEventsObserver', () => { const searchBarCancelPressedFn = jest.fn(); const previewCompletedFn = jest.fn(); const modalDismissedFn = jest.fn(); + const screenPoppedFn = jest.fn(); let subscription: EventSubscription; let uut: ComponentEventsObserver; @@ -68,6 +69,10 @@ describe('ComponentEventsObserver', () => { previewCompletedFn(event); } + screenPopped(event: any) { + screenPoppedFn(event); + } + render() { return 'Hello'; } @@ -115,6 +120,10 @@ describe('ComponentEventsObserver', () => { previewCompletedFn(event); } + screenPopped(event: any) { + screenPoppedFn(event); + } + render() { return 'Hello'; } @@ -191,6 +200,10 @@ describe('ComponentEventsObserver', () => { expect(previewCompletedFn).toHaveBeenCalledTimes(1); expect(previewCompletedFn).toHaveBeenCalledWith({ componentId: 'myCompId' }); + uut.notifyScreenPopped({ componentId: 'myCompId' }); + expect(screenPoppedFn).toHaveBeenCalledTimes(1); + expect(screenPoppedFn).toHaveBeenLastCalledWith({ componentId: 'myCompId' }) + tree.unmount(); expect(willUnmountFn).toHaveBeenCalledTimes(1); }); diff --git a/lib/src/events/ComponentEventsObserver.ts b/lib/src/events/ComponentEventsObserver.ts index 2c45d2abd48..6c4c4db2ff9 100644 --- a/lib/src/events/ComponentEventsObserver.ts +++ b/lib/src/events/ComponentEventsObserver.ts @@ -12,7 +12,8 @@ import { SearchBarCancelPressedEvent, ComponentEvent, PreviewCompletedEvent, - ModalDismissedEvent + ModalDismissedEvent, + ScreenPoppedEvent } from '../interfaces/ComponentEvents'; import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver'; import { Store } from '../components/Store'; @@ -34,6 +35,7 @@ export class ComponentEventsObserver { this.notifySearchBarUpdated = this.notifySearchBarUpdated.bind(this); this.notifySearchBarCancelPressed = this.notifySearchBarCancelPressed.bind(this); this.notifyPreviewCompleted = this.notifyPreviewCompleted.bind(this); + this.notifyScreenPopped = this.notifyScreenPopped.bind(this); } public registerOnceForAllComponentEvents() { @@ -46,6 +48,7 @@ export class ComponentEventsObserver { this.nativeEventsReceiver.registerSearchBarUpdatedListener(this.notifySearchBarUpdated); this.nativeEventsReceiver.registerSearchBarCancelPressedListener(this.notifySearchBarCancelPressed); this.nativeEventsReceiver.registerPreviewCompletedListener(this.notifyPreviewCompleted); + this.nativeEventsReceiver.registerScreenPoppedListener(this.notifyPreviewCompleted); } public bindComponent(component: React.Component, componentId?: string): EventSubscription { @@ -96,6 +99,10 @@ export class ComponentEventsObserver { this.triggerOnAllListenersByComponentId(event, 'previewCompleted'); } + notifyScreenPopped(event: ScreenPoppedEvent) { + this.triggerOnAllListenersByComponentId(event, 'screenPopped'); + } + private triggerOnAllListenersByComponentId(event: ComponentEvent, method: string) { forEach(this.listeners[event.componentId], (component) => { if (component && component[method]) { diff --git a/lib/src/events/EventsRegistry.test.tsx b/lib/src/events/EventsRegistry.test.tsx index 0e713f6638b..4707342182e 100644 --- a/lib/src/events/EventsRegistry.test.tsx +++ b/lib/src/events/EventsRegistry.test.tsx @@ -112,4 +112,11 @@ describe('EventsRegistry', () => { mockScreenEventsRegistry.bindComponent.mockReturnValueOnce(subscription); expect(uut.bindComponent({} as React.Component)).toEqual(subscription); }); + + it('delegates screenPopped to nativeEventsReceiver', () => { + const cb = jest.fn(); + uut.registerScreenPoppedListener(cb); + expect(mockNativeEventsReceiver.registerScreenPoppedListener).toHaveBeenCalledTimes(1); + expect(mockNativeEventsReceiver.registerScreenPoppedListener).toHaveBeenCalledWith(cb); + }); }); diff --git a/lib/src/events/EventsRegistry.ts b/lib/src/events/EventsRegistry.ts index 0125017d584..2099c26195e 100644 --- a/lib/src/events/EventsRegistry.ts +++ b/lib/src/events/EventsRegistry.ts @@ -11,7 +11,8 @@ import { SearchBarUpdatedEvent, SearchBarCancelPressedEvent, PreviewCompletedEvent, - ModalDismissedEvent + ModalDismissedEvent, + ScreenPoppedEvent } from '../interfaces/ComponentEvents'; import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Events'; @@ -65,4 +66,9 @@ export class EventsRegistry { public bindComponent(component: React.Component, componentId?: string): EventSubscription { return this.componentEventsObserver.bindComponent(component, componentId); } + + public registerScreenPoppedListener(callback: (event: ScreenPoppedEvent) => void): EmitterSubscription { + return this.nativeEventsReceiver.registerScreenPoppedListener(callback); + } + } diff --git a/lib/src/interfaces/ComponentEvents.ts b/lib/src/interfaces/ComponentEvents.ts index 9dcaa1e59cb..cc1cec47462 100644 --- a/lib/src/interfaces/ComponentEvents.ts +++ b/lib/src/interfaces/ComponentEvents.ts @@ -33,3 +33,7 @@ export interface PreviewCompletedEvent extends ComponentEvent { componentName?: string; previewComponentId?: string; } + +export interface ScreenPoppedEvent extends ComponentEvent { + componentId: string; +} diff --git a/playground/ios/NavigationTests/RNNNavigationControllerTest.m b/playground/ios/NavigationTests/RNNNavigationControllerTest.m index e44eb40d263..95813d9f0d6 100644 --- a/playground/ios/NavigationTests/RNNNavigationControllerTest.m +++ b/playground/ios/NavigationTests/RNNNavigationControllerTest.m @@ -17,17 +17,19 @@ @implementation RNNNavigationControllerTest { UIViewController* _vc3; RNNNavigationOptions* _options; RNNTestRootViewCreator* _creator; + RNNEventEmitter* _eventEmitter; } - (void)setUp { [super setUp]; + _eventEmitter = [OCMockObject niceMockForClass:[RNNEventEmitter class]]; _creator = [[RNNTestRootViewCreator alloc] init]; _vc1 = [[RNNComponentViewController alloc] initWithLayoutInfo:nil rootViewCreator:nil eventEmitter:nil presenter:[OCMockObject partialMockForObject:[[RNNComponentPresenter alloc] init]] options:[[RNNNavigationOptions alloc] initEmptyOptions] defaultOptions:[[RNNNavigationOptions alloc] initEmptyOptions]]; _vc2 = [[RNNComponentViewController alloc] initWithLayoutInfo:nil rootViewCreator:nil eventEmitter:nil presenter:[[RNNComponentPresenter alloc] init] options:[[RNNNavigationOptions alloc] initEmptyOptions] defaultOptions:[[RNNNavigationOptions alloc] initEmptyOptions]]; _vc2Mock = [OCMockObject partialMockForObject:_vc2]; _vc3 = [UIViewController new]; _options = [OCMockObject partialMockForObject:[[RNNNavigationOptions alloc] initEmptyOptions]]; - self.uut = [[RNNStackController alloc] initWithLayoutInfo:nil creator:_creator options:_options defaultOptions:nil presenter:[OCMockObject partialMockForObject:[[RNNStackPresenter alloc] init]] eventEmitter:nil childViewControllers:@[_vc1, _vc2]]; + self.uut = [[RNNStackController alloc] initWithLayoutInfo:nil creator:_creator options:_options defaultOptions:nil presenter:[OCMockObject partialMockForObject:[[RNNStackPresenter alloc] init]] eventEmitter:_eventEmitter childViewControllers:@[_vc1, _vc2]]; } - (void)testInitWithLayoutInfo_shouldBindPresenter { @@ -139,12 +141,19 @@ - (void)testPopViewControllerShouldInvokeApplyOptionsBeforePoppingForDestination [uut setViewControllers:@[_vc1, _vc2]]; [[(id)uut.presenter expect] applyOptionsBeforePopping:[OCMArg any]]; - [uut popViewControllerAnimated:NO]; - [(id)uut.presenter verify]; } +- (void)testPopViewController_ShouldEmitScreenPoppedEvent { + RNNStackController* uut = [RNNStackController new]; + [uut setViewControllers:@[_vc1, _vc2]]; + + [[(id)uut.eventEmitter expect] sendScreenPoppedEvent:_vc2.layoutInfo.componentId]; + [uut popViewControllerAnimated:NO]; + [(id)uut.eventEmitter verify]; +} + - (void)testOverrideOptionsShouldOverrideOptionsState { RNNNavigationOptions* overrideOptions = [[RNNNavigationOptions alloc] initEmptyOptions]; [(RNNNavigationOptions*)[(id)self.uut.options expect] overrideOptions:overrideOptions];