56.7 kBPlain TextView Raw
1#import <UIKit/UIKit.h>
2
3#import "RNSScreen.h"
4#import "RNSScreenContainer.h"
5#import "RNSScreenWindowTraits.h"
6
7#ifdef RCT_NEW_ARCH_ENABLED
8#import <React/RCTConversions.h>
9#import <React/RCTFabricComponentsPlugins.h>
10#import <React/RCTRootComponentView.h>
11#import <React/RCTSurfaceTouchHandler.h>
12#import <react/renderer/components/rnscreens/EventEmitters.h>
13#import <react/renderer/components/rnscreens/Props.h>
14#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
15#import <rnscreens/RNSScreenComponentDescriptor.h>
16#import "RNSConvert.h"
17#import "RNSHeaderHeightChangeEvent.h"
18#import "RNSScreenViewEvent.h"
19#else
20#import <React/RCTTouchHandler.h>
21#endif
22
23#import <React/RCTShadowView.h>
24#import <React/RCTUIManager.h>
25#import "RNSScreenStack.h"
26#import "RNSScreenStackHeaderConfig.h"
27
28#ifdef RCT_NEW_ARCH_ENABLED
29namespace react = facebook::react;
30#endif // RCT_NEW_ARCH_ENABLED
31
32@interface RNSScreenView ()
33#ifdef RCT_NEW_ARCH_ENABLED
34 <RCTRNSScreenViewProtocol, UIAdaptivePresentationControllerDelegate>
35#else
36 <UIAdaptivePresentationControllerDelegate, RCTInvalidating>
37#endif
38@end
39
40@implementation RNSScreenView {
41#ifdef RCT_NEW_ARCH_ENABLED
42 RCTSurfaceTouchHandler *_touchHandler;
43 react::RNSScreenShadowNode::ConcreteState::Shared _state;
44 // on fabric, they are not available by default so we need them exposed here too
45 NSMutableArray<UIView *> *_reactSubviews;
46#else
47 __weak RCTBridge *_bridge;
48 RCTTouchHandler *_touchHandler;
49 CGRect _reactFrame;
50#endif
51}
52
53#ifdef RCT_NEW_ARCH_ENABLED
54- (instancetype)initWithFrame:(CGRect)frame
55{
56 if (self = [super initWithFrame:frame]) {
57 static const auto defaultProps = std::make_shared<const react::RNSScreenProps>();
58 _props = defaultProps;
59 _reactSubviews = [NSMutableArray new];
60 [self initCommonProps];
61 }
62 return self;
63}
64#else
65- (instancetype)initWithBridge:(RCTBridge *)bridge
66{
67 if (self = [super init]) {
68 _bridge = bridge;
69 [self initCommonProps];
70 }
71
72 return self;
73}
74#endif // RCT_NEW_ARCH_ENABLED
75
76- (void)initCommonProps
77{
78 _controller = [[RNSScreen alloc] initWithView:self];
79 _stackPresentation = RNSScreenStackPresentationPush;
80 _stackAnimation = RNSScreenStackAnimationDefault;
81 _gestureEnabled = YES;
82 _replaceAnimation = RNSScreenReplaceAnimationPop;
83 _dismissed = NO;
84 _hasStatusBarStyleSet = NO;
85 _hasStatusBarAnimationSet = NO;
86 _hasStatusBarHiddenSet = NO;
87 _hasOrientationSet = NO;
88 _hasHomeIndicatorHiddenSet = NO;
89#if !TARGET_OS_TV
90 _sheetExpandsWhenScrolledToEdge = YES;
91#endif // !TARGET_OS_TV
92}
93
94- (UIViewController *)reactViewController
95{
96 return _controller;
97}
98
99#ifdef RCT_NEW_ARCH_ENABLED
100- (NSArray<UIView *> *)reactSubviews
101{
102 return _reactSubviews;
103}
104#endif
105
106- (void)updateBounds
107{
108#ifdef RCT_NEW_ARCH_ENABLED
109 if (_state != nullptr) {
110 RNSScreenStackHeaderConfig *config = [self findHeaderConfig];
111 // in large title, ScrollView handles the offset of content so we cannot set it here also.
112 CGFloat headerHeight =
113 config.largeTitle ? 0 : [_controller calculateHeaderHeightIsModal:self.isPresentedAsNativeModal];
114 auto newState =
115 react::RNSScreenState{RCTSizeFromCGSize(self.bounds.size), RCTPointFromCGPoint(CGPointMake(0, headerHeight))};
116 _state->updateState(std::move(newState));
117 UINavigationController *navctr = _controller.navigationController;
118 [navctr.view setNeedsLayout];
119 }
120#else
121 [_bridge.uiManager setSize:self.bounds.size forView:self];
122#endif
123}
124
125- (void)setStackPresentation:(RNSScreenStackPresentation)stackPresentation
126{
127 switch (stackPresentation) {
128 case RNSScreenStackPresentationModal:
129#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
130 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
131 if (@available(iOS 13.0, tvOS 13.0, *)) {
132 _controller.modalPresentationStyle = UIModalPresentationAutomatic;
133 } else {
134 _controller.modalPresentationStyle = UIModalPresentationFullScreen;
135 }
136#else
137 _controller.modalPresentationStyle = UIModalPresentationFullScreen;
138#endif
139 break;
140 case RNSScreenStackPresentationFullScreenModal:
141 _controller.modalPresentationStyle = UIModalPresentationFullScreen;
142 break;
143#if !TARGET_OS_TV
144 case RNSScreenStackPresentationFormSheet:
145 _controller.modalPresentationStyle = UIModalPresentationFormSheet;
146 break;
147#endif
148 case RNSScreenStackPresentationTransparentModal:
149 _controller.modalPresentationStyle = UIModalPresentationOverFullScreen;
150 break;
151 case RNSScreenStackPresentationContainedModal:
152 _controller.modalPresentationStyle = UIModalPresentationCurrentContext;
153 break;
154 case RNSScreenStackPresentationContainedTransparentModal:
155 _controller.modalPresentationStyle = UIModalPresentationOverCurrentContext;
156 break;
157 case RNSScreenStackPresentationPush:
158 // ignored, we only need to keep in mind not to set presentation delegate
159 break;
160 }
161
162 // There is a bug in UIKit which causes retain loop when presentationController is accessed for a
163 // controller that is not going to be presented modally. We therefore need to avoid setting the
164 // delegate for screens presented using push. This also means that when controller is updated from
165 // modal to push type, this may cause memory leak, we warn about that as well.
166 if (stackPresentation != RNSScreenStackPresentationPush) {
167 // `modalPresentationStyle` must be set before accessing `presentationController`
168 // otherwise a default controller will be created and cannot be changed after.
169 // Documented here:
170 // https://developer.apple.com/documentation/uikit/uiviewcontroller/1621426-presentationcontroller?language=objc
171 _controller.presentationController.delegate = self;
172 } else if (_stackPresentation != RNSScreenStackPresentationPush) {
173#ifdef RCT_NEW_ARCH_ENABLED
174#else
175 RCTLogError(
176 @"Screen presentation updated from modal to push, this may likely result in a screen object leakage. If you need to change presentation style create a new screen object instead");
177#endif // RCT_NEW_ARCH_ENABLED
178 }
179 _stackPresentation = stackPresentation;
180}
181
182- (void)setStackAnimation:(RNSScreenStackAnimation)stackAnimation
183{
184 _stackAnimation = stackAnimation;
185
186 switch (stackAnimation) {
187 case RNSScreenStackAnimationFade:
188 _controller.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
189 break;
190#if !TARGET_OS_TV
191 case RNSScreenStackAnimationFlip:
192 _controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
193 break;
194#endif
195 case RNSScreenStackAnimationNone:
196 case RNSScreenStackAnimationDefault:
197 case RNSScreenStackAnimationSimplePush:
198 case RNSScreenStackAnimationSlideFromBottom:
199 case RNSScreenStackAnimationFadeFromBottom:
200 case RNSScreenStackAnimationSlideFromLeft:
201 // Default
202 break;
203 }
204}
205
206- (void)setGestureEnabled:(BOOL)gestureEnabled
207{
208#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
209 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
210 if (@available(iOS 13.0, tvOS 13.0, *)) {
211 _controller.modalInPresentation = !gestureEnabled;
212 }
213#endif
214
215 _gestureEnabled = gestureEnabled;
216}
217
218- (void)setReplaceAnimation:(RNSScreenReplaceAnimation)replaceAnimation
219{
220 _replaceAnimation = replaceAnimation;
221}
222
223// Nil will be provided when activityState is set as an animated value and we change
224// it from JS to be a plain value (non animated).
225// In case when nil is received, we want to ignore such value and not make
226// any updates as the actual non-nil value will follow immediately.
227- (void)setActivityStateOrNil:(NSNumber *)activityStateOrNil
228{
229 int activityState = [activityStateOrNil intValue];
230 if (activityStateOrNil != nil && activityState != -1 && activityState != _activityState) {
231 _activityState = activityState;
232 [_reactSuperview markChildUpdated];
233 }
234}
235
236#if !TARGET_OS_TV && !TARGET_OS_VISION
237- (void)setStatusBarStyle:(RNSStatusBarStyle)statusBarStyle
238{
239 _hasStatusBarStyleSet = YES;
240 _statusBarStyle = statusBarStyle;
241 [RNSScreenWindowTraits assertViewControllerBasedStatusBarAppearenceSet];
242 [RNSScreenWindowTraits updateStatusBarAppearance];
243}
244
245- (void)setStatusBarAnimation:(UIStatusBarAnimation)statusBarAnimation
246{
247 _hasStatusBarAnimationSet = YES;
248 _statusBarAnimation = statusBarAnimation;
249 [RNSScreenWindowTraits assertViewControllerBasedStatusBarAppearenceSet];
250}
251
252- (void)setStatusBarHidden:(BOOL)statusBarHidden
253{
254 _hasStatusBarHiddenSet = YES;
255 _statusBarHidden = statusBarHidden;
256 [RNSScreenWindowTraits assertViewControllerBasedStatusBarAppearenceSet];
257 [RNSScreenWindowTraits updateStatusBarAppearance];
258
259 // As the status bar could change its visibility, we need to calculate header
260 // height for the correct value in `onHeaderHeightChange` event when navigation
261 // bar is not visible.
262 if (self.controller.navigationController.navigationBarHidden && !self.isModal) {
263 [self.controller calculateAndNotifyHeaderHeightChangeIsModal:NO];
264 }
265}
266
267- (void)setScreenOrientation:(UIInterfaceOrientationMask)screenOrientation
268{
269 _hasOrientationSet = YES;
270 _screenOrientation = screenOrientation;
271 [RNSScreenWindowTraits enforceDesiredDeviceOrientation];
272}
273
274- (void)setHomeIndicatorHidden:(BOOL)homeIndicatorHidden
275{
276 _hasHomeIndicatorHiddenSet = YES;
277 _homeIndicatorHidden = homeIndicatorHidden;
278 [RNSScreenWindowTraits updateHomeIndicatorAutoHidden];
279}
280#endif
281
282- (UIView *)reactSuperview
283{
284 return _reactSuperview;
285}
286
287- (void)addSubview:(UIView *)view
288{
289 if (![view isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
290 [super addSubview:view];
291 } else {
292 ((RNSScreenStackHeaderConfig *)view).screenView = self;
293 }
294}
295
296- (void)notifyDismissedWithCount:(int)dismissCount
297{
298#ifdef RCT_NEW_ARCH_ENABLED
299 // If screen is already unmounted then there will be no event emitter
300 if (_eventEmitter != nullptr) {
301 std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
302 ->onDismissed(react::RNSScreenEventEmitter::OnDismissed{.dismissCount = dismissCount});
303 }
304#else
305 // TODO: hopefully problems connected to dismissed prop are only the case on paper
306 _dismissed = YES;
307 if (self.onDismissed) {
308 dispatch_async(dispatch_get_main_queue(), ^{
309 if (self.onDismissed) {
310 self.onDismissed(@{@"dismissCount" : @(dismissCount)});
311 }
312 });
313 }
314#endif
315}
316
317- (void)notifyDismissCancelledWithDismissCount:(int)dismissCount
318{
319#ifdef RCT_NEW_ARCH_ENABLED
320 // If screen is already unmounted then there will be no event emitter
321 if (_eventEmitter != nullptr) {
322 std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
323 ->onNativeDismissCancelled(
324 react::RNSScreenEventEmitter::OnNativeDismissCancelled{.dismissCount = dismissCount});
325 }
326#else
327 if (self.onNativeDismissCancelled) {
328 self.onNativeDismissCancelled(@{@"dismissCount" : @(dismissCount)});
329 }
330#endif
331}
332
333- (void)notifyWillAppear
334{
335#ifdef RCT_NEW_ARCH_ENABLED
336 // If screen is already unmounted then there will be no event emitter
337 if (_eventEmitter != nullptr) {
338 std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
339 ->onWillAppear(react::RNSScreenEventEmitter::OnWillAppear{});
340 }
341 [self updateLayoutMetrics:_newLayoutMetrics oldLayoutMetrics:_oldLayoutMetrics];
342#else
343 if (self.onWillAppear) {
344 self.onWillAppear(nil);
345 }
346 // we do it here too because at this moment the `parentViewController` is already not nil,
347 // so if the parent is not UINavCtr, the frame will be updated to the correct one.
348 [self reactSetFrame:_reactFrame];
349#endif
350}
351
352- (void)notifyWillDisappear
353{
354 if (_hideKeyboardOnSwipe) {
355 [self endEditing:YES];
356 }
357#ifdef RCT_NEW_ARCH_ENABLED
358 // If screen is already unmounted then there will be no event emitter
359 if (_eventEmitter != nullptr) {
360 std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
361 ->onWillDisappear(react::RNSScreenEventEmitter::OnWillDisappear{});
362 }
363#else
364 if (self.onWillDisappear) {
365 self.onWillDisappear(nil);
366 }
367#endif
368}
369
370- (void)notifyAppear
371{
372#ifdef RCT_NEW_ARCH_ENABLED
373 // If screen is already unmounted then there will be no event emitter
374 if (_eventEmitter != nullptr) {
375 std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
376 ->onAppear(react::RNSScreenEventEmitter::OnAppear{});
377 }
378#else
379 if (self.onAppear) {
380 dispatch_async(dispatch_get_main_queue(), ^{
381 if (self.onAppear) {
382 self.onAppear(nil);
383 }
384 });
385 }
386#endif
387}
388
389- (void)notifyDisappear
390{
391#ifdef RCT_NEW_ARCH_ENABLED
392 // If screen is already unmounted then there will be no event emitter
393 if (_eventEmitter != nullptr) {
394 std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
395 ->onDisappear(react::RNSScreenEventEmitter::OnDisappear{});
396 }
397#else
398 if (self.onDisappear) {
399 self.onDisappear(nil);
400 }
401#endif
402}
403
404- (void)notifyHeaderHeightChange:(double)headerHeight
405{
406#ifdef RCT_NEW_ARCH_ENABLED
407 if (_eventEmitter != nullptr) {
408 std::dynamic_pointer_cast<const facebook::react::RNSScreenEventEmitter>(_eventEmitter)
409 ->onHeaderHeightChange(
410 facebook::react::RNSScreenEventEmitter::OnHeaderHeightChange{.headerHeight = headerHeight});
411 }
412
413 RNSHeaderHeightChangeEvent *event =
414 [[RNSHeaderHeightChangeEvent alloc] initWithEventName:@"onHeaderHeightChange"
415 reactTag:[NSNumber numberWithInt:self.tag]
416 headerHeight:headerHeight];
417 [[RCTBridge currentBridge].eventDispatcher notifyObserversOfEvent:event];
418#else
419 if (self.onHeaderHeightChange) {
420 self.onHeaderHeightChange(@{
421 @"headerHeight" : @(headerHeight),
422 });
423 }
424#endif
425}
426
427- (void)notifyGestureCancel
428{
429#ifdef RCT_NEW_ARCH_ENABLED
430 if (_eventEmitter != nullptr) {
431 std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
432 ->onGestureCancel(react::RNSScreenEventEmitter::OnGestureCancel{});
433 }
434#else
435 if (self.onGestureCancel) {
436 self.onGestureCancel(nil);
437 }
438#endif
439}
440
441- (BOOL)isMountedUnderScreenOrReactRoot
442{
443#ifdef RCT_NEW_ARCH_ENABLED
444#define RNS_EXPECTED_VIEW RCTRootComponentView
445#else
446#define RNS_EXPECTED_VIEW RCTRootView
447#endif
448 for (UIView *parent = self.superview; parent != nil; parent = parent.superview) {
449 if ([parent isKindOfClass:[RNS_EXPECTED_VIEW class]] || [parent isKindOfClass:[RNSScreenView class]]) {
450 return YES;
451 }
452 }
453 return NO;
454#undef RNS_EXPECTED_VIEW
455}
456
457- (void)didMoveToWindow
458{
459 // For RN touches to work we need to instantiate and connect RCTTouchHandler. This only applies
460 // for screens that aren't mounted under RCTRootView e.g., modals that are mounted directly to
461 // root application window.
462 if (self.window != nil && ![self isMountedUnderScreenOrReactRoot]) {
463 if (_touchHandler == nil) {
464#ifdef RCT_NEW_ARCH_ENABLED
465 _touchHandler = [RCTSurfaceTouchHandler new];
466#else
467 _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
468#endif
469 }
470 [_touchHandler attachToView:self];
471 } else {
472 [_touchHandler detachFromView:self];
473 }
474}
475
476#ifdef RCT_NEW_ARCH_ENABLED
477- (RCTSurfaceTouchHandler *)touchHandler
478#else
479- (RCTTouchHandler *)touchHandler
480#endif
481{
482 if (_touchHandler != nil) {
483 return _touchHandler;
484 }
485 UIView *parent = [self superview];
486 while (parent != nil && ![parent respondsToSelector:@selector(touchHandler)])
487 parent = parent.superview;
488 if (parent != nil) {
489 return [parent performSelector:@selector(touchHandler)];
490 }
491 return nil;
492}
493
494- (void)notifyFinishTransitioning
495{
496 [_controller notifyFinishTransitioning];
497}
498
499- (void)notifyTransitionProgress:(double)progress closing:(BOOL)closing goingForward:(BOOL)goingForward
500{
501#ifdef RCT_NEW_ARCH_ENABLED
502 if (_eventEmitter != nullptr) {
503 std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
504 ->onTransitionProgress(react::RNSScreenEventEmitter::OnTransitionProgress{
505 .progress = progress, .closing = closing ? 1 : 0, .goingForward = goingForward ? 1 : 0});
506 }
507 RNSScreenViewEvent *event = [[RNSScreenViewEvent alloc] initWithEventName:@"onTransitionProgress"
508 reactTag:[NSNumber numberWithInt:self.tag]
509 progress:progress
510 closing:closing
511 goingForward:goingForward];
512 [[RCTBridge currentBridge].eventDispatcher notifyObserversOfEvent:event];
513#else
514 if (self.onTransitionProgress) {
515 self.onTransitionProgress(@{
516 @"progress" : @(progress),
517 @"closing" : @(closing ? 1 : 0),
518 @"goingForward" : @(goingForward ? 1 : 0),
519 });
520 }
521#endif
522}
523
524#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
525 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
526- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController
527{
528 [self notifyGestureCancel];
529}
530#endif
531
532- (void)presentationControllerWillDismiss:(UIPresentationController *)presentationController
533{
534 // We need to call both "cancel" and "reset" here because RN's gesture recognizer
535 // does not handle the scenario when it gets cancelled by other top
536 // level gesture recognizer. In this case by the modal dismiss gesture.
537 // Because of that, at the moment when this method gets called the React's
538 // gesture recognizer is already in FAILED state but cancel events never gets
539 // send to JS. Calling "reset" forces RCTTouchHanler to dispatch cancel event.
540 // To test this behavior one need to open a dismissable modal and start
541 // pulling down starting at some touchable item. Without "reset" the touchable
542 // will never go back from highlighted state even when the modal start sliding
543 // down.
544#ifdef RCT_NEW_ARCH_ENABLED
545 [_touchHandler setEnabled:NO];
546 [_touchHandler setEnabled:YES];
547#else
548 [_touchHandler cancel];
549#endif
550 [_touchHandler reset];
551}
552
553- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
554{
555 if (_preventNativeDismiss) {
556 [self notifyDismissCancelledWithDismissCount:1];
557 return NO;
558 }
559 return _gestureEnabled;
560}
561
562- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
563{
564 if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
565 [_reactSuperview performSelector:@selector(presentationControllerDidDismiss:) withObject:presentationController];
566 }
567}
568
569- (nullable RNSScreenStackHeaderConfig *)findHeaderConfig
570{
571 // Fast path
572 if ([self.reactSubviews.lastObject isKindOfClass:RNSScreenStackHeaderConfig.class]) {
573 return (RNSScreenStackHeaderConfig *)self.reactSubviews.lastObject;
574 }
575
576 for (UIView *view in self.reactSubviews) {
577 if ([view isKindOfClass:RNSScreenStackHeaderConfig.class]) {
578 return (RNSScreenStackHeaderConfig *)view;
579 }
580 }
581
582 return nil;
583}
584
585- (BOOL)isModal
586{
587 return self.stackPresentation != RNSScreenStackPresentationPush;
588}
589
590- (BOOL)isPresentedAsNativeModal
591{
592 return self.controller.parentViewController == nil && self.controller.presentingViewController != nil;
593}
594
595- (BOOL)isFullscreenModal
596{
597 switch (self.controller.modalPresentationStyle) {
598 case UIModalPresentationFullScreen:
599 case UIModalPresentationCurrentContext:
600 case UIModalPresentationOverCurrentContext:
601 return YES;
602 default:
603 return NO;
604 }
605}
606
607- (BOOL)isTransparentModal
608{
609 return self.controller.modalPresentationStyle == UIModalPresentationOverFullScreen ||
610 self.controller.modalPresentationStyle == UIModalPresentationOverCurrentContext;
611}
612
613#if !TARGET_OS_TV && !TARGET_OS_VISION
614/**
615 * Updates settings for sheet presentation controller.
616 * Note that this method should not be called inside `stackPresentation` setter, because on Paper we don't have
617 * guarantee that values of all related props had been updated earlier.
618 */
619- (void)updatePresentationStyle
620{
621#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_15_0) && \
622 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_15_0
623 if (@available(iOS 15.0, *)) {
624 UISheetPresentationController *sheet = _controller.sheetPresentationController;
625 if (_stackPresentation == RNSScreenStackPresentationFormSheet && sheet != nil) {
626 sheet.prefersScrollingExpandsWhenScrolledToEdge = _sheetExpandsWhenScrolledToEdge;
627 sheet.prefersGrabberVisible = _sheetGrabberVisible;
628 sheet.preferredCornerRadius =
629 _sheetCornerRadius < 0 ? UISheetPresentationControllerAutomaticDimension : _sheetCornerRadius;
630
631 if (_sheetLargestUndimmedDetent == RNSScreenDetentTypeMedium) {
632 sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierMedium;
633 } else if (_sheetLargestUndimmedDetent == RNSScreenDetentTypeLarge) {
634 sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
635 } else if (_sheetLargestUndimmedDetent == RNSScreenDetentTypeAll) {
636 sheet.largestUndimmedDetentIdentifier = nil;
637 } else {
638 RCTLogError(@"Unhandled value of sheetLargestUndimmedDetent passed");
639 }
640
641 if (_sheetAllowedDetents == RNSScreenDetentTypeMedium) {
642 sheet.detents = @[ UISheetPresentationControllerDetent.mediumDetent ];
643 if (sheet.selectedDetentIdentifier != UISheetPresentationControllerDetentIdentifierMedium) {
644 [sheet animateChanges:^{
645 sheet.selectedDetentIdentifier = UISheetPresentationControllerDetentIdentifierMedium;
646 }];
647 }
648 } else if (_sheetAllowedDetents == RNSScreenDetentTypeLarge) {
649 sheet.detents = @[ UISheetPresentationControllerDetent.largeDetent ];
650 if (sheet.selectedDetentIdentifier != UISheetPresentationControllerDetentIdentifierLarge) {
651 [sheet animateChanges:^{
652 sheet.selectedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
653 }];
654 }
655 } else if (_sheetAllowedDetents == RNSScreenDetentTypeAll) {
656 sheet.detents =
657 @[ UISheetPresentationControllerDetent.mediumDetent, UISheetPresentationControllerDetent.largeDetent ];
658 } else {
659 RCTLogError(@"Unhandled value of sheetAllowedDetents passed");
660 }
661 }
662 }
663#endif // Check for max allowed iOS version
664}
665#endif // !TARGET_OS_TV
666
667#pragma mark - Fabric specific
668#ifdef RCT_NEW_ARCH_ENABLED
669
670- (BOOL)hasHeaderConfig
671{
672 return _config != nil;
673}
674
675+ (react::ComponentDescriptorProvider)componentDescriptorProvider
676{
677 return react::concreteComponentDescriptorProvider<react::RNSScreenComponentDescriptor>();
678}
679
680+ (BOOL)shouldBeRecycled
681{
682 return NO;
683}
684
685- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
686{
687 if ([childComponentView isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
688 _config = (RNSScreenStackHeaderConfig *)childComponentView;
689 _config.screenView = self;
690 }
691 [_reactSubviews insertObject:childComponentView atIndex:index];
692 [super mountChildComponentView:childComponentView index:index];
693}
694
695- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
696{
697 if ([childComponentView isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
698 _config = nil;
699 }
700 [_reactSubviews removeObject:childComponentView];
701 [super unmountChildComponentView:childComponentView index:index];
702}
703
704#pragma mark - RCTComponentViewProtocol
705
706- (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::Shared const &)oldProps
707{
708 const auto &oldScreenProps = *std::static_pointer_cast<const react::RNSScreenProps>(_props);
709 const auto &newScreenProps = *std::static_pointer_cast<const react::RNSScreenProps>(props);
710
711 [self setFullScreenSwipeEnabled:newScreenProps.fullScreenSwipeEnabled];
712
713 [self setGestureEnabled:newScreenProps.gestureEnabled];
714
715 [self setTransitionDuration:[NSNumber numberWithInt:newScreenProps.transitionDuration]];
716
717 [self setHideKeyboardOnSwipe:newScreenProps.hideKeyboardOnSwipe];
718
719 [self setCustomAnimationOnSwipe:newScreenProps.customAnimationOnSwipe];
720
721 [self
722 setGestureResponseDistance:[RNSConvert
723 gestureResponseDistanceDictFromCppStruct:newScreenProps.gestureResponseDistance]];
724
725 [self setPreventNativeDismiss:newScreenProps.preventNativeDismiss];
726
727 [self setActivityStateOrNil:[NSNumber numberWithFloat:newScreenProps.activityState]];
728
729 [self setSwipeDirection:[RNSConvert RNSScreenSwipeDirectionFromCppEquivalent:newScreenProps.swipeDirection]];
730
731#if !TARGET_OS_TV
732 if (newScreenProps.statusBarHidden != oldScreenProps.statusBarHidden) {
733 [self setStatusBarHidden:newScreenProps.statusBarHidden];
734 }
735
736 if (newScreenProps.statusBarStyle != oldScreenProps.statusBarStyle) {
737 [self setStatusBarStyle:[RCTConvert
738 RNSStatusBarStyle:RCTNSStringFromStringNilIfEmpty(newScreenProps.statusBarStyle)]];
739 }
740
741 if (newScreenProps.statusBarAnimation != oldScreenProps.statusBarAnimation) {
742 [self setStatusBarAnimation:[RCTConvert UIStatusBarAnimation:RCTNSStringFromStringNilIfEmpty(
743 newScreenProps.statusBarAnimation)]];
744 }
745
746 if (newScreenProps.screenOrientation != oldScreenProps.screenOrientation) {
747 [self setScreenOrientation:[RCTConvert UIInterfaceOrientationMask:RCTNSStringFromStringNilIfEmpty(
748 newScreenProps.screenOrientation)]];
749 }
750
751 if (newScreenProps.homeIndicatorHidden != oldScreenProps.homeIndicatorHidden) {
752 [self setHomeIndicatorHidden:newScreenProps.homeIndicatorHidden];
753 }
754
755 [self setSheetGrabberVisible:newScreenProps.sheetGrabberVisible];
756 [self setSheetCornerRadius:newScreenProps.sheetCornerRadius];
757 [self setSheetExpandsWhenScrolledToEdge:newScreenProps.sheetExpandsWhenScrolledToEdge];
758
759 if (newScreenProps.sheetAllowedDetents != oldScreenProps.sheetAllowedDetents) {
760 [self setSheetAllowedDetents:[RNSConvert RNSScreenDetentTypeFromAllowedDetents:newScreenProps.sheetAllowedDetents]];
761 }
762
763 if (newScreenProps.sheetLargestUndimmedDetent != oldScreenProps.sheetLargestUndimmedDetent) {
764 [self setSheetLargestUndimmedDetent:
765 [RNSConvert RNSScreenDetentTypeFromLargestUndimmedDetent:newScreenProps.sheetLargestUndimmedDetent]];
766 }
767#endif // !TARGET_OS_TV
768
769 if (newScreenProps.stackPresentation != oldScreenProps.stackPresentation) {
770 [self
771 setStackPresentation:[RNSConvert RNSScreenStackPresentationFromCppEquivalent:newScreenProps.stackPresentation]];
772 }
773
774 if (newScreenProps.stackAnimation != oldScreenProps.stackAnimation) {
775 [self setStackAnimation:[RNSConvert RNSScreenStackAnimationFromCppEquivalent:newScreenProps.stackAnimation]];
776 }
777
778 if (newScreenProps.replaceAnimation != oldScreenProps.replaceAnimation) {
779 [self setReplaceAnimation:[RNSConvert RNSScreenReplaceAnimationFromCppEquivalent:newScreenProps.replaceAnimation]];
780 }
781
782 [super updateProps:props oldProps:oldProps];
783}
784
785- (void)updateState:(react::State::Shared const &)state oldState:(react::State::Shared const &)oldState
786{
787 _state = std::static_pointer_cast<const react::RNSScreenShadowNode::ConcreteState>(state);
788}
789
790- (void)updateLayoutMetrics:(const react::LayoutMetrics &)layoutMetrics
791 oldLayoutMetrics:(const react::LayoutMetrics &)oldLayoutMetrics
792{
793 _newLayoutMetrics = layoutMetrics;
794 _oldLayoutMetrics = oldLayoutMetrics;
795 UIViewController *parentVC = self.reactViewController.parentViewController;
796 if (parentVC != nil && ![parentVC isKindOfClass:[RNSNavigationController class]]) {
797 [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
798 }
799 // when screen is mounted under RNSNavigationController it's size is controller
800 // by the navigation controller itself. That is, it is set to fill space of
801 // the controller. In that case we ignore react layout system from managing
802 // the screen dimensions and we wait for the screen VC to update and then we
803 // pass the dimensions to ui view manager to take into account when laying out
804 // subviews
805 // Explanation taken from `reactSetFrame`, which is old arch equivalent of this code.
806}
807
808- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
809{
810 [super finalizeUpdates:updateMask];
811#if !TARGET_OS_TV && !TARGET_OS_VISION
812 [self updatePresentationStyle];
813#endif // !TARGET_OS_TV
814}
815
816#pragma mark - Paper specific
817#else
818
819- (void)didSetProps:(NSArray<NSString *> *)changedProps
820{
821 [super didSetProps:changedProps];
822#if !TARGET_OS_TV && !TARGET_OS_VISION
823 [self updatePresentationStyle];
824#endif // !TARGET_OS_TV
825}
826
827- (void)setPointerEvents:(RCTPointerEvents)pointerEvents
828{
829 // pointer events settings are managed by the parent screen container, we ignore
830 // any attempt of setting that via React props
831}
832
833- (void)reactSetFrame:(CGRect)frame
834{
835 _reactFrame = frame;
836 UIViewController *parentVC = self.reactViewController.parentViewController;
837 if (parentVC != nil && ![parentVC isKindOfClass:[RNSNavigationController class]]) {
838 [super reactSetFrame:frame];
839 }
840 // when screen is mounted under RNSNavigationController it's size is controller
841 // by the navigation controller itself. That is, it is set to fill space of
842 // the controller. In that case we ignore react layout system from managing
843 // the screen dimensions and we wait for the screen VC to update and then we
844 // pass the dimensions to ui view manager to take into account when laying out
845 // subviews
846}
847
848- (void)invalidate
849{
850 _controller = nil;
851}
852#endif
853
854@end
855
856#ifdef RCT_NEW_ARCH_ENABLED
857Class<RCTComponentViewProtocol> RNSScreenCls(void)
858{
859 return RNSScreenView.class;
860}
861#endif
862
863#pragma mark - RNSScreen
864
865@implementation RNSScreen {
866 __weak id _previousFirstResponder;
867 CGRect _lastViewFrame;
868 RNSScreenView *_initialView;
869 UIView *_fakeView;
870 CADisplayLink *_animationTimer;
871 CGFloat _currentAlpha;
872 BOOL _closing;
873 BOOL _goingForward;
874 int _dismissCount;
875 BOOL _isSwiping;
876 BOOL _shouldNotify;
877}
878
879#pragma mark - Common
880
881- (instancetype)initWithView:(UIView *)view
882{
883 if (self = [super init]) {
884 self.view = view;
885 _fakeView = [UIView new];
886 _shouldNotify = YES;
887#ifdef RCT_NEW_ARCH_ENABLED
888 _initialView = (RNSScreenView *)view;
889#endif
890 }
891 return self;
892}
893
894// TODO: Find out why this is executed when screen is going out
895- (void)viewWillAppear:(BOOL)animated
896{
897 [super viewWillAppear:animated];
898 if (!_isSwiping) {
899 [self.screenView notifyWillAppear];
900 if (self.transitionCoordinator.isInteractive) {
901 // we started dismissing with swipe gesture
902 _isSwiping = YES;
903 }
904 } else {
905 // this event is also triggered if we cancelled the swipe.
906 // The _isSwiping is still true, but we don't want to notify then
907 _shouldNotify = NO;
908 }
909
910 [self hideHeaderIfNecessary];
911 // as per documentation of these methods
912 _goingForward = [self isBeingPresented] || [self isMovingToParentViewController];
913
914 [RNSScreenWindowTraits updateWindowTraits];
915 if (_shouldNotify) {
916 _closing = NO;
917 [self notifyTransitionProgress:0.0 closing:_closing goingForward:_goingForward];
918 [self setupProgressNotification];
919 }
920}
921
922- (void)viewWillDisappear:(BOOL)animated
923{
924 [super viewWillDisappear:animated];
925 // self.navigationController might be null when we are dismissing a modal
926 if (!self.transitionCoordinator.isInteractive && self.navigationController != nil) {
927 // user might have long pressed ios 14 back button item,
928 // so he can go back more than one screen and we need to dismiss more screens in JS stack then.
929 // We check it by calculating the difference between the index of currently displayed screen
930 // and the index of the target screen, which is the view of topViewController at this point.
931 // If the value is lower than 1, it means we are dismissing a modal, or navigating forward, or going back with JS.
932 int selfIndex = [self getIndexOfView:self.screenView];
933 int targetIndex = [self getIndexOfView:self.navigationController.topViewController.view];
934 _dismissCount = selfIndex - targetIndex > 0 ? selfIndex - targetIndex : 1;
935 } else {
936 _dismissCount = 1;
937 }
938
939 // same flow as in viewWillAppear
940 if (!_isSwiping) {
941 [self.screenView notifyWillDisappear];
942 if (self.transitionCoordinator.isInteractive) {
943 _isSwiping = YES;
944 }
945 } else {
946 _shouldNotify = NO;
947 }
948
949 // as per documentation of these methods
950 _goingForward = !([self isBeingDismissed] || [self isMovingFromParentViewController]);
951
952 if (_shouldNotify) {
953 _closing = YES;
954 [self notifyTransitionProgress:0.0 closing:_closing goingForward:_goingForward];
955 [self setupProgressNotification];
956 }
957}
958
959- (void)viewDidAppear:(BOOL)animated
960{
961 [super viewDidAppear:animated];
962 if (!_isSwiping || _shouldNotify) {
963 // we are going forward or dismissing without swipe
964 // or successfully swiped back
965 [self.screenView notifyAppear];
966 [self notifyTransitionProgress:1.0 closing:NO goingForward:_goingForward];
967 } else {
968 [self.screenView notifyGestureCancel];
969 }
970
971 _isSwiping = NO;
972 _shouldNotify = YES;
973}
974
975- (void)viewDidDisappear:(BOOL)animated
976{
977 [super viewDidDisappear:animated];
978 if (self.parentViewController == nil && self.presentingViewController == nil) {
979 if (self.screenView.preventNativeDismiss) {
980 // if we want to prevent the native dismiss, we do not send dismissal event,
981 // but instead call `updateContainer`, which restores the JS navigation stack
982 [self.screenView.reactSuperview updateContainer];
983 [self.screenView notifyDismissCancelledWithDismissCount:_dismissCount];
984 } else {
985 // screen dismissed, send event
986 [self.screenView notifyDismissedWithCount:_dismissCount];
987 }
988 }
989 // same flow as in viewDidAppear
990 if (!_isSwiping || _shouldNotify) {
991 [self.screenView notifyDisappear];
992 [self notifyTransitionProgress:1.0 closing:YES goingForward:_goingForward];
993 }
994
995 _isSwiping = NO;
996 _shouldNotify = YES;
997#ifdef RCT_NEW_ARCH_ENABLED
998#else
999 [self traverseForScrollView:self.screenView];
1000#endif
1001}
1002
1003- (void)viewDidLayoutSubviews
1004{
1005 [super viewDidLayoutSubviews];
1006
1007 // The below code makes the screen view adapt dimensions provided by the system. We take these
1008 // into account only when the view is mounted under RNSNavigationController in which case system
1009 // provides additional padding to account for possible header, and in the case when screen is
1010 // shown as a native modal, as the final dimensions of the modal on iOS 12+ are shorter than the
1011 // screen size
1012 BOOL isDisplayedWithinUINavController = [self.parentViewController isKindOfClass:[RNSNavigationController class]];
1013
1014 // Calculate header height on modal open
1015 if (self.screenView.isPresentedAsNativeModal) {
1016 [self calculateAndNotifyHeaderHeightChangeIsModal:YES];
1017 }
1018
1019 if (isDisplayedWithinUINavController || self.screenView.isPresentedAsNativeModal) {
1020#ifdef RCT_NEW_ARCH_ENABLED
1021 [self.screenView updateBounds];
1022#else
1023 if (!CGRectEqualToRect(_lastViewFrame, self.screenView.frame)) {
1024 _lastViewFrame = self.screenView.frame;
1025 [((RNSScreenView *)self.viewIfLoaded) updateBounds];
1026 }
1027#endif
1028 }
1029}
1030
1031- (BOOL)isModalWithHeader
1032{
1033 return self.screenView.isModal && self.childViewControllers.count == 1 &&
1034 [self.childViewControllers[0] isKindOfClass:UINavigationController.class];
1035}
1036
1037// Checks whether this screen has any child view controllers of type RNSNavigationController.
1038// Useful for checking if this screen has nested stack or is displayed at the top.
1039- (BOOL)hasNestedStack
1040{
1041 for (UIViewController *vc in self.childViewControllers) {
1042 if ([vc isKindOfClass:[RNSNavigationController class]]) {
1043 return YES;
1044 }
1045 }
1046
1047 return NO;
1048}
1049
1050- (CGSize)getStatusBarHeightIsModal:(BOOL)isModal
1051{
1052#if !TARGET_OS_TV && !TARGET_OS_VISION
1053 CGSize fallbackStatusBarSize = [[UIApplication sharedApplication] statusBarFrame].size;
1054
1055#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
1056 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
1057 if (@available(iOS 13.0, *)) {
1058 CGSize primaryStatusBarSize = self.view.window.windowScene.statusBarManager.statusBarFrame.size;
1059 if (primaryStatusBarSize.height == 0 || primaryStatusBarSize.width == 0)
1060 return fallbackStatusBarSize;
1061
1062 return primaryStatusBarSize;
1063 } else {
1064 return fallbackStatusBarSize;
1065 }
1066#endif /* Check for iOS 13.0 */
1067
1068#else
1069 // TVOS does not have status bar.
1070 return CGSizeMake(0, 0);
1071#endif // !TARGET_OS_TV
1072}
1073
1074- (UINavigationController *)getVisibleNavigationControllerIsModal:(BOOL)isModal
1075{
1076 UINavigationController *navctr = self.navigationController;
1077
1078 if (isModal) {
1079 // In case where screen is a modal, we want to calculate childViewController's
1080 // navigation bar height instead of the navigation controller from RNSScreen.
1081 if (self.isModalWithHeader) {
1082 navctr = self.childViewControllers[0];
1083 } else {
1084 // If the modal does not meet requirements (there's no RNSNavigationController which means that probably it
1085 // doesn't have header or there are more than one RNSNavigationController which is invalid) we don't want to
1086 // return anything.
1087 return nil;
1088 }
1089 }
1090
1091 return navctr;
1092}
1093
1094- (CGFloat)calculateHeaderHeightIsModal:(BOOL)isModal
1095{
1096 UINavigationController *navctr = [self getVisibleNavigationControllerIsModal:isModal];
1097
1098 // If there's no navigation controller for the modal (or the navigation bar is hidden), we simply don't want to
1099 // return header height, as modal possibly does not have header when navigation controller is nil,
1100 // and we don't want to count status bar if navigation bar is hidden (inset could be negative).
1101 if (navctr == nil || navctr.isNavigationBarHidden) {
1102 return 0;
1103 }
1104
1105 CGFloat navbarHeight = navctr.navigationBar.frame.size.height;
1106#if !TARGET_OS_TV
1107 CGFloat navbarInset = navctr.navigationBar.frame.origin.y;
1108#else
1109 // On TVOS there's no inset of navigation bar.
1110 CGFloat navbarInset = 0;
1111#endif // !TARGET_OS_TV
1112
1113 return navbarHeight + navbarInset;
1114}
1115
1116- (void)calculateAndNotifyHeaderHeightChangeIsModal:(BOOL)isModal
1117{
1118 CGFloat totalHeight = [self calculateHeaderHeightIsModal:isModal];
1119 [self.screenView notifyHeaderHeightChange:totalHeight];
1120}
1121
1122- (void)notifyFinishTransitioning
1123{
1124 [_previousFirstResponder becomeFirstResponder];
1125 _previousFirstResponder = nil;
1126 // the correct Screen for appearance is set after the transition, same for orientation.
1127 [RNSScreenWindowTraits updateWindowTraits];
1128}
1129
1130- (void)willMoveToParentViewController:(UIViewController *)parent
1131{
1132 [super willMoveToParentViewController:parent];
1133 if (parent == nil) {
1134 id responder = [self findFirstResponder:self.screenView];
1135 if (responder != nil) {
1136 _previousFirstResponder = responder;
1137 }
1138 }
1139}
1140
1141- (id)findFirstResponder:(UIView *)parent
1142{
1143 if (parent.isFirstResponder) {
1144 return parent;
1145 }
1146 for (UIView *subView in parent.subviews) {
1147 id responder = [self findFirstResponder:subView];
1148 if (responder != nil) {
1149 return responder;
1150 }
1151 }
1152 return nil;
1153}
1154
1155#pragma mark - transition progress related methods
1156
1157- (void)setupProgressNotification
1158{
1159 if (self.transitionCoordinator != nil) {
1160 _fakeView.alpha = 0.0;
1161 [self.transitionCoordinator
1162 animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
1163 [[context containerView] addSubview:self->_fakeView];
1164 self->_fakeView.alpha = 1.0;
1165 self->_animationTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleAnimation)];
1166 [self->_animationTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
1167 }
1168 completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
1169 [self->_animationTimer setPaused:YES];
1170 [self->_animationTimer invalidate];
1171 [self->_fakeView removeFromSuperview];
1172 }];
1173 }
1174}
1175
1176- (void)handleAnimation
1177{
1178 if ([[_fakeView layer] presentationLayer] != nil) {
1179 CGFloat fakeViewAlpha = _fakeView.layer.presentationLayer.opacity;
1180 if (_currentAlpha != fakeViewAlpha) {
1181 _currentAlpha = fmax(0.0, fmin(1.0, fakeViewAlpha));
1182 [self notifyTransitionProgress:_currentAlpha closing:_closing goingForward:_goingForward];
1183 }
1184 }
1185}
1186
1187- (void)notifyTransitionProgress:(double)progress closing:(BOOL)closing goingForward:(BOOL)goingForward
1188{
1189 if ([self.view isKindOfClass:[RNSScreenView class]]) {
1190 // if the view is already snapshot, there is not sense in sending progress since on JS side
1191 // the component is already not present
1192 [(RNSScreenView *)self.view notifyTransitionProgress:progress closing:closing goingForward:goingForward];
1193 }
1194}
1195
1196#if !TARGET_OS_TV
1197// if the returned vc is a child, it means that it can provide config;
1198// if the returned vc is self, it means that there is no child for config and self has config to provide,
1199// so we return self which results in asking self for preferredStatusBarStyle/Animation etc.;
1200// if the returned vc is nil, it means none of children could provide config and self does not have config either,
1201// so if it was asked by parent, it will fallback to parent's option, or use default option if it is the top Screen
1202- (UIViewController *)findChildVCForConfigAndTrait:(RNSWindowTrait)trait includingModals:(BOOL)includingModals
1203{
1204 UIViewController *lastViewController = [[self childViewControllers] lastObject];
1205 if ([self.presentedViewController isKindOfClass:[RNSScreen class]]) {
1206 lastViewController = self.presentedViewController;
1207
1208 if (!includingModals) {
1209 return nil;
1210 }
1211
1212 // we don't want to allow controlling of status bar appearance when we present non-fullScreen modal
1213 // and it is not possible if `modalPresentationCapturesStatusBarAppearance` is not set to YES, so even
1214 // if we went into a modal here and ask it, it wouldn't take any effect. For fullScreen modals, the system
1215 // asks them by itself, so we can stop traversing here.
1216 // for screen orientation, we need to start the search again from that modal
1217 UIViewController *modalOrChild = [(RNSScreen *)lastViewController findChildVCForConfigAndTrait:trait
1218 includingModals:includingModals];
1219 if (modalOrChild != nil) {
1220 return modalOrChild;
1221 }
1222
1223 // if searched VC was not found, we don't want to search for configs of child VCs any longer,
1224 // and we don't want to rely on lastViewController.
1225 // That's because the modal did not find a child VC that has an orientation set,
1226 // and it doesn't itself have an orientation set. Hence, we fallback to the standard behavior.
1227 // Please keep in mind that this behavior might be wrong and could lead to undiscovered bugs.
1228 // For more information, see https://github.com/software-mansion/react-native-screens/pull/2008.
1229 }
1230
1231 UIViewController *selfOrNil = [self hasTraitSet:trait] ? self : nil;
1232 if (lastViewController == nil) {
1233 return selfOrNil;
1234 } else {
1235 if ([lastViewController conformsToProtocol:@protocol(RNSViewControllerDelegate)]) {
1236 // If there is a child (should be VC of ScreenContainer or ScreenStack), that has a child that could provide the
1237 // trait, we recursively go into its findChildVCForConfig, and if one of the children has the trait set, we return
1238 // it, otherwise we return self if this VC has config, and nil if it doesn't we use
1239 // `childViewControllerForStatusBarStyle` for all options since the behavior is the same for all of them
1240 UIViewController *childScreen = [lastViewController childViewControllerForStatusBarStyle];
1241 if ([childScreen isKindOfClass:[RNSScreen class]]) {
1242 return [(RNSScreen *)childScreen findChildVCForConfigAndTrait:trait includingModals:includingModals]
1243 ?: selfOrNil;
1244 } else {
1245 return selfOrNil;
1246 }
1247 } else {
1248 // child vc is not from this library, so we don't ask it
1249 return selfOrNil;
1250 }
1251 }
1252}
1253
1254- (BOOL)hasTraitSet:(RNSWindowTrait)trait
1255{
1256 switch (trait) {
1257 case RNSWindowTraitStyle: {
1258 return self.screenView.hasStatusBarStyleSet;
1259 }
1260 case RNSWindowTraitAnimation: {
1261 return self.screenView.hasStatusBarAnimationSet;
1262 }
1263 case RNSWindowTraitHidden: {
1264 return self.screenView.hasStatusBarHiddenSet;
1265 }
1266 case RNSWindowTraitOrientation: {
1267 return self.screenView.hasOrientationSet;
1268 }
1269 case RNSWindowTraitHomeIndicatorHidden: {
1270 return self.screenView.hasHomeIndicatorHiddenSet;
1271 }
1272 default: {
1273 RCTLogError(@"Unknown trait passed: %d", (int)trait);
1274 }
1275 }
1276 return NO;
1277}
1278
1279- (UIViewController *)childViewControllerForStatusBarHidden
1280{
1281 UIViewController *vc = [self findChildVCForConfigAndTrait:RNSWindowTraitHidden includingModals:NO];
1282 return vc == self ? nil : vc;
1283}
1284
1285- (BOOL)prefersStatusBarHidden
1286{
1287 return self.screenView.statusBarHidden;
1288}
1289
1290- (UIViewController *)childViewControllerForStatusBarStyle
1291{
1292 UIViewController *vc = [self findChildVCForConfigAndTrait:RNSWindowTraitStyle includingModals:NO];
1293 return vc == self ? nil : vc;
1294}
1295
1296- (UIStatusBarStyle)preferredStatusBarStyle
1297{
1298 return [RNSScreenWindowTraits statusBarStyleForRNSStatusBarStyle:self.screenView.statusBarStyle];
1299}
1300
1301- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
1302{
1303 UIViewController *vc = [self findChildVCForConfigAndTrait:RNSWindowTraitAnimation includingModals:NO];
1304
1305 if ([vc isKindOfClass:[RNSScreen class]]) {
1306 return ((RNSScreen *)vc).screenView.statusBarAnimation;
1307 }
1308 return UIStatusBarAnimationFade;
1309}
1310
1311- (UIInterfaceOrientationMask)supportedInterfaceOrientations
1312{
1313 UIViewController *vc = [self findChildVCForConfigAndTrait:RNSWindowTraitOrientation includingModals:YES];
1314
1315 if ([vc isKindOfClass:[RNSScreen class]]) {
1316 return ((RNSScreen *)vc).screenView.screenOrientation;
1317 }
1318 return UIInterfaceOrientationMaskAllButUpsideDown;
1319}
1320
1321- (UIViewController *)childViewControllerForHomeIndicatorAutoHidden
1322{
1323 UIViewController *vc = [self findChildVCForConfigAndTrait:RNSWindowTraitHomeIndicatorHidden includingModals:YES];
1324 return vc == self ? nil : vc;
1325}
1326
1327- (BOOL)prefersHomeIndicatorAutoHidden
1328{
1329 return self.screenView.homeIndicatorHidden;
1330}
1331- (int)getParentChildrenCount
1332{
1333 return (int)[[self.screenView.reactSuperview reactSubviews] count];
1334}
1335#endif
1336
1337- (int)getIndexOfView:(UIView *)view
1338{
1339 return (int)[[self.screenView.reactSuperview reactSubviews] indexOfObject:view];
1340}
1341
1342// since on Fabric the view of controller can be a snapshot of type `UIView`,
1343// when we want to check props of ScreenView, we need to get them from _initialView
1344- (RNSScreenView *)screenView
1345{
1346#ifdef RCT_NEW_ARCH_ENABLED
1347 return _initialView;
1348#else
1349 return (RNSScreenView *)self.view;
1350#endif
1351}
1352
1353- (void)hideHeaderIfNecessary
1354{
1355#if !TARGET_OS_TV
1356 // On iOS >=13, there is a bug when user transitions from screen with active search bar to screen without header
1357 // In that case default iOS header will be shown. To fix this we hide header when the screens that appears has header
1358 // hidden and search bar was active on previous screen. We need to do it asynchronously, because default header is
1359 // added after viewWillAppear.
1360 if (@available(iOS 13.0, *)) {
1361 NSUInteger currentIndex = [self.navigationController.viewControllers indexOfObject:self];
1362
1363 // we need to check whether reactSubviews array is empty, because on Fabric child nodes are unmounted first ->
1364 // reactSubviews array may be empty
1365 RNSScreenStackHeaderConfig *config = [self.screenView findHeaderConfig];
1366 if (currentIndex > 0 && config != nil) {
1367 UINavigationItem *prevNavigationItem =
1368 [self.navigationController.viewControllers objectAtIndex:currentIndex - 1].navigationItem;
1369 BOOL wasSearchBarActive = prevNavigationItem.searchController.active;
1370
1371#ifdef RCT_NEW_ARCH_ENABLED
1372 BOOL shouldHideHeader = !config.show;
1373#else
1374 BOOL shouldHideHeader = config.hide;
1375#endif
1376
1377 if (wasSearchBarActive && shouldHideHeader) {
1378 dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0);
1379 dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
1380 [self.navigationController setNavigationBarHidden:YES animated:NO];
1381 });
1382 }
1383 }
1384 }
1385#endif
1386}
1387
1388#ifdef RCT_NEW_ARCH_ENABLED
1389#pragma mark - Fabric specific
1390
1391- (void)setViewToSnapshot:(UIView *)snapshot
1392{
1393 UIView *superView = self.view.superview;
1394 [self.view removeFromSuperview];
1395 self.view = snapshot;
1396 [superView addSubview:self.view];
1397}
1398
1399#else
1400#pragma mark - Paper specific
1401
1402- (void)traverseForScrollView:(UIView *)view
1403{
1404 if (![[self.view valueForKey:@"_bridge"] valueForKey:@"_jsThread"]) {
1405 // we don't want to send `scrollViewDidEndDecelerating` event to JS before the JS thread is ready
1406 return;
1407 }
1408
1409 if ([NSStringFromClass([view class]) isEqualToString:@"AVPlayerView"]) {
1410 // Traversing through AVPlayerView is an uncommon edge case that causes the disappearing screen
1411 // to an excessive traversal through all video player elements
1412 // (e.g., for react-native-video, this includes all controls and additional video views).
1413 // Thus, we want to avoid unnecessary traversals through these views.
1414 return;
1415 }
1416
1417 if ([view isKindOfClass:[UIScrollView class]] &&
1418 ([[(UIScrollView *)view delegate] respondsToSelector:@selector(scrollViewDidEndDecelerating:)])) {
1419 [[(UIScrollView *)view delegate] scrollViewDidEndDecelerating:(id)view];
1420 }
1421 [view.subviews enumerateObjectsUsingBlock:^(__kindof UIView *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
1422 [self traverseForScrollView:obj];
1423 }];
1424}
1425#endif
1426
1427@end
1428
1429@implementation RNSScreenManager
1430
1431RCT_EXPORT_MODULE()
1432
1433// we want to handle the case when activityState is nil
1434RCT_REMAP_VIEW_PROPERTY(activityState, activityStateOrNil, NSNumber)
1435RCT_EXPORT_VIEW_PROPERTY(customAnimationOnSwipe, BOOL);
1436RCT_EXPORT_VIEW_PROPERTY(fullScreenSwipeEnabled, BOOL);
1437RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL)
1438RCT_EXPORT_VIEW_PROPERTY(gestureResponseDistance, NSDictionary)
1439RCT_EXPORT_VIEW_PROPERTY(hideKeyboardOnSwipe, BOOL)
1440RCT_EXPORT_VIEW_PROPERTY(preventNativeDismiss, BOOL)
1441RCT_EXPORT_VIEW_PROPERTY(replaceAnimation, RNSScreenReplaceAnimation)
1442RCT_EXPORT_VIEW_PROPERTY(stackPresentation, RNSScreenStackPresentation)
1443RCT_EXPORT_VIEW_PROPERTY(stackAnimation, RNSScreenStackAnimation)
1444RCT_EXPORT_VIEW_PROPERTY(swipeDirection, RNSScreenSwipeDirection)
1445RCT_EXPORT_VIEW_PROPERTY(transitionDuration, NSNumber)
1446
1447RCT_EXPORT_VIEW_PROPERTY(onAppear, RCTDirectEventBlock);
1448RCT_EXPORT_VIEW_PROPERTY(onDisappear, RCTDirectEventBlock);
1449RCT_EXPORT_VIEW_PROPERTY(onHeaderHeightChange, RCTDirectEventBlock);
1450RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTDirectEventBlock);
1451RCT_EXPORT_VIEW_PROPERTY(onNativeDismissCancelled, RCTDirectEventBlock);
1452RCT_EXPORT_VIEW_PROPERTY(onTransitionProgress, RCTDirectEventBlock);
1453RCT_EXPORT_VIEW_PROPERTY(onWillAppear, RCTDirectEventBlock);
1454RCT_EXPORT_VIEW_PROPERTY(onWillDisappear, RCTDirectEventBlock);
1455RCT_EXPORT_VIEW_PROPERTY(onGestureCancel, RCTDirectEventBlock);
1456
1457#if !TARGET_OS_TV
1458RCT_EXPORT_VIEW_PROPERTY(screenOrientation, UIInterfaceOrientationMask)
1459RCT_EXPORT_VIEW_PROPERTY(statusBarAnimation, UIStatusBarAnimation)
1460RCT_EXPORT_VIEW_PROPERTY(statusBarHidden, BOOL)
1461RCT_EXPORT_VIEW_PROPERTY(statusBarStyle, RNSStatusBarStyle)
1462RCT_EXPORT_VIEW_PROPERTY(homeIndicatorHidden, BOOL)
1463
1464RCT_EXPORT_VIEW_PROPERTY(sheetAllowedDetents, RNSScreenDetentType);
1465RCT_EXPORT_VIEW_PROPERTY(sheetLargestUndimmedDetent, RNSScreenDetentType);
1466RCT_EXPORT_VIEW_PROPERTY(sheetGrabberVisible, BOOL);
1467RCT_EXPORT_VIEW_PROPERTY(sheetCornerRadius, CGFloat);
1468RCT_EXPORT_VIEW_PROPERTY(sheetExpandsWhenScrolledToEdge, BOOL);
1469#endif
1470
1471#if !TARGET_OS_TV && !TARGET_OS_VISION
1472// See:
1473// 1. https://github.com/software-mansion/react-native-screens/pull/1543
1474// 2. https://github.com/software-mansion/react-native-screens/pull/1596
1475// This class is instatiated from React Native's internals during application startup
1476- (instancetype)init
1477{
1478 if (self = [super init]) {
1479 dispatch_async(dispatch_get_main_queue(), ^{
1480 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
1481 });
1482 }
1483 return self;
1484}
1485
1486- (void)dealloc
1487{
1488 dispatch_sync(dispatch_get_main_queue(), ^{
1489 [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
1490 });
1491}
1492#endif // !TARGET_OS_TV
1493
1494#ifdef RCT_NEW_ARCH_ENABLED
1495#else
1496- (UIView *)view
1497{
1498 return [[RNSScreenView alloc] initWithBridge:self.bridge];
1499}
1500#endif
1501
1502+ (BOOL)requiresMainQueueSetup
1503{
1504 // Returning NO here despite the fact some initialization in -init method dispatches tasks
1505 // on main queue, because the comments in RN source code states that modules which return YES
1506 // here will be constructed ahead-of-time -- and this is not required in our case.
1507 return NO;
1508}
1509
1510@end
1511
1512@implementation RCTConvert (RNSScreen)
1513
1514RCT_ENUM_CONVERTER(
1515 RNSScreenStackPresentation,
1516 (@{
1517 @"push" : @(RNSScreenStackPresentationPush),
1518 @"modal" : @(RNSScreenStackPresentationModal),
1519 @"fullScreenModal" : @(RNSScreenStackPresentationFullScreenModal),
1520 @"formSheet" : @(RNSScreenStackPresentationFormSheet),
1521 @"containedModal" : @(RNSScreenStackPresentationContainedModal),
1522 @"transparentModal" : @(RNSScreenStackPresentationTransparentModal),
1523 @"containedTransparentModal" : @(RNSScreenStackPresentationContainedTransparentModal)
1524 }),
1525 RNSScreenStackPresentationPush,
1526 integerValue)
1527
1528RCT_ENUM_CONVERTER(
1529 RNSScreenStackAnimation,
1530 (@{
1531 @"default" : @(RNSScreenStackAnimationDefault),
1532 @"none" : @(RNSScreenStackAnimationNone),
1533 @"fade" : @(RNSScreenStackAnimationFade),
1534 @"fade_from_bottom" : @(RNSScreenStackAnimationFadeFromBottom),
1535 @"flip" : @(RNSScreenStackAnimationFlip),
1536 @"simple_push" : @(RNSScreenStackAnimationSimplePush),
1537 @"slide_from_bottom" : @(RNSScreenStackAnimationSlideFromBottom),
1538 @"slide_from_right" : @(RNSScreenStackAnimationDefault),
1539 @"slide_from_left" : @(RNSScreenStackAnimationSlideFromLeft),
1540 @"ios" : @(RNSScreenStackAnimationDefault),
1541 }),
1542 RNSScreenStackAnimationDefault,
1543 integerValue)
1544
1545RCT_ENUM_CONVERTER(
1546 RNSScreenReplaceAnimation,
1547 (@{
1548 @"push" : @(RNSScreenReplaceAnimationPush),
1549 @"pop" : @(RNSScreenReplaceAnimationPop),
1550 }),
1551 RNSScreenReplaceAnimationPop,
1552 integerValue)
1553
1554RCT_ENUM_CONVERTER(
1555 RNSScreenSwipeDirection,
1556 (@{
1557 @"vertical" : @(RNSScreenSwipeDirectionVertical),
1558 @"horizontal" : @(RNSScreenSwipeDirectionHorizontal),
1559 }),
1560 RNSScreenSwipeDirectionHorizontal,
1561 integerValue)
1562
1563#if !TARGET_OS_TV
1564RCT_ENUM_CONVERTER(
1565 UIStatusBarAnimation,
1566 (@{
1567 @"none" : @(UIStatusBarAnimationNone),
1568 @"fade" : @(UIStatusBarAnimationFade),
1569 @"slide" : @(UIStatusBarAnimationSlide)
1570 }),
1571 UIStatusBarAnimationNone,
1572 integerValue)
1573
1574RCT_ENUM_CONVERTER(
1575 RNSStatusBarStyle,
1576 (@{
1577 @"auto" : @(RNSStatusBarStyleAuto),
1578 @"inverted" : @(RNSStatusBarStyleInverted),
1579 @"light" : @(RNSStatusBarStyleLight),
1580 @"dark" : @(RNSStatusBarStyleDark),
1581 }),
1582 RNSStatusBarStyleAuto,
1583 integerValue)
1584
1585RCT_ENUM_CONVERTER(
1586 RNSScreenDetentType,
1587 (@{
1588 @"large" : @(RNSScreenDetentTypeLarge),
1589 @"medium" : @(RNSScreenDetentTypeMedium),
1590 @"all" : @(RNSScreenDetentTypeAll),
1591 }),
1592 RNSScreenDetentTypeAll,
1593 integerValue)
1594
1595+ (UIInterfaceOrientationMask)UIInterfaceOrientationMask:(id)json
1596{
1597 json = [self NSString:json];
1598 if ([json isEqualToString:@"default"]) {
1599 return UIInterfaceOrientationMaskAllButUpsideDown;
1600 } else if ([json isEqualToString:@"all"]) {
1601 return UIInterfaceOrientationMaskAll;
1602 } else if ([json isEqualToString:@"portrait"]) {
1603 return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown;
1604 } else if ([json isEqualToString:@"portrait_up"]) {
1605 return UIInterfaceOrientationMaskPortrait;
1606 } else if ([json isEqualToString:@"portrait_down"]) {
1607 return UIInterfaceOrientationMaskPortraitUpsideDown;
1608 } else if ([json isEqualToString:@"landscape"]) {
1609 return UIInterfaceOrientationMaskLandscape;
1610 } else if ([json isEqualToString:@"landscape_left"]) {
1611 return UIInterfaceOrientationMaskLandscapeLeft;
1612 } else if ([json isEqualToString:@"landscape_right"]) {
1613 return UIInterfaceOrientationMaskLandscapeRight;
1614 }
1615 return UIInterfaceOrientationMaskAllButUpsideDown;
1616}
1617#endif
1618
1619@end