-
-
Notifications
You must be signed in to change notification settings - Fork 530
/
RNSScreen.mm
1547 lines (1384 loc) · 54.2 KB
/
RNSScreen.mm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#import <UIKit/UIKit.h>
#import "RNSScreen.h"
#import "RNSScreenContainer.h"
#import "RNSScreenWindowTraits.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <React/RCTRootComponentView.h>
#import <React/RCTSurfaceTouchHandler.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#import <rnscreens/RNSScreenComponentDescriptor.h>
#import "RNSConvert.h"
#import "RNSScreenViewEvent.h"
#else
#import <React/RCTTouchHandler.h>
#endif
#import <React/RCTShadowView.h>
#import <React/RCTUIManager.h>
#import "NSArray+RNSUtil.h"
#import "RNSScreenStack.h"
#import "RNSScreenStackHeaderConfig.h"
#ifdef RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@interface RNSScreenView ()
#ifdef RCT_NEW_ARCH_ENABLED
<RCTRNSScreenViewProtocol, UIAdaptivePresentationControllerDelegate, CAAnimationDelegate>
#else
<UIAdaptivePresentationControllerDelegate, RCTInvalidating, CAAnimationDelegate>
#endif
@end
@implementation RNSScreenView {
__weak RCTBridge *_bridge;
#ifdef RCT_NEW_ARCH_ENABLED
RCTSurfaceTouchHandler *_touchHandler;
react::RNSScreenShadowNode::ConcreteState::Shared _state;
// on fabric, they are not available by default so we need them exposed here too
NSMutableArray<UIView *> *_reactSubviews;
#else
RCTTouchHandler *_touchHandler;
CGRect _reactFrame;
#endif
}
#ifdef RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const react::RNSScreenProps>();
_props = defaultProps;
_reactSubviews = [NSMutableArray new];
[self initCommonProps];
}
return self;
}
#endif // RCT_NEW_ARCH_ENABLED
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
[self initCommonProps];
}
return self;
}
- (void)initCommonProps
{
_controller = [[RNSScreen alloc] initWithView:self];
_stackPresentation = RNSScreenStackPresentationPush;
_stackAnimation = RNSScreenStackAnimationDefault;
_gestureEnabled = YES;
_replaceAnimation = RNSScreenReplaceAnimationPop;
_dismissed = NO;
_hasStatusBarStyleSet = NO;
_hasStatusBarAnimationSet = NO;
_hasStatusBarHiddenSet = NO;
_hasOrientationSet = NO;
_hasHomeIndicatorHiddenSet = NO;
#if !TARGET_OS_TV
_sheetExpandsWhenScrolledToEdge = YES;
_sheetCustomDetents = [NSArray array];
_sheetCustomLargestUndimmedDetent = nil;
#endif // !TARGET_OS_TV
}
- (UIViewController *)reactViewController
{
return _controller;
}
#ifdef RCT_NEW_ARCH_ENABLED
- (NSArray<UIView *> *)reactSubviews
{
return _reactSubviews;
}
#endif
- (void)updateBounds
{
#ifdef RCT_NEW_ARCH_ENABLED
if (_state != nullptr) {
CAAnimation *sizeAnimation = [self.layer animationForKey:@"bounds.size"];
if (sizeAnimation != nil && self.layer.presentationLayer.bounds.size.height > self.bounds.size.height) {
CABasicAnimation *callbackOnlyAnimation = [CABasicAnimation new];
callbackOnlyAnimation.duration = sizeAnimation.duration;
callbackOnlyAnimation.beginTime = sizeAnimation.beginTime;
callbackOnlyAnimation.delegate = self;
[self.layer addAnimation:callbackOnlyAnimation forKey:@"rns_sheet_animation"];
} else {
auto newState = react::RNSScreenState{RCTSizeFromCGSize(self.bounds.size)};
_state->updateState(std::move(newState));
UINavigationController *navctr = _controller.navigationController;
[navctr.view setNeedsLayout];
}
}
#else
CAAnimation *sizeAnimation = [self.layer animationForKey:@"bounds.size"];
if (sizeAnimation && self.layer.presentationLayer.bounds.size.height > self.bounds.size.height) {
CABasicAnimation *callbackOnlyAnimation = [CABasicAnimation new];
callbackOnlyAnimation.duration = sizeAnimation.duration;
callbackOnlyAnimation.beginTime = sizeAnimation.beginTime;
callbackOnlyAnimation.delegate = self;
[self.layer addAnimation:callbackOnlyAnimation forKey:@"rns_sheet_animation"];
} else {
[_bridge.uiManager setSize:self.bounds.size forView:self];
}
#endif
}
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished
{
if (finished) {
[self updateBounds];
}
}
- (void)setStackPresentation:(RNSScreenStackPresentation)stackPresentation
{
switch (stackPresentation) {
case RNSScreenStackPresentationModal:
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
if (@available(iOS 13.0, tvOS 13.0, *)) {
_controller.modalPresentationStyle = UIModalPresentationAutomatic;
} else {
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
}
#else
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
#endif
break;
case RNSScreenStackPresentationFullScreenModal:
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
break;
#if !TARGET_OS_TV
case RNSScreenStackPresentationFormSheet:
_controller.modalPresentationStyle = UIModalPresentationFormSheet;
break;
#endif
case RNSScreenStackPresentationTransparentModal:
_controller.modalPresentationStyle = UIModalPresentationOverFullScreen;
break;
case RNSScreenStackPresentationContainedModal:
_controller.modalPresentationStyle = UIModalPresentationCurrentContext;
break;
case RNSScreenStackPresentationContainedTransparentModal:
_controller.modalPresentationStyle = UIModalPresentationOverCurrentContext;
break;
case RNSScreenStackPresentationPush:
// ignored, we only need to keep in mind not to set presentation delegate
break;
}
// There is a bug in UIKit which causes retain loop when presentationController is accessed for a
// controller that is not going to be presented modally. We therefore need to avoid setting the
// delegate for screens presented using push. This also means that when controller is updated from
// modal to push type, this may cause memory leak, we warn about that as well.
if (stackPresentation != RNSScreenStackPresentationPush) {
// `modalPresentationStyle` must be set before accessing `presentationController`
// otherwise a default controller will be created and cannot be changed after.
// Documented here:
// https://developer.apple.com/documentation/uikit/uiviewcontroller/1621426-presentationcontroller?language=objc
_controller.presentationController.delegate = self;
} else if (_stackPresentation != RNSScreenStackPresentationPush) {
#ifdef RCT_NEW_ARCH_ENABLED
// TODO: on Fabric, same controllers can be used as modals and then recycled and used a push which would result in
// this error. It would be good to check if it doesn't leak in such case.
#else
RCTLogError(
@"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");
#endif
}
_stackPresentation = stackPresentation;
}
- (void)setStackAnimation:(RNSScreenStackAnimation)stackAnimation
{
_stackAnimation = stackAnimation;
switch (stackAnimation) {
case RNSScreenStackAnimationFade:
_controller.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
break;
#if !TARGET_OS_TV
case RNSScreenStackAnimationFlip:
_controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
break;
#endif
case RNSScreenStackAnimationNone:
case RNSScreenStackAnimationDefault:
case RNSScreenStackAnimationSimplePush:
case RNSScreenStackAnimationSlideFromBottom:
case RNSScreenStackAnimationFadeFromBottom:
// Default
break;
}
}
- (void)setGestureEnabled:(BOOL)gestureEnabled
{
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
if (@available(iOS 13.0, tvOS 13.0, *)) {
_controller.modalInPresentation = !gestureEnabled;
}
#endif
_gestureEnabled = gestureEnabled;
}
- (void)setReplaceAnimation:(RNSScreenReplaceAnimation)replaceAnimation
{
_replaceAnimation = replaceAnimation;
}
// Nil will be provided when activityState is set as an animated value and we change
// it from JS to be a plain value (non animated).
// In case when nil is received, we want to ignore such value and not make
// any updates as the actual non-nil value will follow immediately.
- (void)setActivityStateOrNil:(NSNumber *)activityStateOrNil
{
int activityState = [activityStateOrNil intValue];
if (activityStateOrNil != nil && activityState != -1 && activityState != _activityState) {
_activityState = activityState;
[_reactSuperview markChildUpdated];
}
}
#if !TARGET_OS_TV
- (void)setStatusBarStyle:(RNSStatusBarStyle)statusBarStyle
{
_hasStatusBarStyleSet = YES;
_statusBarStyle = statusBarStyle;
[RNSScreenWindowTraits assertViewControllerBasedStatusBarAppearenceSet];
[RNSScreenWindowTraits updateStatusBarAppearance];
}
- (void)setStatusBarAnimation:(UIStatusBarAnimation)statusBarAnimation
{
_hasStatusBarAnimationSet = YES;
_statusBarAnimation = statusBarAnimation;
[RNSScreenWindowTraits assertViewControllerBasedStatusBarAppearenceSet];
}
- (void)setStatusBarHidden:(BOOL)statusBarHidden
{
_hasStatusBarHiddenSet = YES;
_statusBarHidden = statusBarHidden;
[RNSScreenWindowTraits assertViewControllerBasedStatusBarAppearenceSet];
[RNSScreenWindowTraits updateStatusBarAppearance];
}
- (void)setScreenOrientation:(UIInterfaceOrientationMask)screenOrientation
{
_hasOrientationSet = YES;
_screenOrientation = screenOrientation;
[RNSScreenWindowTraits enforceDesiredDeviceOrientation];
}
- (void)setHomeIndicatorHidden:(BOOL)homeIndicatorHidden
{
_hasHomeIndicatorHiddenSet = YES;
_homeIndicatorHidden = homeIndicatorHidden;
[RNSScreenWindowTraits updateHomeIndicatorAutoHidden];
}
#endif
- (UIView *)reactSuperview
{
return _reactSuperview;
}
- (void)addSubview:(UIView *)view
{
if (![view isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
[super addSubview:view];
} else {
((RNSScreenStackHeaderConfig *)view).screenView = self;
}
}
- (void)notifyDismissedWithCount:(int)dismissCount
{
#ifdef RCT_NEW_ARCH_ENABLED
// If screen is already unmounted then there will be no event emitter
// it will be cleaned in prepareForRecycle
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
->onDismissed(react::RNSScreenEventEmitter::OnDismissed{.dismissCount = dismissCount});
}
#else
// TODO: hopefully problems connected to dismissed prop are only the case on paper
_dismissed = YES;
if (self.onDismissed) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.onDismissed) {
self.onDismissed(@{@"dismissCount" : @(dismissCount)});
}
});
}
#endif
}
- (void)notifyDismissCancelledWithDismissCount:(int)dismissCount
{
#ifdef RCT_NEW_ARCH_ENABLED
// If screen is already unmounted then there will be no event emitter
// it will be cleaned in prepareForRecycle
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
->onNativeDismissCancelled(
react::RNSScreenEventEmitter::OnNativeDismissCancelled{.dismissCount = dismissCount});
}
#else
if (self.onNativeDismissCancelled) {
self.onNativeDismissCancelled(@{@"dismissCount" : @(dismissCount)});
}
#endif
}
- (void)notifyWillAppear
{
#ifdef RCT_NEW_ARCH_ENABLED
// If screen is already unmounted then there will be no event emitter
// it will be cleaned in prepareForRecycle
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
->onWillAppear(react::RNSScreenEventEmitter::OnWillAppear{});
}
[self updateLayoutMetrics:_newLayoutMetrics oldLayoutMetrics:_oldLayoutMetrics];
#else
if (self.onWillAppear) {
self.onWillAppear(nil);
}
// we do it here too because at this moment the `parentViewController` is already not nil,
// so if the parent is not UINavCtr, the frame will be updated to the correct one.
[self reactSetFrame:_reactFrame];
#endif
}
- (void)notifyWillDisappear
{
if (_hideKeyboardOnSwipe) {
[self endEditing:YES];
}
#ifdef RCT_NEW_ARCH_ENABLED
// If screen is already unmounted then there will be no event emitter
// it will be cleaned in prepareForRecycle
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
->onWillDisappear(react::RNSScreenEventEmitter::OnWillDisappear{});
}
#else
if (self.onWillDisappear) {
self.onWillDisappear(nil);
}
#endif
}
- (void)notifyAppear
{
#ifdef RCT_NEW_ARCH_ENABLED
// If screen is already unmounted then there will be no event emitter
// it will be cleaned in prepareForRecycle
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
->onAppear(react::RNSScreenEventEmitter::OnAppear{});
}
#else
if (self.onAppear) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.onAppear) {
self.onAppear(nil);
}
});
}
#endif
}
- (void)notifyDisappear
{
#ifdef RCT_NEW_ARCH_ENABLED
// If screen is already unmounted then there will be no event emitter
// it will be cleaned in prepareForRecycle
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
->onDisappear(react::RNSScreenEventEmitter::OnDisappear{});
}
#else
if (self.onDisappear) {
self.onDisappear(nil);
}
#endif
}
- (void)notifyGestureCancel
{
#ifdef RCT_NEW_ARCH_ENABLED
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
->onGestureCancel(react::RNSScreenEventEmitter::OnGestureCancel{});
}
#else
if (self.onGestureCancel) {
self.onGestureCancel(nil);
}
#endif
}
- (BOOL)isMountedUnderScreenOrReactRoot
{
#ifdef RCT_NEW_ARCH_ENABLED
#define RNS_EXPECTED_VIEW RCTRootComponentView
#else
#define RNS_EXPECTED_VIEW RCTRootView
#endif
for (UIView *parent = self.superview; parent != nil; parent = parent.superview) {
if ([parent isKindOfClass:[RNS_EXPECTED_VIEW class]] || [parent isKindOfClass:[RNSScreenView class]]) {
return YES;
}
}
return NO;
#undef RNS_EXPECTED_VIEW
}
- (void)didMoveToWindow
{
// For RN touches to work we need to instantiate and connect RCTTouchHandler. This only applies
// for screens that aren't mounted under RCTRootView e.g., modals that are mounted directly to
// root application window.
if (self.window != nil && ![self isMountedUnderScreenOrReactRoot]) {
if (_touchHandler == nil) {
#ifdef RCT_NEW_ARCH_ENABLED
_touchHandler = [RCTSurfaceTouchHandler new];
#else
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
#endif
}
[_touchHandler attachToView:self];
} else {
[_touchHandler detachFromView:self];
}
}
#ifdef RCT_NEW_ARCH_ENABLED
- (RCTSurfaceTouchHandler *)touchHandler
#else
- (RCTTouchHandler *)touchHandler
#endif
{
if (_touchHandler != nil) {
return _touchHandler;
}
UIView *parent = [self superview];
while (parent != nil && ![parent respondsToSelector:@selector(touchHandler)])
parent = parent.superview;
if (parent != nil) {
return [parent performSelector:@selector(touchHandler)];
}
return nil;
}
- (void)notifyFinishTransitioning
{
[_controller notifyFinishTransitioning];
}
- (void)notifyTransitionProgress:(double)progress closing:(BOOL)closing goingForward:(BOOL)goingForward
{
#ifdef RCT_NEW_ARCH_ENABLED
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSScreenEventEmitter>(_eventEmitter)
->onTransitionProgress(react::RNSScreenEventEmitter::OnTransitionProgress{
.progress = progress, .closing = closing ? 1 : 0, .goingForward = goingForward ? 1 : 0});
}
RNSScreenViewEvent *event = [[RNSScreenViewEvent alloc] initWithEventName:@"onTransitionProgress"
reactTag:[NSNumber numberWithInt:self.tag]
progress:progress
closing:closing
goingForward:goingForward];
[[RCTBridge currentBridge].eventDispatcher sendEvent:event];
#else
if (self.onTransitionProgress) {
self.onTransitionProgress(@{
@"progress" : @(progress),
@"closing" : @(closing ? 1 : 0),
@"goingForward" : @(goingForward ? 1 : 0),
});
}
#endif
}
- (void)presentationControllerWillDismiss:(UIPresentationController *)presentationController
{
// We need to call both "cancel" and "reset" here because RN's gesture recognizer
// does not handle the scenario when it gets cancelled by other top
// level gesture recognizer. In this case by the modal dismiss gesture.
// Because of that, at the moment when this method gets called the React's
// gesture recognizer is already in FAILED state but cancel events never gets
// send to JS. Calling "reset" forces RCTTouchHanler to dispatch cancel event.
// To test this behavior one need to open a dismissable modal and start
// pulling down starting at some touchable item. Without "reset" the touchable
// will never go back from highlighted state even when the modal start sliding
// down.
#ifdef RCT_NEW_ARCH_ENABLED
[_touchHandler setEnabled:NO];
[_touchHandler setEnabled:YES];
#else
[_touchHandler cancel];
#endif
[_touchHandler reset];
}
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
{
if (_preventNativeDismiss) {
[self notifyDismissCancelledWithDismissCount:1];
return NO;
}
return _gestureEnabled;
}
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
[_reactSuperview performSelector:@selector(presentationControllerDidDismiss:) withObject:presentationController];
}
}
- (BOOL)isModal
{
return self.stackPresentation != RNSScreenStackPresentationPush;
}
- (RNSScreenStackHeaderConfig *_Nullable)findHeaderConfig
{
for (UIView *view in self.reactSubviews) {
if ([view isKindOfClass:RNSScreenStackHeaderConfig.class]) {
return (RNSScreenStackHeaderConfig *)view;
}
}
return nil;
}
#if !TARGET_OS_TV
/**
* Updates settings for sheet presentation controller.
* Note that this method should not be called inside `stackPresentation` setter, because on Paper we don't have
* guarantee that values of all related props had been updated earlier.
*/
- (void)updateFormSheetPresentationStyle
{
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_15_0) && \
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_15_0
if (@available(iOS 15.0, *)) {
UISheetPresentationController *sheet = _controller.sheetPresentationController;
if (_stackPresentation == RNSScreenStackPresentationFormSheet && sheet != nil) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_16_0) && \
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0
if ([_sheetCustomDetents rns_isNotEmpty]) {
if (@available(iOS 16.0, *)) {
sheet.detents = [self detentsFromMaxHeightFractions:_sheetCustomDetents];
}
} else
#endif // Check for iOS >= 16
{
if (_sheetAllowedDetents == RNSScreenDetentTypeMedium) {
sheet.detents = @[ UISheetPresentationControllerDetent.mediumDetent ];
if (sheet.selectedDetentIdentifier != UISheetPresentationControllerDetentIdentifierMedium) {
[sheet animateChanges:^{
sheet.selectedDetentIdentifier = UISheetPresentationControllerDetentIdentifierMedium;
}];
}
} else if (_sheetAllowedDetents == RNSScreenDetentTypeLarge) {
sheet.detents = @[ UISheetPresentationControllerDetent.largeDetent ];
if (sheet.selectedDetentIdentifier != UISheetPresentationControllerDetentIdentifierLarge) {
[sheet animateChanges:^{
sheet.selectedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
}];
}
} else if (_sheetAllowedDetents == RNSScreenDetentTypeAll) {
sheet.detents = @[
UISheetPresentationControllerDetent.mediumDetent,
UISheetPresentationControllerDetent.largeDetent,
];
}
}
sheet.prefersScrollingExpandsWhenScrolledToEdge = _sheetExpandsWhenScrolledToEdge;
sheet.prefersGrabberVisible = _sheetGrabberVisible;
sheet.preferredCornerRadius =
_sheetCornerRadius < 0 ? UISheetPresentationControllerAutomaticDimension : _sheetCornerRadius;
int detentIndex = _sheetCustomLargestUndimmedDetent != nil ? _sheetCustomLargestUndimmedDetent.intValue : -1;
if (detentIndex != -1 && [_sheetCustomDetents rns_isNotEmpty]) {
if (detentIndex >= 0 && detentIndex < _sheetCustomDetents.count) {
sheet.largestUndimmedDetentIdentifier = _sheetCustomLargestUndimmedDetent.stringValue;
} else {
sheet.largestUndimmedDetentIdentifier = nil;
}
} else if (_sheetLargestUndimmedDetent == RNSScreenDetentTypeMedium) {
sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierMedium;
} else if (_sheetLargestUndimmedDetent == RNSScreenDetentTypeLarge) {
sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
} else if (_sheetLargestUndimmedDetent == RNSScreenDetentTypeAll) {
sheet.largestUndimmedDetentIdentifier = nil;
} else {
RCTLogError(@"Unhandled value of sheetLargestUndimmedDetent passed");
}
}
}
#endif // Check for iOS >= 15
}
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_16_0) && \
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0
- (NSArray<UISheetPresentationControllerDetent *> *)detentsFromMaxHeightFractions:(NSArray<NSNumber *> *)fractions
API_AVAILABLE(ios(16.0))
{
NSMutableArray<UISheetPresentationControllerDetent *> *customDetents =
[NSMutableArray arrayWithCapacity:fractions.count];
int detentIndex = 0;
for (NSNumber *frac in fractions) {
NSString *ident = [[NSNumber numberWithInt:detentIndex] stringValue];
[customDetents addObject:[UISheetPresentationControllerDetent
customDetentWithIdentifier:ident
resolver:^CGFloat(
id<UISheetPresentationControllerDetentResolutionContext> ctx) {
return ctx.maximumDetentValue * frac.floatValue;
}]];
++detentIndex;
}
return customDetents;
}
#endif // Check for iOS >= 16
#endif // !TARGET_OS_TV
#pragma mark - Fabric specific
#ifdef RCT_NEW_ARCH_ENABLED
- (BOOL)hasHeaderConfig
{
return _config != nil;
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSScreenComponentDescriptor>();
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if ([childComponentView isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
_config = (RNSScreenStackHeaderConfig *)childComponentView;
_config.screenView = self;
}
[_reactSubviews insertObject:childComponentView atIndex:index];
[super mountChildComponentView:childComponentView index:index];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if ([childComponentView isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
_config = nil;
}
[_reactSubviews removeObject:childComponentView];
[super unmountChildComponentView:childComponentView index:index];
}
#pragma mark - RCTComponentViewProtocol
- (void)prepareForRecycle
{
[super prepareForRecycle];
// TODO: Make sure that there is no edge case when this should be uncommented
// _controller=nil;
_dismissed = NO;
_state.reset();
_touchHandler = nil;
// We set this prop to default value here to workaround view-recycling.
// Let's assume the view has had _stackPresentation == <some modal stack presentation> set
// before below line was executed. Then, when instantiated again (with the same modal presentation)
// updateProps:oldProps: method would be called and setter for stack presentation would not be called.
// This is crucial as in that setter we register `self.controller` as a delegate
// (UIAdaptivePresentationControllerDelegate) to presentation controller and this leads to buggy modal behaviour as we
// rely on UIAdaptivePresentationControllerDelegate callbacks. Restoring the default value and then comparing against
// it in updateProps:oldProps: allows for setter to be called, however if there was some additional logic to execute
// when stackPresentation is set to "push" the setter would not be triggered.
_stackPresentation = RNSScreenStackPresentationPush;
}
- (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::Shared const &)oldProps
{
const auto &oldScreenProps = *std::static_pointer_cast<const react::RNSScreenProps>(_props);
const auto &newScreenProps = *std::static_pointer_cast<const react::RNSScreenProps>(props);
[self setFullScreenSwipeEnabled:newScreenProps.fullScreenSwipeEnabled];
[self setGestureEnabled:newScreenProps.gestureEnabled];
[self setTransitionDuration:[NSNumber numberWithInt:newScreenProps.transitionDuration]];
[self setHideKeyboardOnSwipe:newScreenProps.hideKeyboardOnSwipe];
[self setCustomAnimationOnSwipe:newScreenProps.customAnimationOnSwipe];
[self
setGestureResponseDistance:[RNSConvert
gestureResponseDistanceDictFromCppStruct:newScreenProps.gestureResponseDistance]];
[self setPreventNativeDismiss:newScreenProps.preventNativeDismiss];
[self setActivityStateOrNil:[NSNumber numberWithFloat:newScreenProps.activityState]];
[self setSwipeDirection:[RNSConvert RNSScreenSwipeDirectionFromCppEquivalent:newScreenProps.swipeDirection]];
#if !TARGET_OS_TV
if (newScreenProps.statusBarHidden != oldScreenProps.statusBarHidden) {
[self setStatusBarHidden:newScreenProps.statusBarHidden];
}
if (newScreenProps.statusBarStyle != oldScreenProps.statusBarStyle) {
[self setStatusBarStyle:[RCTConvert
RNSStatusBarStyle:RCTNSStringFromStringNilIfEmpty(newScreenProps.statusBarStyle)]];
}
if (newScreenProps.statusBarAnimation != oldScreenProps.statusBarAnimation) {
[self setStatusBarAnimation:[RCTConvert UIStatusBarAnimation:RCTNSStringFromStringNilIfEmpty(
newScreenProps.statusBarAnimation)]];
}
if (newScreenProps.screenOrientation != oldScreenProps.screenOrientation) {
[self setScreenOrientation:[RCTConvert UIInterfaceOrientationMask:RCTNSStringFromStringNilIfEmpty(
newScreenProps.screenOrientation)]];
}
if (newScreenProps.homeIndicatorHidden != oldScreenProps.homeIndicatorHidden) {
[self setHomeIndicatorHidden:newScreenProps.homeIndicatorHidden];
}
[self setSheetGrabberVisible:newScreenProps.sheetGrabberVisible];
[self setSheetCornerRadius:newScreenProps.sheetCornerRadius];
[self setSheetExpandsWhenScrolledToEdge:newScreenProps.sheetExpandsWhenScrolledToEdge];
if (newScreenProps.sheetAllowedDetents != oldScreenProps.sheetAllowedDetents) {
[self setSheetAllowedDetents:[RNSConvert RNSScreenDetentTypeFromAllowedDetents:newScreenProps.sheetAllowedDetents]];
}
if (newScreenProps.sheetLargestUndimmedDetent != oldScreenProps.sheetLargestUndimmedDetent) {
[self setSheetLargestUndimmedDetent:
[RNSConvert RNSScreenDetentTypeFromLargestUndimmedDetent:newScreenProps.sheetLargestUndimmedDetent]];
}
#endif // !TARGET_OS_TV
// Notice that we compare against _stackPresentation, not oldScreenProps.stackPresentation.
// See comment in prepareForRecycle method for explanation.
RNSScreenStackPresentation newStackPresentation =
[RNSConvert RNSScreenStackPresentationFromCppEquivalent:newScreenProps.stackPresentation];
if (newStackPresentation != _stackPresentation) {
[self setStackPresentation:newStackPresentation];
}
if (newScreenProps.stackAnimation != oldScreenProps.stackAnimation) {
[self setStackAnimation:[RNSConvert RNSScreenStackAnimationFromCppEquivalent:newScreenProps.stackAnimation]];
}
if (newScreenProps.replaceAnimation != oldScreenProps.replaceAnimation) {
[self setReplaceAnimation:[RNSConvert RNSScreenReplaceAnimationFromCppEquivalent:newScreenProps.replaceAnimation]];
}
if (_stackPresentation == RNSScreenStackPresentationFormSheet) {
if (newScreenProps.sheetCustomDetents != oldScreenProps.sheetCustomDetents) {
[self setSheetCustomDetents:[RNSConvert NSNumberMutableArrayFromFloatVector:newScreenProps.sheetCustomDetents]];
}
if (newScreenProps.sheetCustomLargestUndimmedDetent != oldScreenProps.sheetCustomLargestUndimmedDetent) {
[self
setSheetCustomLargestUndimmedDetent:[NSNumber numberWithInt:newScreenProps.sheetCustomLargestUndimmedDetent]];
}
}
[super updateProps:props oldProps:oldProps];
}
- (void)updateState:(react::State::Shared const &)state oldState:(react::State::Shared const &)oldState
{
_state = std::static_pointer_cast<const react::RNSScreenShadowNode::ConcreteState>(state);
}
- (void)updateLayoutMetrics:(const react::LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const react::LayoutMetrics &)oldLayoutMetrics
{
_newLayoutMetrics = layoutMetrics;
_oldLayoutMetrics = oldLayoutMetrics;
UIViewController *parentVC = self.reactViewController.parentViewController;
if (parentVC != nil && ![parentVC isKindOfClass:[RNSNavigationController class]]) {
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
}
// when screen is mounted under RNSNavigationController it's size is controller
// by the navigation controller itself. That is, it is set to fill space of
// the controller. In that case we ignore react layout system from managing
// the screen dimensions and we wait for the screen VC to update and then we
// pass the dimensions to ui view manager to take into account when laying out
// subviews
// Explanation taken from `reactSetFrame`, which is old arch equivalent of this code.
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
#if !TARGET_OS_TV
[self updateFormSheetPresentationStyle];
#endif // !TARGET_OS_TV
}
#pragma mark - Paper specific
#else
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
#if !TARGET_OS_TV
[self updateFormSheetPresentationStyle];
#endif // !TARGET_OS_TV
}
- (void)setPointerEvents:(RCTPointerEvents)pointerEvents
{
// pointer events settings are managed by the parent screen container, we ignore
// any attempt of setting that via React props
}
- (void)reactSetFrame:(CGRect)frame
{
_reactFrame = frame;
UIViewController *parentVC = self.reactViewController.parentViewController;
if (parentVC != nil && ![parentVC isKindOfClass:[RNSNavigationController class]]) {
[super reactSetFrame:frame];
}
// when screen is mounted under RNSNavigationController it's size is controller
// by the navigation controller itself. That is, it is set to fill space of
// the controller. In that case we ignore react layout system from managing
// the screen dimensions and we wait for the screen VC to update and then we
// pass the dimensions to ui view manager to take into account when laying out
// subviews
}
- (void)invalidate
{
_controller = nil;
}
#endif
@end
#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSScreenCls(void)
{
return RNSScreenView.class;
}
#endif
#pragma mark - RNSScreen
@implementation RNSScreen {
__weak id _previousFirstResponder;
CGRect _lastViewFrame;
RNSScreenView *_initialView;
UIView *_fakeView;
CADisplayLink *_animationTimer;
CGFloat _currentAlpha;
BOOL _closing;
BOOL _goingForward;
int _dismissCount;
BOOL _isSwiping;
BOOL _shouldNotify;
}
#pragma mark - Common
- (instancetype)initWithView:(UIView *)view
{
if (self = [super init]) {
self.view = view;
_fakeView = [UIView new];
_shouldNotify = YES;
#ifdef RCT_NEW_ARCH_ENABLED
_initialView = (RNSScreenView *)view;
#endif
}
return self;
}
// TODO: Find out why this is executed when screen is going out
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (!_isSwiping) {
[self.screenView notifyWillAppear];
if (self.transitionCoordinator.isInteractive) {
// we started dismissing with swipe gesture
_isSwiping = YES;
}
} else {
// this event is also triggered if we cancelled the swipe.
// The _isSwiping is still true, but we don't want to notify then
_shouldNotify = NO;
}
[self hideHeaderIfNecessary];
// as per documentation of these methods
_goingForward = [self isBeingPresented] || [self isMovingToParentViewController];
[RNSScreenWindowTraits updateWindowTraits];
if (_shouldNotify) {
_closing = NO;
[self notifyTransitionProgress:0.0 closing:_closing goingForward:_goingForward];
[self setupProgressNotification];
}
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// self.navigationController might be null when we are dismissing a modal
if (!self.transitionCoordinator.isInteractive && self.navigationController != nil) {
// user might have long pressed ios 14 back button item,
// so he can go back more than one screen and we need to dismiss more screens in JS stack then.
// We check it by calculating the difference between the index of currently displayed screen
// and the index of the target screen, which is the view of topViewController at this point.
// If the value is lower than 1, it means we are dismissing a modal, or navigating forward, or going back with JS.
int selfIndex = [self getIndexOfView:self.screenView];
int targetIndex = [self getIndexOfView:self.navigationController.topViewController.view];
_dismissCount = selfIndex - targetIndex > 0 ? selfIndex - targetIndex : 1;
} else {
_dismissCount = 1;
}
// same flow as in viewWillAppear
if (!_isSwiping) {
[self.screenView notifyWillDisappear];
if (self.transitionCoordinator.isInteractive) {
_isSwiping = YES;
}
} else {
_shouldNotify = NO;
}
// as per documentation of these methods
_goingForward = !([self isBeingDismissed] || [self isMovingFromParentViewController]);
if (_shouldNotify) {
_closing = YES;
[self notifyTransitionProgress:0.0 closing:_closing goingForward:_goingForward];
[self setupProgressNotification];
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (!_isSwiping || _shouldNotify) {
// we are going forward or dismissing without swipe
// or successfully swiped back
[self.screenView notifyAppear];
[self notifyTransitionProgress:1.0 closing:NO goingForward:_goingForward];
} else {
[self.screenView notifyGestureCancel];
}