@lodev09/react-native-true-sheet 3.8.0-beta.2 → 3.8.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +22 -2
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +29 -31
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +10 -9
  4. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +6 -12
  5. package/ios/TrueSheetContentView.mm +6 -0
  6. package/ios/TrueSheetModule.mm +33 -9
  7. package/ios/TrueSheetView.h +3 -1
  8. package/ios/TrueSheetView.mm +41 -33
  9. package/ios/TrueSheetViewController.h +1 -0
  10. package/ios/TrueSheetViewController.mm +4 -8
  11. package/ios/core/RNScreensEventObserver.mm +23 -15
  12. package/lib/module/TrueSheet.js +40 -0
  13. package/lib/module/TrueSheet.js.map +1 -1
  14. package/lib/module/TrueSheet.web.js +26 -40
  15. package/lib/module/TrueSheet.web.js.map +1 -1
  16. package/lib/module/TrueSheetProvider.js +1 -0
  17. package/lib/module/TrueSheetProvider.js.map +1 -1
  18. package/lib/module/TrueSheetProvider.web.js +7 -32
  19. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  20. package/lib/module/index.js +1 -1
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/mocks/index.js +3 -0
  23. package/lib/module/mocks/index.js.map +1 -1
  24. package/lib/module/mocks/reanimated.js +2 -0
  25. package/lib/module/mocks/reanimated.js.map +1 -1
  26. package/lib/module/navigation/TrueSheetRouter.js +42 -8
  27. package/lib/module/navigation/TrueSheetRouter.js.map +1 -1
  28. package/lib/module/navigation/screen/useSheetScreenState.js +8 -17
  29. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  30. package/lib/module/reanimated/ReanimatedTrueSheet.web.js +2 -2
  31. package/lib/module/reanimated/ReanimatedTrueSheet.web.js.map +1 -1
  32. package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
  33. package/lib/typescript/src/TrueSheet.d.ts +29 -2
  34. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  35. package/lib/typescript/src/TrueSheet.types.d.ts +0 -48
  36. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  37. package/lib/typescript/src/TrueSheet.web.d.ts +4 -2
  38. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  39. package/lib/typescript/src/TrueSheetProvider.d.ts +3 -2
  40. package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -1
  41. package/lib/typescript/src/TrueSheetProvider.web.d.ts +6 -14
  42. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  43. package/lib/typescript/src/index.d.ts +1 -1
  44. package/lib/typescript/src/index.d.ts.map +1 -1
  45. package/lib/typescript/src/mocks/index.d.ts +6 -3
  46. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  47. package/lib/typescript/src/mocks/reanimated.d.ts +4 -2
  48. package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -1
  49. package/lib/typescript/src/navigation/TrueSheetRouter.d.ts.map +1 -1
  50. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  51. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts +4 -3
  52. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts.map +1 -1
  53. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +9 -1
  54. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
  55. package/package.json +1 -1
  56. package/src/TrueSheet.tsx +39 -5
  57. package/src/TrueSheet.types.ts +0 -50
  58. package/src/TrueSheet.web.tsx +32 -50
  59. package/src/TrueSheetProvider.tsx +7 -2
  60. package/src/TrueSheetProvider.web.tsx +19 -38
  61. package/src/index.ts +1 -1
  62. package/src/mocks/index.ts +7 -6
  63. package/src/mocks/reanimated.ts +4 -5
  64. package/src/navigation/TrueSheetRouter.ts +51 -16
  65. package/src/navigation/screen/useSheetScreenState.ts +5 -11
  66. package/src/reanimated/ReanimatedTrueSheet.web.tsx +28 -30
  67. package/src/specs/NativeTrueSheetModule.ts +10 -1
@@ -54,7 +54,7 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
54
54
  }
55
55
 
56
56
  /**
57
- * Dismiss a sheet by reference
57
+ * Dismiss a sheet and all sheets presented on top of it
58
58
  *
59
59
  * @param viewTag Native view tag of the sheet component
60
60
  * @param promise Promise that resolves when sheet is fully dismissed
@@ -73,6 +73,26 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
73
73
  }
74
74
  }
75
75
 
76
+ /**
77
+ * Dismiss only the sheets presented on top of this sheet, keeping this sheet presented
78
+ *
79
+ * @param viewTag Native view tag of the sheet component
80
+ * @param promise Promise that resolves when all child sheets are fully dismissed
81
+ * @throws VIEW_NOT_FOUND if the view with the given tag is not found
82
+ * @throws INVALID_VIEW_TYPE if the view is not a TrueSheetView
83
+ * @throws OPERATION_FAILED if the operation fails for any other reason
84
+ */
85
+ @ReactMethod
86
+ fun dismissStackByRef(viewTag: Double, animated: Boolean, promise: Promise) {
87
+ val tag = viewTag.toInt()
88
+
89
+ withTrueSheetView(tag, promise) { view ->
90
+ view.dismissStack(animated) {
91
+ promise.resolve(null)
92
+ }
93
+ }
94
+ }
95
+
76
96
  /**
77
97
  * Resize a sheet to a different index by reference
78
98
  *
@@ -111,7 +131,7 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
111
131
  return@post
112
132
  }
113
133
 
114
- rootSheet.dismissAll(animated) {
134
+ rootSheet.dismiss(animated) {
115
135
  promise.resolve(null)
116
136
  }
117
137
  } catch (e: Exception) {
@@ -135,11 +135,13 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
135
135
  val child = getChildAt(index)
136
136
  if (child is TrueSheetContainerView) {
137
137
  child.delegate = null
138
- viewController.createSheetSnapshot()
139
138
 
140
- // Dismiss when container is removed
141
- if (viewController.isPresented) {
142
- dismissAll(true) {}
139
+ // Skip if already dismissing - snapshot preserves visuals during dismiss
140
+ if (!viewController.isBeingDismissed) {
141
+ viewController.createSheetSnapshot()
142
+ if (viewController.isPresented) {
143
+ dismiss(true) {}
144
+ }
143
145
  }
144
146
  }
145
147
  viewController.removeView(child)
@@ -171,13 +173,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
171
173
  cleanupScreenEventObserver()
172
174
  didInitiallyPresent = false
173
175
 
174
- if (viewController.isPresented) {
175
- viewController.dismissPromise = {
176
- viewController.delegate = null
177
- }
178
- viewController.dismiss()
179
- } else {
180
- viewController.delegate = null
176
+ viewController.dismissPromise = { viewController.delegate = null }
177
+
178
+ if (viewController.isPresented && !viewController.isBeingDismissed) {
179
+ viewController.dismiss(animated = false)
181
180
  }
182
181
  }
183
182
 
@@ -344,7 +343,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
344
343
  viewController.coordinatorLayout?.let { rootContainerView?.addView(it) }
345
344
 
346
345
  // Register with observer to track sheet stack hierarchy
347
- viewController.parentSheetView = TrueSheetStackManager.onSheetWillPresent(this, detentIndex)
346
+ viewController.parentSheetView = TrueSheetStackManager.registerSheet(this)
348
347
  }
349
348
  viewController.presentPromise = promiseCallback
350
349
  viewController.present(detentIndex, animated)
@@ -352,30 +351,26 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
352
351
 
353
352
  @UiThread
354
353
  fun dismiss(animated: Boolean = true, promiseCallback: () -> Unit) {
355
- // iOS-like behavior: calling dismiss on a presenting controller dismisses
356
- // its presented controller (and everything above it), but NOT itself.
357
- // See: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621505-dismiss
358
- val sheetsAbove = TrueSheetStackManager.getSheetsAbove(this)
359
- if (sheetsAbove.isNotEmpty()) {
360
- for (sheet in sheetsAbove) {
361
- sheet.viewController.dismiss(animated)
362
- }
363
- promiseCallback()
364
- return
365
- }
354
+ // Dismiss all sheets above first
355
+ dismissStack(animated) {}
366
356
 
357
+ // Then dismiss itself
367
358
  viewController.dismissPromise = promiseCallback
368
359
  viewController.dismiss(animated)
369
360
  }
370
361
 
371
362
  @UiThread
372
- fun dismissAll(animated: Boolean = true, promiseCallback: () -> Unit) {
373
- // Dismiss all sheets above first
374
- dismiss(animated) {}
363
+ fun dismissStack(animated: Boolean = true, promiseCallback: () -> Unit) {
364
+ val sheetsAbove = TrueSheetStackManager.getSheetsAbove(this)
365
+ if (sheetsAbove.isNotEmpty()) {
366
+ // Create snapshot only for topmost sheet (first in reversed list)
367
+ sheetsAbove.firstOrNull()?.viewController?.createSheetSnapshot()
375
368
 
376
- // Then dismiss itself
377
- viewController.dismissPromise = promiseCallback
378
- viewController.dismiss(animated)
369
+ for (sheet in sheetsAbove) {
370
+ sheet.viewController.dismiss(animated)
371
+ }
372
+ }
373
+ promiseCallback()
379
374
  }
380
375
 
381
376
  @UiThread
@@ -397,7 +392,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
397
392
  if (viewController.containerView == null) return@post
398
393
 
399
394
  viewController.setupSheetDetentsForSizeChange()
400
- TrueSheetStackManager.onSheetSizeChanged(this)
395
+ TrueSheetStackManager.updateParentTranslation(this)
401
396
  }
402
397
  }
403
398
 
@@ -451,6 +446,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
451
446
  // ==================== TrueSheetViewControllerDelegate ====================
452
447
 
453
448
  override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
449
+ // Update parent sheet translation now that content is measured
450
+ TrueSheetStackManager.updateParentTranslation(this)
451
+
454
452
  val surfaceId = UIManagerHelper.getSurfaceId(this)
455
453
  eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position, detent))
456
454
  }
@@ -477,7 +475,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
477
475
  val surfaceId = UIManagerHelper.getSurfaceId(this)
478
476
  eventDispatcher?.dispatchEvent(DidDismissEvent(surfaceId, id))
479
477
 
480
- TrueSheetStackManager.onSheetDidDismiss(this, hadParent)
478
+ TrueSheetStackManager.unregisterSheet(this, hadParent)
481
479
  }
482
480
 
483
481
  override fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float) {
@@ -131,7 +131,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
131
131
  private set
132
132
 
133
133
  private var interactionState: InteractionState = InteractionState.Idle
134
- private var isDismissing = false
134
+ internal var isBeingDismissed = false
135
+ private set
135
136
  var wasHiddenByScreen = false
136
137
  private var shouldAnimatePresent = false
137
138
  private var isPresentAnimating = false
@@ -340,7 +341,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
340
341
  sheetView = null
341
342
 
342
343
  interactionState = InteractionState.Idle
343
- isDismissing = false
344
+ isBeingDismissed = false
344
345
  isPresented = false
345
346
  isSheetVisible = false
346
347
  wasHiddenByScreen = false
@@ -479,8 +480,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
479
480
 
480
481
  private fun handleStateChanged(sheetView: View, newState: Int) {
481
482
  if (newState == BottomSheetBehavior.STATE_HIDDEN) {
482
- if (isDismissing) return
483
- isDismissing = true
483
+ if (isBeingDismissed) return
484
+ isBeingDismissed = true
484
485
  dismissKeyboard()
485
486
  emitWillDismissEvents()
486
487
  finishDismiss()
@@ -502,7 +503,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
502
503
 
503
504
  private fun handleSlide(sheetView: View, slideOffset: Float) {
504
505
  // Skip during dismiss animation
505
- if (isDismissing) return
506
+ if (isBeingDismissed) return
506
507
 
507
508
  val behavior = behavior ?: return
508
509
 
@@ -705,9 +706,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
705
706
  }
706
707
 
707
708
  fun dismiss(animated: Boolean = true) {
708
- if (isDismissing) return
709
+ if (isBeingDismissed) return
709
710
 
710
- isDismissing = true
711
+ isBeingDismissed = true
711
712
  dismissKeyboard()
712
713
  emitWillDismissEvents()
713
714
 
@@ -894,7 +895,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
894
895
  if (!dimmed) return
895
896
  if (contentHeight == 0) return
896
897
 
897
- val keyboardOffset = if (isDismissing) 0 else currentKeyboardInset
898
+ val keyboardOffset = if (isBeingDismissed) 0 else currentKeyboardInset
898
899
  val top = (sheetTop ?: sheetView?.top ?: return) + keyboardOffset
899
900
 
900
901
  if (animated) {
@@ -981,7 +982,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
981
982
  if (!shouldHandleKeyboard(checkFocus = false)) return
982
983
 
983
984
  setupSheetDetents()
984
- if (!isDismissing && detentIndexBeforeKeyboard >= 0) {
985
+ if (!isBeingDismissed && detentIndexBeforeKeyboard >= 0) {
985
986
  setStateForDetentIndex(detentIndexBeforeKeyboard)
986
987
  }
987
988
  }
@@ -31,18 +31,14 @@ object TrueSheetStackManager {
31
31
  }
32
32
 
33
33
  /**
34
- * Called when a sheet is about to be presented.
35
- * Returns the visible parent sheet to stack on, or null if none.
34
+ * Registers a sheet in the stack and returns its parent sheet if any.
36
35
  * Only returns a parent if it's in the same container (e.g., same Screen).
37
36
  */
38
37
  @JvmStatic
39
- fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int): TrueSheetView? {
38
+ fun registerSheet(sheetView: TrueSheetView): TrueSheetView? {
40
39
  synchronized(presentedSheetStack) {
41
40
  val parentSheet = findTopmostSheet()?.takeIf { it.rootContainerView == sheetView.rootContainerView }
42
41
 
43
- val childSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(detentIndex)
44
- parentSheet?.updateTranslationForChild(childSheetTop)
45
-
46
42
  if (!presentedSheetStack.contains(sheetView)) {
47
43
  presentedSheetStack.add(sheetView)
48
44
  }
@@ -52,11 +48,10 @@ object TrueSheetStackManager {
52
48
  }
53
49
 
54
50
  /**
55
- * Called when a sheet has been dismissed.
56
- * Resets parent sheet translation if this sheet was stacked on it.
51
+ * Unregisters a sheet from the stack and resets parent translation if needed.
57
52
  */
58
53
  @JvmStatic
59
- fun onSheetDidDismiss(sheetView: TrueSheetView, hadParent: Boolean) {
54
+ fun unregisterSheet(sheetView: TrueSheetView, hadParent: Boolean) {
60
55
  synchronized(presentedSheetStack) {
61
56
  presentedSheetStack.remove(sheetView)
62
57
  if (hadParent) {
@@ -66,12 +61,11 @@ object TrueSheetStackManager {
66
61
  }
67
62
 
68
63
  /**
69
- * Called when a presented sheet's size changes (e.g., after setupSheetDetents).
70
- * Updates parent sheet translations to match the new sheet position.
64
+ * Updates parent sheet translation based on the child sheet's position.
71
65
  * Only affects parent sheets in the same container.
72
66
  */
73
67
  @JvmStatic
74
- fun onSheetSizeChanged(sheetView: TrueSheetView) {
68
+ fun updateParentTranslation(sheetView: TrueSheetView) {
75
69
  synchronized(presentedSheetStack) {
76
70
  val index = presentedSheetStack.indexOf(sheetView)
77
71
  val parentSheet = getParentSheetAt(index, sheetView.rootContainerView) ?: return
@@ -138,9 +138,15 @@ using namespace facebook::react;
138
138
  CGFloat newHeight = containerView.bounds.size.height - scrollViewFrameInContainer.origin.y;
139
139
 
140
140
  if (newHeight > 0) {
141
+ // Preserve contentOffset before changing frame to prevent scroll jump
142
+ CGPoint savedContentOffset = _pinnedScrollView.scrollView.contentOffset;
143
+
141
144
  CGRect frame = _pinnedScrollView.frame;
142
145
  frame.size.height = newHeight;
143
146
  _pinnedScrollView.frame = frame;
147
+
148
+ // Restore contentOffset after frame change
149
+ _pinnedScrollView.scrollView.contentOffset = savedContentOffset;
144
150
  }
145
151
  }
146
152
 
@@ -93,6 +93,30 @@ RCT_EXPORT_MODULE(TrueSheetModule)
93
93
  });
94
94
  }
95
95
 
96
+ - (void)dismissStackByRef:(double)viewTag
97
+ animated:(BOOL)animated
98
+ resolve:(RCTPromiseResolveBlock)resolve
99
+ reject:(RCTPromiseRejectBlock)reject {
100
+ RCTExecuteOnMainQueue(^{
101
+ TrueSheetView *trueSheetView = [TrueSheetModule getTrueSheetViewByTag:@((NSInteger)viewTag)];
102
+
103
+ if (!trueSheetView) {
104
+ reject(@"SHEET_NOT_FOUND", [NSString stringWithFormat:@"No sheet found with tag %d", (int)viewTag], nil);
105
+ return;
106
+ }
107
+
108
+ [trueSheetView
109
+ dismissStackAnimated:animated
110
+ completion:^(BOOL success, NSError *_Nullable error) {
111
+ if (success) {
112
+ resolve(nil);
113
+ } else {
114
+ reject(@"DISMISS_FAILED", error.localizedDescription ?: @"Failed to dismiss stack", error);
115
+ }
116
+ }];
117
+ });
118
+ }
119
+
96
120
  - (void)resizeByRef:(double)viewTag
97
121
  index:(double)index
98
122
  resolve:(RCTPromiseResolveBlock)resolve
@@ -141,15 +165,15 @@ RCT_EXPORT_MODULE(TrueSheetModule)
141
165
  return;
142
166
  }
143
167
 
144
- [rootSheet
145
- dismissAllAnimated:animated
146
- completion:^(BOOL success, NSError *_Nullable error) {
147
- if (success) {
148
- resolve(nil);
149
- } else {
150
- reject(@"DISMISS_FAILED", error.localizedDescription ?: @"Failed to dismiss sheets", error);
151
- }
152
- }];
168
+ [rootSheet emitDismissedPosition];
169
+ [rootSheet dismissAnimated:animated
170
+ completion:^(BOOL success, NSError *_Nullable error) {
171
+ if (success) {
172
+ resolve(nil);
173
+ } else {
174
+ reject(@"DISMISS_FAILED", error.localizedDescription ?: @"Failed to dismiss sheets", error);
175
+ }
176
+ }];
153
177
  }
154
178
  });
155
179
  }
@@ -32,9 +32,11 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
32
32
 
33
33
  - (void)dismissAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion;
34
34
 
35
+ - (void)emitDismissedPosition;
36
+
35
37
  - (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion;
36
38
 
37
- - (void)dismissAllAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion;
39
+ - (void)dismissStackAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion;
38
40
 
39
41
  @end
40
42
 
@@ -111,7 +111,7 @@ using namespace facebook::react;
111
111
  UIViewController *vc = [self findPresentingViewController];
112
112
 
113
113
  // Only present if the view controller is in the same window and not being dismissed
114
- if (vc && vc.view.window == self.window && !vc.isBeingDismissed) {
114
+ if (vc && vc.view.window == self.window && !_controller.isBeingDismissed) {
115
115
  _didInitiallyPresent = YES;
116
116
  [self presentAtIndex:_initialDetentIndex animated:_initialDetentAnimated completion:nil];
117
117
  } else {
@@ -407,12 +407,7 @@ using namespace facebook::react;
407
407
  - (void)presentAtIndex:(NSInteger)index
408
408
  animated:(BOOL)animated
409
409
  completion:(nullable TrueSheetCompletionBlock)completion {
410
- if (_controller.isBeingPresented) {
411
- RCTLogWarn(@"TrueSheet: sheet is being presented. Wait for it to transition before presenting again.");
412
- return;
413
- }
414
-
415
- if (_controller.isPresented) {
410
+ if (_controller.isBeingPresented || _controller.isPresented) {
416
411
  RCTLogWarn(@"TrueSheet: sheet is already presented. Use resize() to change detent.");
417
412
  if (completion) {
418
413
  completion(YES, nil);
@@ -420,6 +415,9 @@ using namespace facebook::react;
420
415
  return;
421
416
  }
422
417
 
418
+ // Reset navigation dismiss flag when presenting (handles view recycling edge cases)
419
+ _dismissedByNavigation = NO;
420
+
423
421
  UIViewController *presentingViewController = [self findPresentingViewController];
424
422
  if (!presentingViewController) {
425
423
  NSError *error = [NSError errorWithDomain:@"com.lodev09.TrueSheet"
@@ -446,27 +444,6 @@ using namespace facebook::react;
446
444
  }];
447
445
  }
448
446
 
449
- - (void)dismissAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion {
450
- if (_controller.isBeingDismissed) {
451
- RCTLogWarn(@"TrueSheet: sheet is being dismissed. No need to dismiss it again.");
452
- return;
453
- }
454
-
455
- if (!_controller.isPresented) {
456
- if (completion) {
457
- completion(YES, nil);
458
- }
459
- return;
460
- }
461
-
462
- [_controller dismissViewControllerAnimated:animated
463
- completion:^{
464
- if (completion) {
465
- completion(YES, nil);
466
- }
467
- }];
468
- }
469
-
470
447
  - (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion {
471
448
  if (!_controller.isPresented) {
472
449
  RCTLogWarn(@"TrueSheet: Cannot resize. Sheet is not presented.");
@@ -489,16 +466,20 @@ using namespace facebook::react;
489
466
  return _controller;
490
467
  }
491
468
 
492
- - (void)dismissAllAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion {
493
- if (!_controller.isPresented) {
469
+ - (void)emitDismissedPosition {
470
+ [self viewControllerDidChangePosition:-1 position:_controller.screenHeight detent:0 realtime:NO];
471
+ }
472
+
473
+ - (void)dismissAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion {
474
+ if (_controller.isBeingDismissed || !_controller.isPresented) {
475
+ RCTLogWarn(@"TrueSheet: sheet is already dismissed. No need to dismiss it again.");
476
+
494
477
  if (completion) {
495
478
  completion(YES, nil);
496
479
  }
497
480
  return;
498
481
  }
499
482
 
500
- [self viewControllerDidChangePosition:-1 position:_controller.screenHeight detent:0 realtime:NO];
501
-
502
483
  // Dismiss from the presenting view controller to dismiss this sheet and all its children
503
484
  UIViewController *presenter = _controller.presentingViewController;
504
485
  [presenter dismissViewControllerAnimated:animated
@@ -509,6 +490,33 @@ using namespace facebook::react;
509
490
  }];
510
491
  }
511
492
 
493
+ - (void)dismissStackAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion {
494
+ if (_controller.isBeingDismissed || !_controller.isPresented) {
495
+ RCTLogWarn(@"TrueSheet: sheet is already dismissed. No need to dismiss it again.");
496
+
497
+ if (completion) {
498
+ completion(YES, nil);
499
+ }
500
+ return;
501
+ }
502
+
503
+ // Only dismiss presented children, not this sheet itself
504
+ if (!_controller.presentedViewController) {
505
+ if (completion) {
506
+ completion(YES, nil);
507
+ }
508
+ return;
509
+ }
510
+
511
+ // Calling dismiss on _controller dismisses all VCs presented on top of it, but keeps _controller presented
512
+ [_controller dismissViewControllerAnimated:animated
513
+ completion:^{
514
+ if (completion) {
515
+ completion(YES, nil);
516
+ }
517
+ }];
518
+ }
519
+
512
520
  #pragma mark - TrueSheetContainerViewDelegate
513
521
 
514
522
  /**
@@ -627,7 +635,7 @@ using namespace facebook::react;
627
635
  - (void)presenterScreenWillDisappear {
628
636
  if (_controller.isPresented && !_controller.isBeingDismissed) {
629
637
  _dismissedByNavigation = YES;
630
- [self dismissAllAnimated:YES completion:nil];
638
+ [self dismissAnimated:YES completion:nil];
631
639
  }
632
640
  }
633
641
 
@@ -69,6 +69,7 @@ NS_ASSUME_NONNULL_BEGIN
69
69
  @property (nonatomic, assign) BOOL isPresented;
70
70
  @property (nonatomic, assign) NSInteger activeDetentIndex;
71
71
  @property (nonatomic, readonly) BOOL isTopmostPresentedController;
72
+
72
73
  @property (nonatomic, readonly) CGFloat screenHeight;
73
74
 
74
75
  - (void)applyActiveDetent;
@@ -222,12 +222,8 @@
222
222
  }
223
223
  }
224
224
 
225
- - (BOOL)isDismissing {
226
- return self.presentingViewController == nil || self.isBeingDismissed;
227
- }
228
-
229
225
  - (void)emitWillDismissEvents {
230
- if (self.isDismissing && !_isWillDismissEmitted) {
226
+ if (self.isBeingDismissed && !_isWillDismissEmitted) {
231
227
  _isWillDismissEmitted = YES;
232
228
 
233
229
  [self.delegate viewControllerWillBlur];
@@ -237,7 +233,7 @@
237
233
  }
238
234
 
239
235
  - (void)emitDidDismissEvents {
240
- if (self.isDismissing) {
236
+ if (self.isBeingDismissed) {
241
237
  _isPresented = NO;
242
238
  _isWillDismissEmitted = NO;
243
239
 
@@ -398,12 +394,12 @@
398
394
  CGRect dismissedFrame = CGRectMake(0, self.screenHeight, 0, 0);
399
395
  CGRect presentedFrame = CGRectMake(0, self.currentPosition, 0, 0);
400
396
 
401
- _transitionFakeView.frame = self.isDismissing ? presentedFrame : dismissedFrame;
397
+ _transitionFakeView.frame = self.isBeingDismissed ? presentedFrame : dismissedFrame;
402
398
  [self storeResolvedPositionForIndex:self.currentDetentIndex];
403
399
 
404
400
  auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
405
401
  [[context containerView] addSubview:self->_transitionFakeView];
406
- self->_transitionFakeView.frame = self.isDismissing ? dismissedFrame : presentedFrame;
402
+ self->_transitionFakeView.frame = self.isBeingDismissed ? dismissedFrame : presentedFrame;
407
403
 
408
404
  self->_transitioningTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleTransitionTracker)];
409
405
  [self->_transitioningTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
@@ -19,14 +19,14 @@ using namespace facebook::react;
19
19
  @implementation RNScreensEventObserver {
20
20
  std::weak_ptr<const EventDispatcher> _eventDispatcher;
21
21
  std::shared_ptr<const EventListener> _eventListener;
22
- NSInteger _presenterScreenTag;
22
+ NSMutableSet<NSNumber *> *_screenTags;
23
23
  __weak UIViewController *_presenterScreenController;
24
24
  BOOL _dismissedByNavigation;
25
25
  }
26
26
 
27
27
  - (instancetype)init {
28
28
  if (self = [super init]) {
29
- _presenterScreenTag = 0;
29
+ _screenTags = [NSMutableSet new];
30
30
  _presenterScreenController = nil;
31
31
  }
32
32
  return self;
@@ -61,7 +61,7 @@ using namespace facebook::react;
61
61
  [strongSelf.delegate presenterScreenWillDisappear];
62
62
  }
63
63
  } else if (event.type == "topWillAppear") {
64
- if (screenTag == strongSelf->_presenterScreenTag && strongSelf->_dismissedByNavigation) {
64
+ if ([strongSelf->_screenTags containsObject:@(screenTag)] && strongSelf->_dismissedByNavigation) {
65
65
  strongSelf->_dismissedByNavigation = NO;
66
66
  [strongSelf.delegate presenterScreenWillAppear];
67
67
  }
@@ -85,33 +85,41 @@ using namespace facebook::react;
85
85
  }
86
86
 
87
87
  - (void)capturePresenterScreenFromView:(UIView *)view {
88
- _presenterScreenTag = 0;
88
+ [_screenTags removeAllObjects];
89
89
  _presenterScreenController = nil;
90
90
 
91
91
  for (UIView *current = view.superview; current; current = current.superview) {
92
- NSString *className = NSStringFromClass([current class]);
93
-
94
- if ([className isEqualToString:@"RNSScreenView"]) {
95
- _presenterScreenTag = current.tag;
96
- for (UIResponder *r = current.nextResponder; r; r = r.nextResponder) {
97
- if ([r isKindOfClass:[UIViewController class]]) {
98
- _presenterScreenController = (UIViewController *)r;
99
- break;
92
+ if ([NSStringFromClass([current class]) isEqualToString:@"RNSScreenView"]) {
93
+ [_screenTags addObject:@(current.tag)];
94
+
95
+ // Capture the view controller from the first (immediate presenter) screen
96
+ if (!_presenterScreenController) {
97
+ for (UIResponder *r = current.nextResponder; r; r = r.nextResponder) {
98
+ if ([r isKindOfClass:[UIViewController class]]) {
99
+ _presenterScreenController = (UIViewController *)r;
100
+ break;
101
+ }
100
102
  }
101
103
  }
102
- break;
103
104
  }
104
105
  }
105
106
  }
106
107
 
107
108
  - (BOOL)shouldDismissForScreenTag:(NSInteger)screenTag {
108
- if (_presenterScreenTag != screenTag) {
109
+ if (![_screenTags containsObject:@(screenTag)]) {
109
110
  return NO;
110
111
  }
111
112
 
113
+ UINavigationController *navController = _presenterScreenController.navigationController;
114
+
115
+ // If nav controller is nil or being dismissed, dismiss the sheet
116
+ if (!navController || navController.isBeingDismissed) {
117
+ return YES;
118
+ }
119
+
112
120
  // Skip if screen is still top of nav stack (e.g. modal dismiss - sheet dismisses naturally with modal)
113
121
  // Dismiss if a new screen was pushed or popped
114
- return _presenterScreenController.navigationController.topViewController != _presenterScreenController;
122
+ return navController.topViewController != _presenterScreenController;
115
123
  }
116
124
 
117
125
  @end