@lodev09/react-native-true-sheet 4.0.0-beta.7 → 4.0.0-beta.8

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.
package/README.md CHANGED
@@ -32,6 +32,7 @@ The true native bottom sheet experience for your React Native Apps. 💩
32
32
  * 📜 **`scrollableRef`** - Pass a `ScrollView`/`FlatList` ref to wire nested scrolling, keyboard insets, and `auto` detent sizing
33
33
  * 🦶 **Relative footers** - Footers take up space below the content by default
34
34
  * 🧭 **Expo Router** - First-class support via the `Sheet` layout from `/navigation/expo-router`
35
+ * 🍞 **`TrueSheetOverlay`** - Render toasts, dialogs, and other [overlays above your sheets](https://sheet.lodev09.com/guides/overlays) — no more `FullWindowOverlay`/`Modal` workarounds
35
36
 
36
37
  ```sh
37
38
  npx expo install @lodev09/react-native-true-sheet@beta
@@ -17,7 +17,28 @@ buildscript {
17
17
 
18
18
 
19
19
  apply plugin: "com.android.library"
20
- apply plugin: "kotlin-android"
20
+ // Android Gradle Plugin 9 ships built-in Kotlin support and applies the Kotlin
21
+ // plugin itself. Applying it a second time fails the configuration phase with
22
+ // "Cannot add extension with name 'kotlin'". Apply it only when AGP is not
23
+ // providing Kotlin: AGP 8 and older, or AGP 9 with android.builtInKotlin=false.
24
+ def shouldApplyKotlinPlugin() {
25
+ def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
26
+ if (agpMajor <= 8) {
27
+ return true
28
+ }
29
+ // AGP 10 removes the opt-out: built-in Kotlin is always active there.
30
+ if (agpMajor >= 10) {
31
+ return false
32
+ }
33
+ def propertyVal = providers.gradleProperty("android.builtInKotlin").orNull
34
+ def builtInKotlinEnabled = propertyVal != null ? propertyVal.toBoolean() : true
35
+ return !builtInKotlinEnabled
36
+ }
37
+
38
+ if (shouldApplyKotlinPlugin()) {
39
+ apply plugin: "kotlin-android"
40
+ }
41
+
21
42
  apply plugin: "com.facebook.react"
22
43
 
23
44
  def getExtOrIntegerDefault(name) {
@@ -27,16 +27,11 @@ NS_ASSUME_NONNULL_BEGIN
27
27
 
28
28
  /**
29
29
  * Register a sheet component view with its React tag
30
- * Called automatically by TrueSheetView during initialization
30
+ * Called automatically by TrueSheetView when it attaches to a window.
31
+ * The registry holds views weakly, so no unregistration is needed.
31
32
  */
32
33
  + (void)registerView:(TrueSheetView *)view withTag:(NSNumber *)tag;
33
34
 
34
- /**
35
- * Unregister a sheet component view
36
- * Called automatically by TrueSheetView during dealloc
37
- */
38
- + (void)unregisterViewWithTag:(NSNumber *)tag;
39
-
40
35
  @end
41
36
 
42
37
  NS_ASSUME_NONNULL_END
@@ -18,8 +18,9 @@
18
18
 
19
19
  #import <TrueSheetSpec/TrueSheetSpec.h>
20
20
 
21
- // Static registry to store view references by tag
22
- static NSMutableDictionary<NSNumber *, TrueSheetView *> *viewRegistry;
21
+ // Static registry of live hosts by tag — weak so an unmounted host deallocates
22
+ // (RN zeroes the tag before teardown, so hosts can't unregister themselves)
23
+ static NSMapTable<NSNumber *, TrueSheetView *> *viewRegistry;
23
24
 
24
25
  @interface TrueSheetModule () <NativeTrueSheetModuleSpec>
25
26
  @end
@@ -30,7 +31,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
30
31
 
31
32
  + (void)initialize {
32
33
  if (self == [TrueSheetModule class]) {
33
- viewRegistry = [NSMutableDictionary new];
34
+ viewRegistry = [NSMapTable strongToWeakObjectsMapTable];
34
35
  }
35
36
  }
36
37
 
@@ -151,7 +152,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
151
152
  // Find the root presented sheet (one without a parent TrueSheet)
152
153
  TrueSheetView *rootSheet = nil;
153
154
 
154
- for (TrueSheetView *view in viewRegistry.allValues) {
155
+ for (TrueSheetView *view in viewRegistry.objectEnumerator) {
155
156
  if (!view.viewController.isPresented) {
156
157
  continue;
157
158
  }
@@ -191,7 +192,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
191
192
  }
192
193
 
193
194
  @synchronized(viewRegistry) {
194
- return viewRegistry[reactTag];
195
+ return [viewRegistry objectForKey:reactTag];
195
196
  }
196
197
  }
197
198
 
@@ -201,17 +202,7 @@ RCT_EXPORT_MODULE(TrueSheetModule)
201
202
  }
202
203
 
203
204
  @synchronized(viewRegistry) {
204
- viewRegistry[tag] = view;
205
- }
206
- }
207
-
208
- + (void)unregisterViewWithTag:(NSNumber *)tag {
209
- if (!tag) {
210
- return;
211
- }
212
-
213
- @synchronized(viewRegistry) {
214
- [viewRegistry removeObjectForKey:tag];
205
+ [viewRegistry setObject:view forKey:tag];
215
206
  }
216
207
  }
217
208
 
@@ -28,6 +28,11 @@ static NSHashTable<TrueSheetOverlayView *> *gOverlayViews;
28
28
 
29
29
  @implementation TrueSheetOverlayView {
30
30
  TrueSheetOverlayContainerView *_containerView;
31
+ // Owns `_containerView` so it has a view controller in its responder chain. The
32
+ // container is a sibling of `rootViewController.view`, and a window's responder chain
33
+ // skips its root view controller — without an owner, `-[UIView reactViewController]`
34
+ // finds nothing and content that installs a view controller cannot attach.
35
+ UIViewController *_containerController;
31
36
  RCTSurfaceTouchHandler *_touchHandler;
32
37
  TrueSheetOverlayViewShadowNode::ConcreteState::Shared _state;
33
38
  CGSize _lastStateSize;
@@ -56,6 +61,11 @@ static NSHashTable<TrueSheetOverlayView *> *gOverlayViews;
56
61
  _containerView = [[TrueSheetOverlayContainerView alloc] initWithFrame:CGRectZero];
57
62
  _containerView.delegate = self;
58
63
 
64
+ // Assigning the container as its view is what puts the controller in the responder
65
+ // chain; the containment below is for lifecycle only.
66
+ _containerController = [UIViewController new];
67
+ _containerController.view = _containerView;
68
+
59
69
  _touchHandler = [[RCTSurfaceTouchHandler alloc] init];
60
70
  [_touchHandler attachToView:_containerView];
61
71
 
@@ -79,8 +89,16 @@ static NSHashTable<TrueSheetOverlayView *> *gOverlayViews;
79
89
  if (window) {
80
90
  _containerView.frame = window.bounds;
81
91
  [window addSubview:_containerView];
92
+
93
+ UIViewController *root = window.rootViewController;
94
+ if (root != nil && _containerController.parentViewController != root) {
95
+ [root addChildViewController:_containerController];
96
+ [_containerController didMoveToParentViewController:root];
97
+ }
82
98
  } else {
99
+ [_containerController willMoveToParentViewController:nil];
83
100
  [_containerView removeFromSuperview];
101
+ [_containerController removeFromParentViewController];
84
102
  }
85
103
  }
86
104
 
@@ -126,7 +144,9 @@ static NSHashTable<TrueSheetOverlayView *> *gOverlayViews;
126
144
  - (void)prepareForRecycle {
127
145
  [super prepareForRecycle];
128
146
 
147
+ [_containerController willMoveToParentViewController:nil];
129
148
  [_containerView removeFromSuperview];
149
+ [_containerController removeFromParentViewController];
130
150
  _lastStateSize = CGSizeZero;
131
151
  _state.reset();
132
152
  self.hidden = YES;
@@ -125,13 +125,38 @@ using namespace facebook::react;
125
125
 
126
126
  UIViewController *vc = [self findPresentingViewController];
127
127
 
128
- // Only present if the view controller is in the same window and not being dismissed
129
- if (!vc || vc.view.window != self.window || _controller.isBeingDismissed) {
128
+ // Only present if the view controller is in the same window
129
+ if (!vc || vc.view.window != self.window) {
130
130
  // Animate next time when sheet finally moves to the correct window
131
131
  _initialDetentAnimated = YES;
132
132
  return;
133
133
  }
134
134
 
135
+ // A sheet still animating out of the presenter (e.g. our own controller,
136
+ // recycled mid-dismissal) blocks presentation — retry once its transition ends.
137
+ UIViewController *dismissing = vc.presentedViewController;
138
+ if (dismissing && dismissing.isBeingDismissed) {
139
+ _initialDetentAnimated = YES;
140
+
141
+ __weak __typeof(self) weakSelf = self;
142
+ void (^retry)(void) = ^{
143
+ dispatch_async(dispatch_get_main_queue(), ^{
144
+ [weakSelf presentInitialIfNeeded];
145
+ });
146
+ };
147
+
148
+ id<UIViewControllerTransitionCoordinator> coordinator = dismissing.transitionCoordinator;
149
+ if (coordinator) {
150
+ [coordinator animateAlongsideTransition:nil
151
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
152
+ retry();
153
+ }];
154
+ } else {
155
+ retry();
156
+ }
157
+ return;
158
+ }
159
+
135
160
  _didInitiallyPresent = YES;
136
161
 
137
162
  // Deferred a runloop turn so sibling mounts from the current transaction
@@ -176,8 +201,6 @@ using namespace facebook::react;
176
201
 
177
202
  [_snapshotView removeFromSuperview];
178
203
  _snapshotView = nil;
179
-
180
- [TrueSheetModule unregisterViewWithTag:@(self.tag)];
181
204
  }
182
205
 
183
206
  #pragma mark - RCTComponentViewProtocol
@@ -383,7 +406,38 @@ using namespace facebook::react;
383
406
  - (void)prepareForRecycle {
384
407
  [super prepareForRecycle];
385
408
 
386
- [TrueSheetModule unregisterViewWithTag:@(self.tag)];
409
+ // Pooled mid-dismissal: the next mount can reuse this host — and its
410
+ // controller — while the previous sheet is still animating out. Detach until
411
+ // that transition ends so its tail (didBlur, didDismiss, position) can't reach
412
+ // the new mount, then hand the controller back and let auto-present retry.
413
+ if (_controller.isBeingDismissed) {
414
+ _controller.delegate = nil;
415
+
416
+ __weak __typeof(self) weakSelf = self;
417
+ void (^reattach)(void) = ^{
418
+ dispatch_async(dispatch_get_main_queue(), ^{
419
+ __typeof(self) strongSelf = weakSelf;
420
+ if (!strongSelf)
421
+ return;
422
+
423
+ strongSelf->_controller.delegate = strongSelf;
424
+ strongSelf->_controller.activeDetentIndex = -1;
425
+ if (strongSelf.window) {
426
+ [strongSelf presentInitialIfNeeded];
427
+ }
428
+ });
429
+ };
430
+
431
+ id<UIViewControllerTransitionCoordinator> coordinator = _controller.transitionCoordinator;
432
+ if (coordinator) {
433
+ [coordinator animateAlongsideTransition:nil
434
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
435
+ reattach();
436
+ }];
437
+ } else {
438
+ reattach();
439
+ }
440
+ }
387
441
 
388
442
  _lastStateSize = CGSizeZero;
389
443
  _didInitiallyPresent = NO;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "4.0.0-beta.7",
3
+ "version": "4.0.0-beta.8",
4
4
  "description": "The true native bottom sheet experience for your React Native Apps.",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",