UNPKG

52.2 kBPlain TextView Raw
1#ifdef RCT_NEW_ARCH_ENABLED
2#import <React/RCTFabricComponentsPlugins.h>
3#import <React/RCTFabricSurface.h>
4#import <React/RCTMountingTransactionObserving.h>
5#import <React/RCTSurfaceTouchHandler.h>
6#import <React/RCTSurfaceView.h>
7#import <React/UIView+React.h>
8#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
9#import <react/renderer/components/rnscreens/EventEmitters.h>
10#import <react/renderer/components/rnscreens/Props.h>
11#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
12#import "RCTSurfaceTouchHandler+RNSUtility.h"
13#else
14#import <React/RCTBridge.h>
15#import <React/RCTRootContentView.h>
16#import <React/RCTShadowView.h>
17#import <React/RCTTouchHandler.h>
18#import <React/RCTUIManager.h>
19#import <React/RCTUIManagerUtils.h>
20#import "RCTTouchHandler+RNSUtility.h"
21#endif // RCT_NEW_ARCH_ENABLED
22
23#import "RNSScreen.h"
24#import "RNSScreenStack.h"
25#import "RNSScreenStackAnimator.h"
26#import "RNSScreenStackHeaderConfig.h"
27#import "RNSScreenWindowTraits.h"
28
29#ifdef RCT_NEW_ARCH_ENABLED
30namespace react = facebook::react;
31#endif // RCT_NEW_ARCH_ENABLED
32
33@interface RNSScreenStackView () <
34 UINavigationControllerDelegate,
35 UIAdaptivePresentationControllerDelegate,
36 UIGestureRecognizerDelegate,
37 UIViewControllerTransitioningDelegate
38#ifdef RCT_NEW_ARCH_ENABLED
39 ,
40 RCTMountingTransactionObserving
41#endif
42 >
43
44@property (nonatomic) NSMutableArray<UIViewController *> *presentedModals;
45@property (nonatomic) BOOL updatingModals;
46@property (nonatomic) BOOL scheduleModalsUpdate;
47
48@end
49
50@implementation RNSNavigationController
51
52#if !TARGET_OS_TV
53- (UIViewController *)childViewControllerForStatusBarStyle
54{
55 return [self topViewController];
56}
57
58- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
59{
60 return [self topViewController].preferredStatusBarUpdateAnimation;
61}
62
63- (UIViewController *)childViewControllerForStatusBarHidden
64{
65 return [self topViewController];
66}
67
68- (void)viewDidLayoutSubviews
69{
70 [super viewDidLayoutSubviews];
71 if ([self.topViewController isKindOfClass:[RNSScreen class]]) {
72 RNSScreen *screenController = (RNSScreen *)self.topViewController;
73 BOOL isNotDismissingModal = screenController.presentedViewController == nil ||
74 (screenController.presentedViewController != nil &&
75 ![screenController.presentedViewController isBeingDismissed]);
76
77 // Calculate header height during simple transition from one screen to another.
78 // If RNSScreen includes a navigation controller of type RNSNavigationController, it should not calculate
79 // header height, as it could have nested stack.
80 if (![screenController hasNestedStack] && isNotDismissingModal) {
81 [screenController calculateAndNotifyHeaderHeightChangeIsModal:NO];
82 }
83 }
84}
85
86- (UIInterfaceOrientationMask)supportedInterfaceOrientations
87{
88 return [self topViewController].supportedInterfaceOrientations;
89}
90
91- (UIViewController *)childViewControllerForHomeIndicatorAutoHidden
92{
93 return [self topViewController];
94}
95#endif
96
97@end
98
99#if !TARGET_OS_TV && !TARGET_OS_VISION
100@interface RNSScreenEdgeGestureRecognizer : UIScreenEdgePanGestureRecognizer
101@end
102
103@implementation RNSScreenEdgeGestureRecognizer
104@end
105
106@interface RNSPanGestureRecognizer : UIPanGestureRecognizer
107@end
108
109@implementation RNSPanGestureRecognizer
110@end
111#endif
112
113@implementation RNSScreenStackView {
114 UINavigationController *_controller;
115 NSMutableArray<RNSScreenView *> *_reactSubviews;
116 BOOL _invalidated;
117 BOOL _isFullWidthSwiping;
118 UIPercentDrivenInteractiveTransition *_interactionController;
119 BOOL _hasLayout;
120 __weak RNSScreenStackManager *_manager;
121 BOOL _updateScheduled;
122#ifdef RCT_NEW_ARCH_ENABLED
123 UIView *_snapshot;
124#endif
125}
126
127#ifdef RCT_NEW_ARCH_ENABLED
128- (instancetype)initWithFrame:(CGRect)frame
129{
130 if (self = [super initWithFrame:frame]) {
131 static const auto defaultProps = std::make_shared<const react::RNSScreenStackProps>();
132 _props = defaultProps;
133 [self initCommonProps];
134 }
135
136 return self;
137}
138#endif // RCT_NEW_ARCH_ENABLED
139
140- (instancetype)initWithManager:(RNSScreenStackManager *)manager
141{
142 if (self = [super init]) {
143 _hasLayout = NO;
144 _invalidated = NO;
145 _manager = manager;
146 [self initCommonProps];
147 }
148 return self;
149}
150
151- (void)initCommonProps
152{
153 _reactSubviews = [NSMutableArray new];
154 _presentedModals = [NSMutableArray new];
155 _controller = [RNSNavigationController new];
156 _controller.delegate = self;
157#if !TARGET_OS_TV && !TARGET_OS_VISION
158 [self setupGestureHandlers];
159#endif
160 // we have to initialize viewControllers with a non empty array for
161 // largeTitle header to render in the opened state. If it is empty
162 // the header will render in collapsed state which is perhaps a bug
163 // in UIKit but ¯\_(ツ)_/¯
164 [_controller setViewControllers:@[ [UIViewController new] ]];
165}
166
167#pragma mark - helper methods
168
169- (BOOL)shouldCancelDismissFromView:(RNSScreenView *)fromView toView:(RNSScreenView *)toView
170{
171 int fromIndex = (int)[_reactSubviews indexOfObject:fromView];
172 int toIndex = (int)[_reactSubviews indexOfObject:toView];
173 for (int i = fromIndex; i > toIndex; i--) {
174 if (_reactSubviews[i].preventNativeDismiss) {
175 return YES;
176 break;
177 }
178 }
179 return NO;
180}
181
182#pragma mark - Common
183
184- (void)emitOnFinishTransitioningEvent
185{
186#ifdef RCT_NEW_ARCH_ENABLED
187 if (_eventEmitter != nullptr) {
188 std::dynamic_pointer_cast<const react::RNSScreenStackEventEmitter>(_eventEmitter)
189 ->onFinishTransitioning(react::RNSScreenStackEventEmitter::OnFinishTransitioning{});
190 }
191#else
192 if (self.onFinishTransitioning) {
193 self.onFinishTransitioning(nil);
194 }
195#endif
196}
197
198- (void)navigationController:(UINavigationController *)navigationController
199 willShowViewController:(UIViewController *)viewController
200 animated:(BOOL)animated
201{
202 UIView *view = viewController.view;
203#ifdef RCT_NEW_ARCH_ENABLED
204 if (![view isKindOfClass:[RNSScreenView class]]) {
205 // if the current view is a snapshot, config was already removed so we don't trigger the method
206 return;
207 }
208#endif
209 RNSScreenStackHeaderConfig *config = nil;
210 for (UIView *subview in view.reactSubviews) {
211 if ([subview isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
212 config = (RNSScreenStackHeaderConfig *)subview;
213 break;
214 }
215 }
216 [RNSScreenStackHeaderConfig willShowViewController:viewController animated:animated withConfig:config];
217}
218
219- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
220{
221 // We don't directly set presentation delegate but instead rely on the ScreenView's delegate to
222 // forward certain calls to the container (Stack).
223 if ([presentationController.presentedViewController isKindOfClass:[RNSScreen class]]) {
224 // we trigger the update of status bar's appearance here because there is no other lifecycle method
225 // that can handle it when dismissing a modal, the same for orientation
226 [RNSScreenWindowTraits updateWindowTraits];
227 [_presentedModals removeObject:presentationController.presentedViewController];
228
229 _updatingModals = NO;
230#ifdef RCT_NEW_ARCH_ENABLED
231 [self emitOnFinishTransitioningEvent];
232#else
233 // we double check if there are no new controllers pending to be presented since someone could
234 // have tried to push another one during the transition.
235 // We don't do it on Fabric since update of container will be triggered from "unmount" method afterwards
236 [self updateContainer];
237 if (self.onFinishTransitioning) {
238 // instead of directly triggering onFinishTransitioning this time we enqueue the event on the
239 // main queue. We do that because onDismiss event is also enqueued and we want for the transition
240 // finish event to arrive later than onDismiss (see RNSScreen#notifyDismiss)
241 dispatch_async(dispatch_get_main_queue(), ^{
242 [self emitOnFinishTransitioningEvent];
243 });
244 }
245#endif
246 }
247}
248
249- (NSArray<UIView *> *)reactSubviews
250{
251 return _reactSubviews;
252}
253
254- (void)didMoveToWindow
255{
256 [super didMoveToWindow];
257#ifdef RCT_NEW_ARCH_ENABLED
258 // for handling nested stacks
259 [self maybeAddToParentAndUpdateContainer];
260#else
261 if (!_invalidated) {
262 // We check whether the view has been invalidated before running side-effects in didMoveToWindow
263 // This is needed because when LayoutAnimations are used it is possible for view to be re-attached
264 // to a window despite the fact it has been removed from the React Native view hierarchy.
265 [self maybeAddToParentAndUpdateContainer];
266 }
267#endif
268}
269
270- (void)maybeAddToParentAndUpdateContainer
271{
272 BOOL wasScreenMounted = _controller.parentViewController != nil;
273#ifdef RCT_NEW_ARCH_ENABLED
274 BOOL isScreenReadyForShowing = self.window;
275#else
276 BOOL isScreenReadyForShowing = self.window && _hasLayout;
277#endif
278 if (!isScreenReadyForShowing && !wasScreenMounted) {
279 // We wait with adding to parent controller until the stack is mounted and has its initial
280 // layout done.
281 // If we add it before layout, some of the items (specifically items from the navigation bar),
282 // won't be able to position properly. Also the position and size of such items, even if it
283 // happens to change, won't be properly updated (this is perhaps some internal issue of UIKit).
284 // If we add it when window is not attached, some of the view transitions will be bloced (i.e.
285 // modal transitions) and the internal view controler's state will get out of sync with what's
286 // on screen without us knowing.
287 return;
288 }
289 [self updateContainer];
290 if (!wasScreenMounted) {
291 // when stack hasn't been added to parent VC yet we do two things:
292 // 1) we run updateContainer (the one above) – we do this because we want push view controllers to
293 // be installed before the VC is mounted. If we do that after it is added to parent the push
294 // updates operations are going to be blocked by UIKit.
295 // 2) we add navigation VS to parent – this is needed for the VC lifecycle events to be dispatched
296 // properly
297 // 3) we again call updateContainer – this time we do this to open modal controllers. Modals
298 // won't open in (1) because they require navigator to be added to parent. We handle that case
299 // gracefully in setModalViewControllers and can retry opening at any point.
300 [self reactAddControllerToClosestParent:_controller];
301 [self updateContainer];
302 }
303}
304
305- (void)reactAddControllerToClosestParent:(UIViewController *)controller
306{
307 if (!controller.parentViewController) {
308 UIView *parentView = (UIView *)self.reactSuperview;
309 while (parentView) {
310 if (parentView.reactViewController) {
311 [parentView.reactViewController addChildViewController:controller];
312 [self addSubview:controller.view];
313#if !TARGET_OS_TV
314 _controller.interactivePopGestureRecognizer.delegate = self;
315#endif
316 [controller didMoveToParentViewController:parentView.reactViewController];
317 // On iOS pre 12 we observed that `willShowViewController` delegate method does not always
318 // get triggered when the navigation controller is instantiated. As the only thing we do in
319 // that delegate method is ask nav header to update to the current state it does not hurt to
320 // trigger that logic from here too such that we can be sure the header is properly updated.
321 [self navigationController:_controller willShowViewController:_controller.topViewController animated:NO];
322 break;
323 }
324 parentView = (UIView *)parentView.reactSuperview;
325 }
326 return;
327 }
328}
329
330+ (UIViewController *)findTopMostPresentedViewControllerFromViewController:(UIViewController *)controller
331{
332 auto presentedVc = controller;
333 while (presentedVc.presentedViewController != nil) {
334 presentedVc = presentedVc.presentedViewController;
335 }
336 return presentedVc;
337}
338
339- (UIViewController *)findReactRootViewController
340{
341 UIView *parent = self;
342 while (parent) {
343 parent = parent.reactSuperview;
344 if (parent.isReactRootView) {
345 return parent.reactViewController;
346 }
347 }
348 return nil;
349}
350
351- (UIViewController *)lastFromPresentedViewControllerChainStartingFrom:(UIViewController *)vc
352{
353 UIViewController *lastNonNullVc = vc;
354 UIViewController *lastVc = vc.presentedViewController;
355 while (lastVc != nil) {
356 lastNonNullVc = lastVc;
357 lastVc = lastVc.presentedViewController;
358 }
359 return lastNonNullVc;
360}
361
362- (void)setModalViewControllers:(NSArray<UIViewController *> *)controllers
363{
364 // prevent re-entry
365 if (_updatingModals) {
366 _scheduleModalsUpdate = YES;
367 return;
368 }
369
370 // when there is no change we return immediately. This check is important because sometime we may
371 // accidently trigger modal dismiss if we don't verify to run the below code only when an actual
372 // change in the list of presented modal was made.
373 if ([_presentedModals isEqualToArray:controllers]) {
374 return;
375 }
376
377 // if view controller is not yet attached to window we skip updates now and run them when view
378 // is attached
379 if (self.window == nil && _presentedModals.lastObject.view.window == nil) {
380 return;
381 }
382
383 _updatingModals = YES;
384
385 NSMutableArray<UIViewController *> *newControllers = [NSMutableArray arrayWithArray:controllers];
386 [newControllers removeObjectsInArray:_presentedModals];
387
388 // We need to find bottom-most view controller that should stay on the stack
389 // for the duration of transition.
390
391 // There are couple of scenarios:
392 // (1) no modals are presented or all modals were presented by this RNSNavigationController,
393 // (2) there are modals presented by other RNSNavigationControllers (nested/outer),
394 // (3) there are modals presented by other controllers (e.g. React Native's Modal view).
395
396 // Last controller that is common for both _presentedModals & controllers
397 __block UIViewController *changeRootController = _controller;
398 // Last common controller index + 1
399 NSUInteger changeRootIndex = 0;
400 for (NSUInteger i = 0; i < MIN(_presentedModals.count, controllers.count); i++) {
401 if (_presentedModals[i] == controllers[i]) {
402 changeRootController = controllers[i];
403 changeRootIndex = i + 1;
404 } else {
405 break;
406 }
407 }
408
409 // we verify that controllers added on top of changeRootIndex are all new. Unfortunately modal
410 // VCs cannot be reshuffled (there are some visual glitches when we try to dismiss then show as
411 // even non-animated dismissal has delay and updates the screen several times)
412 for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
413 if ([_presentedModals containsObject:controllers[i]]) {
414 RCTAssert(false, @"Modally presented controllers are being reshuffled, this is not allowed");
415 }
416 }
417
418 __weak RNSScreenStackView *weakSelf = self;
419
420 void (^afterTransitions)(void) = ^{
421 [weakSelf emitOnFinishTransitioningEvent];
422 weakSelf.updatingModals = NO;
423 if (weakSelf.scheduleModalsUpdate) {
424 // if modals update was requested during setModalViewControllers we set scheduleModalsUpdate
425 // flag in order to perform updates at a later point. Here we are done with all modals
426 // transitions and check this flag again. If it was set, we reset the flag and execute updates.
427 weakSelf.scheduleModalsUpdate = NO;
428 [weakSelf updateContainer];
429 }
430 // we trigger the update of orientation here because, when dismissing the modal from JS,
431 // neither `viewWillAppear` nor `presentationControllerDidDismiss` are called, same for status bar.
432 [RNSScreenWindowTraits updateWindowTraits];
433 };
434
435 void (^finish)(void) = ^{
436 NSUInteger oldCount = weakSelf.presentedModals.count;
437 if (changeRootIndex < oldCount) {
438 [weakSelf.presentedModals removeObjectsInRange:NSMakeRange(changeRootIndex, oldCount - changeRootIndex)];
439 }
440 BOOL isAttached =
441 changeRootController.parentViewController != nil || changeRootController.presentingViewController != nil;
442 if (!isAttached || changeRootIndex >= controllers.count) {
443 // if change controller view is not attached, presenting modals will silently fail on iOS.
444 // In such a case we trigger controllers update from didMoveToWindow.
445 // We also don't run any present transitions if changeRootIndex is greater or equal to the size
446 // of new controllers array. This means that no new controllers should be presented.
447 afterTransitions();
448 return;
449 } else {
450 UIViewController *previous = changeRootController;
451
452 for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
453 UIViewController *next = controllers[i];
454 BOOL lastModal = (i == controllers.count - 1);
455
456#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
457 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
458 if (@available(iOS 13.0, tvOS 13.0, *)) {
459 // Inherit UI style from its parent - solves an issue with incorrect style being applied to some UIKit views
460 // like date picker or segmented control.
461 next.overrideUserInterfaceStyle = self->_controller.overrideUserInterfaceStyle;
462 }
463#endif
464
465 BOOL shouldAnimate = lastModal && [next isKindOfClass:[RNSScreen class]] &&
466 ((RNSScreen *)next).screenView.stackAnimation != RNSScreenStackAnimationNone;
467
468 // if you want to present another modal quick enough after dismissing the previous one,
469 // it will result in wrong changeRootController, see repro in
470 // https://github.com/software-mansion/react-native-screens/issues/1299 We call `updateContainer` again in
471 // `presentationControllerDidDismiss` to cover this case and present new controller
472 if (previous.beingDismissed) {
473 return;
474 }
475
476 [previous presentViewController:next
477 animated:shouldAnimate
478 completion:^{
479 [weakSelf.presentedModals addObject:next];
480 if (lastModal) {
481 afterTransitions();
482 };
483 }];
484 previous = next;
485 }
486 }
487 };
488
489 // changeRootController is the last controller that *is owned by this stack*, and should stay unchanged after this
490 // batch of transitions. Therefore changeRootController.presentedViewController is the first constroller to be
491 // dismissed (implying also all controllers above). Notice here, that firstModalToBeDismissed could have been
492 // RNSScreen modal presented from *this* stack, another stack, or any other view controller with modal presentation
493 // provided by third-party libraries (e.g. React Native's Modal view). In case of presence of other (not managed by
494 // us) modal controllers, weird interactions might arise. The code below, besides handling our presentation /
495 // dismissal logic also attempts to handle possible wide gamut of cases of interactions with third-party modal
496 // controllers, however it's not perfect.
497 // TODO: Find general way to manage owned and foreign modal view controllers and refactor this code. Consider building
498 // model first (data structue, attempting to be aware of all modals in presentation and some text-like algorithm for
499 // computing required operations).
500
501 UIViewController *firstModalToBeDismissed = changeRootController.presentedViewController;
502
503 if (firstModalToBeDismissed != nil) {
504 BOOL shouldAnimate = changeRootIndex == controllers.count &&
505 [firstModalToBeDismissed isKindOfClass:[RNSScreen class]] &&
506 ((RNSScreen *)firstModalToBeDismissed).screenView.stackAnimation != RNSScreenStackAnimationNone;
507
508 if ([_presentedModals containsObject:firstModalToBeDismissed] ||
509 ![firstModalToBeDismissed isKindOfClass:RNSScreen.class]) {
510 // We dismiss every VC that was presented by changeRootController VC or its descendant.
511 // After the series of dismissals is completed we run completion block in which
512 // we present modals on top of changeRootController (which may be the this stack VC)
513 //
514 // There also might the second case, where the firstModalToBeDismissed is foreign.
515 // See: https://github.com/software-mansion/react-native-screens/issues/2048
516 // For now, to mitigate the issue, we also decide to trigger its dismissal before
517 // starting the presentation chain down below in finish() callback.
518 [changeRootController dismissViewControllerAnimated:shouldAnimate completion:finish];
519 return;
520 }
521
522 UIViewController *lastModalVc = [self lastFromPresentedViewControllerChainStartingFrom:firstModalToBeDismissed];
523
524 if (lastModalVc != firstModalToBeDismissed) {
525 [lastModalVc dismissViewControllerAnimated:shouldAnimate completion:finish];
526 return;
527 }
528 }
529
530 // changeRootController does not have presentedViewController but it does not mean that no modals are in presentation;
531 // modals could be presented by another stack (nested / outer), third-party view controller or they could be using
532 // UIModalPresentationCurrentContext / UIModalPresentationOverCurrentContext presentation styles; in the last case
533 // for some reason system asks top-level (react root) vc to present instead of our stack, despite the fact that
534 // `definesPresentationContext` returns `YES` for UINavigationController.
535 // So we first need to find top-level controller manually:
536 UIViewController *reactRootVc = [self findReactRootViewController];
537 UIViewController *topMostVc = [RNSScreenStackView findTopMostPresentedViewControllerFromViewController:reactRootVc];
538
539 if (topMostVc != reactRootVc) {
540 changeRootController = topMostVc;
541
542 // Here we handle just the simplest case where the top level VC was dismissed. In any more complex
543 // scenario we will still have problems, see: https://github.com/software-mansion/react-native-screens/issues/1813
544 if ([_presentedModals containsObject:topMostVc] && ![controllers containsObject:topMostVc]) {
545 [changeRootController dismissViewControllerAnimated:YES completion:finish];
546 return;
547 }
548 }
549
550 // We didn't detect any controllers for dismissal, thus we start presenting new VCs
551 finish();
552}
553
554- (void)setPushViewControllers:(NSArray<UIViewController *> *)controllers
555{
556 // when there is no change we return immediately
557 if ([_controller.viewControllers isEqualToArray:controllers]) {
558 return;
559 }
560
561 // if view controller is not yet attached to window we skip updates now and run them when view
562 // is attached
563 if (self.window == nil) {
564 return;
565 }
566 // when transition is ongoing, any updates made to the controller will not be reflected until the
567 // transition is complete. In particular, when we push/pop view controllers we expect viewControllers
568 // property to be updated immediately. Based on that property we then calculate future updates.
569 // When the transition is ongoing the property won't be updated immediatly. We therefore avoid
570 // making any updated when transition is ongoing and schedule updates for when the transition
571 // is complete.
572 if (_controller.transitionCoordinator != nil) {
573 if (!_updateScheduled) {
574 _updateScheduled = YES;
575 __weak RNSScreenStackView *weakSelf = self;
576 [_controller.transitionCoordinator
577 animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
578 // do nothing here, we only want to be notified when transition is complete
579 }
580 completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
581 self->_updateScheduled = NO;
582 [weakSelf updateContainer];
583 }];
584 }
585 return;
586 }
587
588 UIViewController *top = controllers.lastObject;
589#ifdef RCT_NEW_ARCH_ENABLED
590 UIViewController *previousTop = _controller.topViewController;
591#else
592 UIViewController *previousTop = _controller.viewControllers.lastObject;
593#endif
594
595 // At the start we set viewControllers to contain a single UIViewController
596 // instance. This is a workaround for header height adjustment bug (see comment
597 // in the init function). Here, we need to detect if the initial empty
598 // controller is still there
599 BOOL firstTimePush = ![previousTop isKindOfClass:[RNSScreen class]];
600
601 if (firstTimePush) {
602 // nothing pushed yet
603 [_controller setViewControllers:controllers animated:NO];
604 } else if (top != previousTop) {
605 if (![controllers containsObject:previousTop]) {
606 // if the previous top screen does not exist anymore and the new top was not on the stack before, probably replace
607 // was called, so we check the animation
608 if (![_controller.viewControllers containsObject:top] &&
609 ((RNSScreenView *)top.view).replaceAnimation == RNSScreenReplaceAnimationPush) {
610 // setting new controllers with animation does `push` animation by default
611#ifdef RCT_NEW_ARCH_ENABLED
612 // This is a workaround for the case, when in the app we're trying to do `replace` action on screens, when
613 // there's already ongoing transition to some screen. In such case, we're making the snapshot, but we're trying
614 // to add it to the wrong superview (where it should be UIViewControllerWrapperView, but it's
615 // _UIParallaxDimmingView instead). At the moment of RN 0.74 we can't queue the unmounts for such situation
616 // either, so we need to turn off animations, when the view is not yet mounted, but it will appear after the
617 // transition of previous replacement.
618 [_controller setViewControllers:controllers animated:previousTop.view.window != nil];
619#else
620 [_controller setViewControllers:controllers animated:YES];
621#endif // RCT_NEW_ARCH_ENABLED
622 } else {
623 // last top controller is no longer on stack
624 // in this case we set the controllers stack to the new list with
625 // added the last top element to it and perform (animated) pop
626 NSMutableArray *newControllers = [NSMutableArray arrayWithArray:controllers];
627 [newControllers addObject:previousTop];
628 [_controller setViewControllers:newControllers animated:NO];
629 [_controller popViewControllerAnimated:YES];
630 }
631 } else if (![_controller.viewControllers containsObject:top]) {
632 // new top controller is not on the stack
633 // in such case we update the stack except from the last element with
634 // no animation and do animated push of the last item
635 NSMutableArray *newControllers = [NSMutableArray arrayWithArray:controllers];
636 [newControllers removeLastObject];
637
638 [_controller setViewControllers:newControllers animated:NO];
639 [_controller pushViewController:top animated:YES];
640 } else {
641 // don't really know what this case could be, but may need to handle it
642 // somehow
643 [_controller setViewControllers:controllers animated:NO];
644 }
645 } else {
646 // change wasn't on the top of the stack. We don't need animation.
647 [_controller setViewControllers:controllers animated:NO];
648 }
649}
650
651- (void)updateContainer
652{
653 NSMutableArray<UIViewController *> *pushControllers = [NSMutableArray new];
654 NSMutableArray<UIViewController *> *modalControllers = [NSMutableArray new];
655 for (RNSScreenView *screen in _reactSubviews) {
656 if (!screen.dismissed && screen.controller != nil) {
657 if (pushControllers.count == 0) {
658 // first screen on the list needs to be places as "push controller"
659 [pushControllers addObject:screen.controller];
660 } else {
661 if (screen.stackPresentation == RNSScreenStackPresentationPush) {
662 [pushControllers addObject:screen.controller];
663 } else {
664 [modalControllers addObject:screen.controller];
665 }
666 }
667 }
668 }
669
670 [self setPushViewControllers:pushControllers];
671 [self setModalViewControllers:modalControllers];
672}
673
674- (void)layoutSubviews
675{
676 [super layoutSubviews];
677 _controller.view.frame = self.bounds;
678
679 // We need to update the bounds of the modal views here, since
680 // for contained modals they are not updated by modals themselves.
681 for (UIViewController *modal in _presentedModals) {
682 // Because `layoutSubviews` method is also called on grabbing the modal,
683 // we don't want to update the frame when modal is being dismissed.
684 // We also want to get the frame in correct position. In the best case, it
685 // should be modal's superview (UITransitionView), which frame is being changed correctly.
686 // Otherwise, when superview is nil, we will fallback to the bounds of the ScreenStack.
687 BOOL isModalBeingDismissed = [modal isKindOfClass:[RNSScreen class]] && ((RNSScreen *)modal).isBeingDismissed;
688 CGRect correctFrame = modal.view.superview != nil ? modal.view.superview.frame : self.bounds;
689
690 if (!CGRectEqualToRect(modal.view.frame, correctFrame) && !isModalBeingDismissed) {
691 modal.view.frame = correctFrame;
692 }
693 }
694}
695
696- (void)dismissOnReload
697{
698#ifdef RCT_NEW_ARCH_ENABLED
699#else
700 dispatch_async(dispatch_get_main_queue(), ^{
701 [self invalidate];
702 });
703#endif // RCT_NEW_ARCH_ENABLED
704}
705
706#pragma mark methods connected to transitioning
707
708- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
709 animationControllerForOperation:(UINavigationControllerOperation)operation
710 fromViewController:(UIViewController *)fromVC
711 toViewController:(UIViewController *)toVC
712{
713 RNSScreenView *screen;
714 if (operation == UINavigationControllerOperationPush) {
715 screen = ((RNSScreen *)toVC).screenView;
716 } else if (operation == UINavigationControllerOperationPop) {
717 screen = ((RNSScreen *)fromVC).screenView;
718 }
719 BOOL shouldCancelDismiss = [self shouldCancelDismissFromView:(RNSScreenView *)fromVC.view
720 toView:(RNSScreenView *)toVC.view];
721 if (screen != nil &&
722 // when preventing the native dismiss with back button, we have to return the animator.
723 // Also, we need to return the animator when full width swiping even if the animation is not custom,
724 // otherwise the screen will be just popped immediately due to no animation
725 ((operation == UINavigationControllerOperationPop && shouldCancelDismiss) || _isFullWidthSwiping ||
726 [RNSScreenStackAnimator isCustomAnimation:screen.stackAnimation] || _customAnimation)) {
727 return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
728 }
729 return nil;
730}
731
732- (void)cancelTouchesInParent
733{
734 // cancel touches in parent, this is needed to cancel RN touch events. For example when Touchable
735 // item is close to an edge and we start pulling from edge we want the Touchable to be cancelled.
736 // Without the below code the Touchable will remain active (highlighted) for the duration of back
737 // gesture and onPress may fire when we release the finger.
738#ifdef RCT_NEW_ARCH_ENABLED
739 // On Fabric there is no view that exposes touchHandler above us in the view hierarchy, however it is still
740 // utilised. `RCTSurfaceView` should be present above us, which hosts `RCTFabricSurface` instance, which in turn
741 // hosts `RCTSurfaceTouchHandler` as a private field. When initialised, `RCTSurfaceTouchHandler` is attached to the
742 // surface view as a gestureRecognizer <- and this is where we can lay our hands on it.
743 UIView *parent = _controller.view;
744 while (parent != nil && ![parent isKindOfClass:RCTSurfaceView.class]) {
745 parent = parent.superview;
746 }
747
748 // This could be possible in modal context
749 if (parent == nil) {
750 return;
751 }
752
753 RCTSurfaceTouchHandler *touchHandler = nil;
754 // Experimentation shows that RCTSurfaceTouchHandler is the only gestureRecognizer registered here,
755 // so we should not be afraid of any performance hit here.
756 for (UIGestureRecognizer *recognizer in parent.gestureRecognizers) {
757 if ([recognizer isKindOfClass:RCTSurfaceTouchHandler.class]) {
758 touchHandler = static_cast<RCTSurfaceTouchHandler *>(recognizer);
759 }
760 }
761
762 [touchHandler rnscreens_cancelTouches];
763#else
764 // On Paper we can access touchHandler hosted by `RCTRootContentView` which should be above ScreenStack
765 // in view hierarchy.
766 UIView *parent = _controller.view;
767 while (parent != nil && ![parent respondsToSelector:@selector(touchHandler)]) {
768 parent = parent.superview;
769 }
770 if (parent != nil) {
771 RCTTouchHandler *touchHandler = [parent performSelector:@selector(touchHandler)];
772 [touchHandler rnscreens_cancelTouches];
773 }
774#endif // RCT_NEW_ARCH_ENABLED
775}
776
777- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
778{
779 if (_disableSwipeBack) {
780 return NO;
781 }
782 RNSScreenView *topScreen = _reactSubviews.lastObject;
783
784#if TARGET_OS_TV || TARGET_OS_VISION
785 [self cancelTouchesInParent];
786 return YES;
787#else
788 // RNSPanGestureRecognizer will receive events iff topScreen.fullScreenSwipeEnabled == YES;
789 // Events are filtered in gestureRecognizer:shouldReceivePressOrTouchEvent: method
790 if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
791 if ([self isInGestureResponseDistance:gestureRecognizer topScreen:topScreen]) {
792 _isFullWidthSwiping = YES;
793 [self cancelTouchesInParent];
794 return YES;
795 }
796 return NO;
797 }
798
799 // Now we're dealing with RNSScreenEdgeGestureRecognizer (or _UIParallaxTransitionPanGestureRecognizer)
800 if (topScreen.customAnimationOnSwipe && [RNSScreenStackAnimator isCustomAnimation:topScreen.stackAnimation]) {
801 if ([gestureRecognizer isKindOfClass:[RNSScreenEdgeGestureRecognizer class]]) {
802 UIRectEdge edges = ((RNSScreenEdgeGestureRecognizer *)gestureRecognizer).edges;
803 BOOL isRTL = _controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft;
804 BOOL isSlideFromLeft = topScreen.stackAnimation == RNSScreenStackAnimationSlideFromLeft;
805 // if we do not set any explicit `semanticContentAttribute`, it is `UISemanticContentAttributeUnspecified` instead
806 // of `UISemanticContentAttributeForceLeftToRight`, so we just check if it is RTL or not
807 BOOL isCorrectEdge = (isRTL && edges == UIRectEdgeRight) ||
808 (!isRTL && isSlideFromLeft && edges == UIRectEdgeRight) ||
809 (isRTL && isSlideFromLeft && edges == UIRectEdgeLeft) || (!isRTL && edges == UIRectEdgeLeft);
810 if (isCorrectEdge) {
811 [self cancelTouchesInParent];
812 return YES;
813 }
814 }
815 return NO;
816 } else {
817 if ([gestureRecognizer isKindOfClass:[RNSScreenEdgeGestureRecognizer class]]) {
818 // it should only recognize with `customAnimationOnSwipe` set
819 return NO;
820 }
821 // _UIParallaxTransitionPanGestureRecognizer (other...)
822 [self cancelTouchesInParent];
823 return YES;
824 }
825
826#endif // TARGET_OS_TV
827}
828
829#if !TARGET_OS_TV && !TARGET_OS_VISION
830- (void)setupGestureHandlers
831{
832 // gesture recognizers for custom stack animations
833 RNSScreenEdgeGestureRecognizer *leftEdgeSwipeGestureRecognizer =
834 [[RNSScreenEdgeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
835 leftEdgeSwipeGestureRecognizer.edges = UIRectEdgeLeft;
836 leftEdgeSwipeGestureRecognizer.delegate = self;
837 [self addGestureRecognizer:leftEdgeSwipeGestureRecognizer];
838
839 RNSScreenEdgeGestureRecognizer *rightEdgeSwipeGestureRecognizer =
840 [[RNSScreenEdgeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
841 rightEdgeSwipeGestureRecognizer.edges = UIRectEdgeRight;
842 rightEdgeSwipeGestureRecognizer.delegate = self;
843 [self addGestureRecognizer:rightEdgeSwipeGestureRecognizer];
844
845 // gesture recognizer for full width swipe gesture
846 RNSPanGestureRecognizer *panRecognizer = [[RNSPanGestureRecognizer alloc] initWithTarget:self
847 action:@selector(handleSwipe:)];
848 panRecognizer.delegate = self;
849 [self addGestureRecognizer:panRecognizer];
850}
851
852- (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
853{
854 RNSScreenView *topScreen = _reactSubviews.lastObject;
855 float translation;
856 float velocity;
857 float distance;
858 if (topScreen.swipeDirection == RNSScreenSwipeDirectionVertical) {
859 translation = [gestureRecognizer translationInView:gestureRecognizer.view].y;
860 velocity = [gestureRecognizer velocityInView:gestureRecognizer.view].y;
861 distance = gestureRecognizer.view.bounds.size.height;
862 } else {
863 translation = [gestureRecognizer translationInView:gestureRecognizer.view].x;
864 velocity = [gestureRecognizer velocityInView:gestureRecognizer.view].x;
865 distance = gestureRecognizer.view.bounds.size.width;
866 BOOL isRTL = _controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft;
867 if (isRTL) {
868 translation = -translation;
869 velocity = -velocity;
870 }
871 }
872
873 bool isInverted = topScreen.stackAnimation == RNSScreenStackAnimationSlideFromLeft;
874
875 float transitionProgress = (translation / distance);
876 transitionProgress = isInverted ? transitionProgress * -1 : transitionProgress;
877
878 switch (gestureRecognizer.state) {
879 case UIGestureRecognizerStateBegan: {
880 _interactionController = [UIPercentDrivenInteractiveTransition new];
881 [_controller popViewControllerAnimated:YES];
882 break;
883 }
884
885 case UIGestureRecognizerStateChanged: {
886 [_interactionController updateInteractiveTransition:transitionProgress];
887 break;
888 }
889
890 case UIGestureRecognizerStateCancelled: {
891 [_interactionController cancelInteractiveTransition];
892 break;
893 }
894
895 case UIGestureRecognizerStateEnded: {
896 // values taken from
897 // https://github.com/react-navigation/react-navigation/blob/54739828598d7072c1bf7b369659e3682db3edc5/packages/stack/src/views/Stack/Card.tsx#L316
898 float snapPoint = distance / 2;
899 float gestureDistance = translation + velocity * 0.3;
900 gestureDistance = isInverted ? gestureDistance * -1 : gestureDistance;
901 BOOL shouldFinishTransition = gestureDistance > snapPoint;
902 if (shouldFinishTransition) {
903 [_interactionController finishInteractiveTransition];
904 } else {
905 [_interactionController cancelInteractiveTransition];
906 }
907 _interactionController = nil;
908 }
909 default: {
910 break;
911 }
912 }
913}
914
915- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
916 interactionControllerForAnimationController:
917 (id<UIViewControllerAnimatedTransitioning>)animationController
918{
919 RNSScreenView *fromView = [_controller.transitionCoordinator viewForKey:UITransitionContextFromViewKey];
920 RNSScreenView *toView = [_controller.transitionCoordinator viewForKey:UITransitionContextToViewKey];
921 // we can intercept clicking back button here, we check reactSuperview since this method also fires when
922 // navigating back from JS
923 if (_interactionController == nil && fromView.reactSuperview) {
924 BOOL shouldCancelDismiss = [self shouldCancelDismissFromView:fromView toView:toView];
925 if (shouldCancelDismiss) {
926 _interactionController = [UIPercentDrivenInteractiveTransition new];
927 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
928 [self->_interactionController cancelInteractiveTransition];
929 self->_interactionController = nil;
930 int fromIndex = (int)[self->_reactSubviews indexOfObject:fromView];
931 int toIndex = (int)[self->_reactSubviews indexOfObject:toView];
932 int indexDiff = fromIndex - toIndex;
933 int dismissCount = indexDiff > 0 ? indexDiff : 1;
934 [self updateContainer];
935 [fromView notifyDismissCancelledWithDismissCount:dismissCount];
936 });
937 }
938 }
939 return _interactionController;
940}
941
942- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:
943 (id<UIViewControllerAnimatedTransitioning>)animator
944{
945 return _interactionController;
946}
947
948- (void)navigationController:(UINavigationController *)navigationController
949 didShowViewController:(UIViewController *)viewController
950 animated:(BOOL)animated
951{
952 [self emitOnFinishTransitioningEvent];
953 [RNSScreenWindowTraits updateWindowTraits];
954 // Because of the bug that caused view to have incorrect dimensions while changing the orientation,
955 // we need to signalize that it needs to be layouted.
956 // see https://github.com/software-mansion/react-native-screens/pull/1970
957 [_controller.view setNeedsLayout];
958}
959#endif
960
961- (void)markChildUpdated
962{
963 // do nothing
964}
965
966- (void)didUpdateChildren
967{
968 // do nothing
969}
970
971- (UIViewController *)reactViewController
972{
973 return _controller;
974}
975
976- (BOOL)isInGestureResponseDistance:(UIGestureRecognizer *)gestureRecognizer topScreen:(RNSScreenView *)topScreen
977{
978 NSDictionary *gestureResponseDistanceValues = topScreen.gestureResponseDistance;
979 float x = [gestureRecognizer locationInView:gestureRecognizer.view].x;
980 float y = [gestureRecognizer locationInView:gestureRecognizer.view].y;
981 BOOL isRTL = _controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft;
982 if (isRTL) {
983 x = _controller.view.frame.size.width - x;
984 }
985
986 // see:
987 // https://github.com/software-mansion/react-native-screens/pull/1442/commits/74d4bae321875d8305ad021b3d448ebf713e7d56
988 // this prop is always default initialized so we do not expect any nils
989 float start = [gestureResponseDistanceValues[@"start"] floatValue];
990 float end = [gestureResponseDistanceValues[@"end"] floatValue];
991 float top = [gestureResponseDistanceValues[@"top"] floatValue];
992 float bottom = [gestureResponseDistanceValues[@"bottom"] floatValue];
993
994 // we check if any of the constraints are violated and return NO if so
995 return !(
996 (start != -1 && x < start) || (end != -1 && x > end) || (top != -1 && y < top) || (bottom != -1 && y > bottom));
997}
998
999// By default, the header buttons that are not inside the native hit area
1000// cannot be clicked, so we check it by ourselves
1001- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
1002{
1003 if (CGRectContainsPoint(_controller.navigationBar.frame, point)) {
1004 RNSScreenView *topMostScreen = (RNSScreenView *)_reactSubviews.lastObject;
1005 UIView *headerConfig = topMostScreen.findHeaderConfig;
1006 if ([headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
1007 UIView *headerHitTestResult = [headerConfig hitTest:point withEvent:event];
1008 if (headerHitTestResult != nil) {
1009 return headerHitTestResult;
1010 }
1011 }
1012 }
1013 return [super hitTest:point withEvent:event];
1014}
1015
1016#if !TARGET_OS_TV && !TARGET_OS_VISION
1017
1018- (BOOL)isScrollViewPanGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
1019{
1020 // NOTE: This hack is required to restore native behavior of edge swipe (interactive pop gesture)
1021 // without this, on a screen with a scroll view, it's only possible to pop view by panning horizontally
1022 // if even slightly diagonal (or if in motion), scroll view will scroll, and edge swipe will be cancelled
1023 if (![[gestureRecognizer view] isKindOfClass:[UIScrollView class]]) {
1024 return NO;
1025 }
1026 UIScrollView *scrollView = (UIScrollView *)gestureRecognizer.view;
1027 return scrollView.panGestureRecognizer == gestureRecognizer;
1028}
1029
1030// Custom method for compatibility with iOS < 13.4
1031// RNSScreenStackView is a UIGestureRecognizerDelegate for three types of gesture recognizers:
1032// RNSPanGestureRecognizer, RNSScreenEdgeGestureRecognizer, _UIParallaxTransitionPanGestureRecognizer
1033// Be careful when adding another type of gesture recognizer.
1034- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePressOrTouchEvent:(NSObject *)event
1035{
1036 RNSScreenView *topScreen = _reactSubviews.lastObject;
1037
1038 if (![topScreen isKindOfClass:[RNSScreenView class]] || !topScreen.gestureEnabled ||
1039 _controller.viewControllers.count < 2 || [topScreen isModal]) {
1040 return NO;
1041 }
1042
1043 // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled.
1044 if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
1045 return topScreen.fullScreenSwipeEnabled;
1046 }
1047
1048 // RNSScreenEdgeGestureRecognizer || _UIParallaxTransitionPanGestureRecognizer
1049 return YES;
1050}
1051
1052- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;
1053{
1054 return [self gestureRecognizer:gestureRecognizer shouldReceivePressOrTouchEvent:press];
1055}
1056
1057- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
1058{
1059 return [self gestureRecognizer:gestureRecognizer shouldReceivePressOrTouchEvent:touch];
1060}
1061
1062- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
1063 shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
1064{
1065 if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]] &&
1066 [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]) {
1067 RNSPanGestureRecognizer *panGestureRecognizer = (RNSPanGestureRecognizer *)gestureRecognizer;
1068 BOOL isBackGesture = [panGestureRecognizer translationInView:panGestureRecognizer.view].x > 0 &&
1069 _controller.viewControllers.count > 1;
1070
1071 if (gestureRecognizer.state == UIGestureRecognizerStateBegan || isBackGesture) {
1072 return NO;
1073 }
1074
1075 return YES;
1076 }
1077 return NO;
1078}
1079
1080- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
1081 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
1082{
1083 return (
1084 [gestureRecognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]] &&
1085 [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]);
1086}
1087
1088#endif // !TARGET_OS_TV
1089
1090- (void)insertReactSubview:(RNSScreenView *)subview atIndex:(NSInteger)atIndex
1091{
1092 if (![subview isKindOfClass:[RNSScreenView class]]) {
1093 RCTLogError(@"ScreenStack only accepts children of type Screen");
1094 return;
1095 }
1096 subview.reactSuperview = self;
1097 [_reactSubviews insertObject:subview atIndex:atIndex];
1098}
1099
1100- (void)removeReactSubview:(RNSScreenView *)subview
1101{
1102 subview.reactSuperview = nil;
1103 [_reactSubviews removeObject:subview];
1104}
1105
1106- (void)didUpdateReactSubviews
1107{
1108 // we need to wait until children have their layout set. At this point they don't have the layout
1109 // set yet, however the layout call is already enqueued on ui thread. Enqueuing update call on the
1110 // ui queue will guarantee that the update will run after layout.
1111 dispatch_async(dispatch_get_main_queue(), ^{
1112 self->_hasLayout = YES;
1113 [self maybeAddToParentAndUpdateContainer];
1114 });
1115}
1116
1117- (void)startScreenTransition
1118{
1119 if (_interactionController == nil) {
1120 _customAnimation = YES;
1121 _interactionController = [UIPercentDrivenInteractiveTransition new];
1122 [_controller popViewControllerAnimated:YES];
1123 }
1124}
1125
1126- (void)updateScreenTransition:(double)progress
1127{
1128 [_interactionController updateInteractiveTransition:progress];
1129}
1130
1131- (void)finishScreenTransition:(BOOL)canceled
1132{
1133 _customAnimation = NO;
1134 if (canceled) {
1135 [_interactionController updateInteractiveTransition:0.0];
1136 [_interactionController cancelInteractiveTransition];
1137 } else {
1138 [_interactionController updateInteractiveTransition:1.0];
1139 [_interactionController finishInteractiveTransition];
1140 }
1141 _interactionController = nil;
1142}
1143
1144#ifdef RCT_NEW_ARCH_ENABLED
1145#pragma mark - Fabric specific
1146
1147- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
1148{
1149 if (![childComponentView isKindOfClass:[RNSScreenView class]]) {
1150 RCTLogError(@"ScreenStack only accepts children of type Screen");
1151 return;
1152 }
1153
1154 RCTAssert(
1155 childComponentView.reactSuperview == nil,
1156 @"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)",
1157 self,
1158 childComponentView,
1159 @(index),
1160 @([childComponentView.superview tag]));
1161
1162 [_reactSubviews insertObject:(RNSScreenView *)childComponentView atIndex:index];
1163 ((RNSScreenView *)childComponentView).reactSuperview = self;
1164 dispatch_async(dispatch_get_main_queue(), ^{
1165 [self maybeAddToParentAndUpdateContainer];
1166 });
1167}
1168
1169- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
1170{
1171 RNSScreenView *screenChildComponent = (RNSScreenView *)childComponentView;
1172 // We should only do a snapshot of a screen that is on the top.
1173 // We also check `_presentedModals` since if you push 2 modals, second one is not a "child" of _controller.
1174 // Also, when dissmised with a gesture, the screen already is not under the window, so we don't need to apply
1175 // snapshot.
1176 if (screenChildComponent.window != nil &&
1177 ((screenChildComponent == _controller.visibleViewController.view && _presentedModals.count < 2) ||
1178 screenChildComponent == [_presentedModals.lastObject view])) {
1179 [screenChildComponent.controller setViewToSnapshot:_snapshot];
1180 }
1181
1182 RCTAssert(
1183 screenChildComponent.reactSuperview == self,
1184 @"Attempt to unmount a view which is mounted inside different view. (parent: %@, child: %@, index: %@)",
1185 self,
1186 screenChildComponent,
1187 @(index));
1188 RCTAssert(
1189 (_reactSubviews.count > index) && [_reactSubviews objectAtIndex:index] == childComponentView,
1190 @"Attempt to unmount a view which has a different index. (parent: %@, child: %@, index: %@, actual index: %@, tag at index: %@)",
1191 self,
1192 screenChildComponent,
1193 @(index),
1194 @([_reactSubviews indexOfObject:screenChildComponent]),
1195 @([[_reactSubviews objectAtIndex:index] tag]));
1196 screenChildComponent.reactSuperview = nil;
1197 [_reactSubviews removeObject:screenChildComponent];
1198 [screenChildComponent removeFromSuperview];
1199 dispatch_async(dispatch_get_main_queue(), ^{
1200 [self maybeAddToParentAndUpdateContainer];
1201 });
1202}
1203
1204- (void)takeSnapshot
1205{
1206 if (_presentedModals.count < 2) {
1207 _snapshot = [_controller.visibleViewController.view snapshotViewAfterScreenUpdates:NO];
1208 } else {
1209 _snapshot = [[_presentedModals.lastObject view] snapshotViewAfterScreenUpdates:NO];
1210 }
1211}
1212
1213- (void)mountingTransactionWillMount:(react::MountingTransaction const &)transaction
1214 withSurfaceTelemetry:(react::SurfaceTelemetry const &)surfaceTelemetry
1215{
1216 for (auto &mutation : transaction.getMutations()) {
1217 if (mutation.type == react::ShadowViewMutation::Type::Remove && mutation.parentShadowView.componentName != nil &&
1218 strcmp(mutation.parentShadowView.componentName, "RNSScreenStack") == 0) {
1219 [self takeSnapshot];
1220 return;
1221 }
1222 }
1223}
1224
1225- (void)prepareForRecycle
1226{
1227 [super prepareForRecycle];
1228 _reactSubviews = [NSMutableArray new];
1229
1230 for (UIViewController *controller in _presentedModals) {
1231 [controller dismissViewControllerAnimated:NO completion:nil];
1232 }
1233
1234 [_presentedModals removeAllObjects];
1235 [_controller willMoveToParentViewController:nil];
1236 [_controller removeFromParentViewController];
1237 [_controller setViewControllers:@[ [UIViewController new] ]];
1238}
1239
1240+ (react::ComponentDescriptorProvider)componentDescriptorProvider
1241{
1242 return react::concreteComponentDescriptorProvider<react::RNSScreenStackComponentDescriptor>();
1243}
1244#else
1245#pragma mark - Paper specific
1246
1247- (void)invalidate
1248{
1249 _invalidated = YES;
1250 for (UIViewController *controller in _presentedModals) {
1251 [controller dismissViewControllerAnimated:NO completion:nil];
1252 }
1253 [_presentedModals removeAllObjects];
1254 [_controller willMoveToParentViewController:nil];
1255 [_controller removeFromParentViewController];
1256}
1257
1258#endif // RCT_NEW_ARCH_ENABLED
1259
1260@end
1261
1262#ifdef RCT_NEW_ARCH_ENABLED
1263Class<RCTComponentViewProtocol> RNSScreenStackCls(void)
1264{
1265 return RNSScreenStackView.class;
1266}
1267#endif
1268
1269@implementation RNSScreenStackManager {
1270 NSPointerArray *_stacks;
1271}
1272
1273RCT_EXPORT_MODULE()
1274
1275RCT_EXPORT_VIEW_PROPERTY(onFinishTransitioning, RCTDirectEventBlock);
1276
1277#ifdef RCT_NEW_ARCH_ENABLED
1278#else
1279- (UIView *)view
1280{
1281 RNSScreenStackView *view = [[RNSScreenStackView alloc] initWithManager:self];
1282 if (!_stacks) {
1283 _stacks = [NSPointerArray weakObjectsPointerArray];
1284 }
1285 [_stacks addPointer:(__bridge void *)view];
1286 return view;
1287}
1288#endif // RCT_NEW_ARCH_ENABLED
1289
1290- (void)invalidate
1291{
1292 for (RNSScreenStackView *stack in _stacks) {
1293 [stack dismissOnReload];
1294 }
1295 _stacks = nil;
1296}
1297
1298@end