@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 +1 -0
- package/android/build.gradle +22 -1
- package/ios/TrueSheetModule.h +2 -7
- package/ios/TrueSheetModule.mm +7 -16
- package/ios/TrueSheetOverlayView.mm +20 -0
- package/ios/TrueSheetView.mm +59 -5
- package/package.json +1 -1
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
|
package/android/build.gradle
CHANGED
|
@@ -17,7 +17,28 @@ buildscript {
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
apply plugin: "com.android.library"
|
|
20
|
-
|
|
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) {
|
package/ios/TrueSheetModule.h
CHANGED
|
@@ -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
|
|
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
|
package/ios/TrueSheetModule.mm
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
#import <TrueSheetSpec/TrueSheetSpec.h>
|
|
20
20
|
|
|
21
|
-
// Static registry
|
|
22
|
-
|
|
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 = [
|
|
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.
|
|
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
|
|
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
|
|
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;
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -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
|
|
129
|
-
if (!vc || vc.view.window != self.window
|
|
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
|
-
|
|
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