@lodev09/react-native-true-sheet 4.0.0-beta.2 → 4.0.0-beta.4
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/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +28 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +206 -149
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +6 -17
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +73 -13
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +101 -18
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +25 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +3 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +14 -24
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +0 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +50 -4
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +7 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +11 -3
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +7 -0
- package/ios/TrueSheetContainerView.h +18 -5
- package/ios/TrueSheetContainerView.mm +10 -6
- package/ios/TrueSheetContentView.h +31 -10
- package/ios/TrueSheetContentView.mm +137 -162
- package/ios/TrueSheetFooterView.h +6 -0
- package/ios/TrueSheetFooterView.mm +48 -2
- package/ios/TrueSheetView.mm +95 -21
- package/ios/TrueSheetViewController.h +9 -0
- package/ios/TrueSheetViewController.mm +108 -25
- package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
- package/lib/module/TrueSheet.js +77 -11
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +26 -24
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +4 -0
- package/lib/typescript/src/TrueSheet.d.ts +4 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +50 -1
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +3 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +78 -9
- package/src/TrueSheet.types.ts +53 -1
- package/src/TrueSheet.web.tsx +37 -23
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetViewNativeComponent.ts +4 -0
- package/src/navigation/types.ts +1 -0
package/ios/TrueSheetView.mm
CHANGED
|
@@ -53,6 +53,7 @@ using namespace facebook::react;
|
|
|
53
53
|
NSInteger _initialDetentIndex;
|
|
54
54
|
TrueSheetViewInsetAdjustment _insetAdjustment;
|
|
55
55
|
ScrollableOptions *_scrollableOptions;
|
|
56
|
+
NSInteger _scrollableHandle;
|
|
56
57
|
BOOL _hasAutoDetent;
|
|
57
58
|
BOOL _initialDetentAnimated;
|
|
58
59
|
BOOL _isSheetUpdatePending;
|
|
@@ -110,18 +111,47 @@ using namespace facebook::react;
|
|
|
110
111
|
return;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
[self presentInitialIfNeeded];
|
|
115
|
+
}
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
// JS holds initialDetentIndex back one commit so effect-driven content (e.g. a
|
|
118
|
+
// navigation footer set via setOptions) commits together with it — by the time
|
|
119
|
+
// the prop lands the tree is final and the sheet presents at its final height.
|
|
120
|
+
// Called from didMoveToWindow (re-attach) and finalizeUpdates (the prop
|
|
121
|
+
// arriving after attach, which didMoveToWindow won't re-fire for).
|
|
122
|
+
- (void)presentInitialIfNeeded {
|
|
123
|
+
if (_initialDetentIndex < 0 || _didInitiallyPresent)
|
|
124
|
+
return;
|
|
125
|
+
|
|
126
|
+
UIViewController *vc = [self findPresentingViewController];
|
|
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) {
|
|
130
|
+
// Animate next time when sheet finally moves to the correct window
|
|
131
|
+
_initialDetentAnimated = YES;
|
|
132
|
+
return;
|
|
124
133
|
}
|
|
134
|
+
|
|
135
|
+
_didInitiallyPresent = YES;
|
|
136
|
+
|
|
137
|
+
// Deferred a runloop turn so sibling mounts from the current transaction
|
|
138
|
+
// (e.g. a footer committed alongside the prop) land before measuring.
|
|
139
|
+
__weak __typeof(self) weakSelf = self;
|
|
140
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
141
|
+
__typeof(self) strongSelf = weakSelf;
|
|
142
|
+
if (!strongSelf || strongSelf->_controller.isPresented || strongSelf->_controller.isBeingPresented)
|
|
143
|
+
return;
|
|
144
|
+
|
|
145
|
+
[strongSelf presentAtIndex:strongSelf->_initialDetentIndex
|
|
146
|
+
animated:strongSelf->_initialDetentAnimated
|
|
147
|
+
completion:^(BOOL success, NSError *_Nullable error) {
|
|
148
|
+
// Present can fail if the view detached during this turn —
|
|
149
|
+
// reset so the next attach/prop update retries.
|
|
150
|
+
if (!success) {
|
|
151
|
+
strongSelf->_didInitiallyPresent = NO;
|
|
152
|
+
}
|
|
153
|
+
}];
|
|
154
|
+
});
|
|
125
155
|
}
|
|
126
156
|
|
|
127
157
|
- (void)dealloc {
|
|
@@ -248,17 +278,23 @@ using namespace facebook::react;
|
|
|
248
278
|
_initialDetentIndex = newProps.initialDetentIndex;
|
|
249
279
|
_initialDetentAnimated = newProps.initialDetentAnimated;
|
|
250
280
|
|
|
281
|
+
_scrollableHandle = newProps.scrollableHandle;
|
|
282
|
+
|
|
251
283
|
const auto &scrollableOpts = newProps.scrollableOptions;
|
|
252
284
|
BOOL scrollingExpandsSheet = scrollableOpts.scrollingExpandsSheet;
|
|
285
|
+
BOOL contentInsetAdjustment = scrollableOpts.contentInsetAdjustmentBehavior;
|
|
253
286
|
auto topEdgeEffect = scrollableOpts.topScrollEdgeEffect;
|
|
254
287
|
auto bottomEdgeEffect = scrollableOpts.bottomScrollEdgeEffect;
|
|
255
|
-
BOOL hasScrollableOptions = scrollableOpts.keyboardScrollOffset > 0 ||
|
|
288
|
+
BOOL hasScrollableOptions = scrollableOpts.keyboardScrollOffset > 0 || scrollableOpts.keyboardOffset != 0 ||
|
|
289
|
+
!scrollingExpandsSheet || !contentInsetAdjustment ||
|
|
256
290
|
topEdgeEffect != TrueSheetViewTopScrollEdgeEffect::Hidden ||
|
|
257
291
|
bottomEdgeEffect != TrueSheetViewBottomScrollEdgeEffect::Hidden;
|
|
258
292
|
|
|
259
293
|
if (hasScrollableOptions) {
|
|
260
294
|
ScrollableOptions *options = [[ScrollableOptions alloc] init];
|
|
295
|
+
options.contentInsetAdjustment = contentInsetAdjustment;
|
|
261
296
|
options.keyboardScrollOffset = scrollableOpts.keyboardScrollOffset;
|
|
297
|
+
options.keyboardOffset = scrollableOpts.keyboardOffset;
|
|
262
298
|
options.scrollingExpandsSheet = scrollingExpandsSheet;
|
|
263
299
|
options.topScrollEdgeEffect = topEdgeEffect;
|
|
264
300
|
options.bottomScrollEdgeEffect = bottomEdgeEffect;
|
|
@@ -342,6 +378,9 @@ using namespace facebook::react;
|
|
|
342
378
|
_pendingPropsUpdate = YES;
|
|
343
379
|
} else if (_initialDetentIndex >= 0) {
|
|
344
380
|
_pendingLayoutUpdate = NO;
|
|
381
|
+
if (self.window) {
|
|
382
|
+
[self presentInitialIfNeeded];
|
|
383
|
+
}
|
|
345
384
|
}
|
|
346
385
|
}
|
|
347
386
|
|
|
@@ -403,9 +442,8 @@ using namespace facebook::react;
|
|
|
403
442
|
_controller.headerHeight = @(headerHeight);
|
|
404
443
|
}
|
|
405
444
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
_controller.footerHeight = @(footerHeight);
|
|
445
|
+
if ([_containerView footerHeight] > 0) {
|
|
446
|
+
[self syncFooterMetrics];
|
|
409
447
|
}
|
|
410
448
|
|
|
411
449
|
CGFloat peekContentHeight = [_containerView peekContentHeight];
|
|
@@ -471,7 +509,7 @@ using namespace facebook::react;
|
|
|
471
509
|
[_controller setupSheetSizing];
|
|
472
510
|
[_controller setupSheetProps];
|
|
473
511
|
|
|
474
|
-
//
|
|
512
|
+
// Detect before measuring so a first-time detection doesn't shift sizes mid-animation.
|
|
475
513
|
[self setupScrollable];
|
|
476
514
|
|
|
477
515
|
// Measure synchronously so the presentation starts at the right height —
|
|
@@ -480,11 +518,12 @@ using namespace facebook::react;
|
|
|
480
518
|
if (_containerView) {
|
|
481
519
|
_controller.contentHeight = @([_containerView contentHeight]);
|
|
482
520
|
_controller.headerHeight = @([_containerView headerHeight]);
|
|
483
|
-
|
|
521
|
+
[self syncFooterMetrics];
|
|
484
522
|
_controller.peekContentHeight = @([_containerView peekContentHeight]);
|
|
485
523
|
}
|
|
486
524
|
_pendingSizeChange = NO;
|
|
487
525
|
|
|
526
|
+
[self refreshFooterBottomInset];
|
|
488
527
|
[_controller setupSheetDetents];
|
|
489
528
|
[_controller setupActiveDetentWithIndex:index];
|
|
490
529
|
|
|
@@ -583,6 +622,10 @@ using namespace facebook::react;
|
|
|
583
622
|
* Debounced sheet update to handle rapid content/header size changes.
|
|
584
623
|
*/
|
|
585
624
|
- (void)setupSheetDetentsForSizeChange {
|
|
625
|
+
// Keep the footer's absorbed inset in sync with the latest measured heights
|
|
626
|
+
// even before presenting, so the sheet presents at its final size.
|
|
627
|
+
[self refreshFooterBottomInset];
|
|
628
|
+
|
|
586
629
|
// Retargeting the in-flight presentation makes UIKit snap the whole stack
|
|
587
630
|
// (including the sheet behind) instead of animating. Defer to didPresent —
|
|
588
631
|
// presentAtIndex measured synchronously, so this is usually a no-op.
|
|
@@ -610,6 +653,7 @@ using namespace facebook::react;
|
|
|
610
653
|
// Refresh here (not just on peek size events) since the peek's offset
|
|
611
654
|
// within the content can change without its own size changing.
|
|
612
655
|
self->_controller.peekContentHeight = @([self->_containerView peekContentHeight]);
|
|
656
|
+
[self refreshFooterBottomInset];
|
|
613
657
|
[self->_controller setupSheetDetentsForSizeChange];
|
|
614
658
|
});
|
|
615
659
|
}
|
|
@@ -625,7 +669,7 @@ using namespace facebook::react;
|
|
|
625
669
|
}
|
|
626
670
|
|
|
627
671
|
- (void)containerViewFooterDidChangeSize:(CGSize)newSize {
|
|
628
|
-
|
|
672
|
+
[self syncFooterMetrics];
|
|
629
673
|
[self setupSheetDetentsForSizeChange];
|
|
630
674
|
[_controller setupAccessibilityContainer];
|
|
631
675
|
}
|
|
@@ -634,7 +678,7 @@ using namespace facebook::react;
|
|
|
634
678
|
[self setupSheetDetentsForSizeChange];
|
|
635
679
|
}
|
|
636
680
|
|
|
637
|
-
// When the ScrollView changes (e.g. conditional remount), re-
|
|
681
|
+
// When the ScrollView changes (e.g. conditional remount), re-detect the new ScrollView.
|
|
638
682
|
- (void)containerViewScrollViewDidChange {
|
|
639
683
|
[self setupScrollable];
|
|
640
684
|
}
|
|
@@ -716,6 +760,10 @@ using namespace facebook::react;
|
|
|
716
760
|
[self updateStateWithSize:size];
|
|
717
761
|
}
|
|
718
762
|
|
|
763
|
+
- (void)viewControllerSafeAreaInsetsDidChange {
|
|
764
|
+
[self refreshFooterBottomInset];
|
|
765
|
+
}
|
|
766
|
+
|
|
719
767
|
- (void)viewControllerWillFocus {
|
|
720
768
|
[TrueSheetFocusEvents emitWillFocus:_eventEmitter];
|
|
721
769
|
}
|
|
@@ -781,16 +829,41 @@ using namespace facebook::react;
|
|
|
781
829
|
if (!_containerView)
|
|
782
830
|
return;
|
|
783
831
|
|
|
784
|
-
_containerView.insetAdjustment = _insetAdjustment;
|
|
785
832
|
_containerView.scrollableOptions = _scrollableOptions;
|
|
833
|
+
_containerView.scrollableHandle = _scrollableHandle;
|
|
834
|
+
_containerView.contentInsetAdjustment = (_scrollableOptions ? _scrollableOptions.contentInsetAdjustment : YES) &&
|
|
835
|
+
_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic;
|
|
786
836
|
_containerView.hasAutoDetent = _hasAutoDetent;
|
|
787
|
-
|
|
837
|
+
[self refreshFooterBottomInset];
|
|
788
838
|
[_containerView setupScrollable];
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// footerHeight and appliedFooterBottomInset must stay paired — detent math
|
|
842
|
+
// subtracts exactly the inset baked into the measured footer height (see
|
|
843
|
+
// autoDetentBottomInset/peekDetentBottomInset).
|
|
844
|
+
- (void)syncFooterMetrics {
|
|
845
|
+
_controller.footerHeight = @([_containerView footerHeight]);
|
|
846
|
+
_controller.appliedFooterBottomInset = [_containerView footerAppliedBottomInset];
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Recomputes the inset the footer absorbs and pushes it down. The inset
|
|
850
|
+
// depends on measured heights and detents (see maxNaturalDetentHeight), so
|
|
851
|
+
// this runs on size and detent changes — not just prop updates.
|
|
852
|
+
- (void)refreshFooterBottomInset {
|
|
853
|
+
if (!_containerView)
|
|
854
|
+
return;
|
|
855
|
+
|
|
856
|
+
// appliedFooterBottomInset is NOT set here — it tracks what's actually
|
|
857
|
+
// baked into the footer's measured height (reported with its layout), not
|
|
858
|
+
// what was just pushed. Setting it at push time desyncs the pair and lets
|
|
859
|
+
// a seeded inset read as natural content height (see maxNaturalDetentHeight).
|
|
860
|
+
CGFloat inset = _controller.footerBottomInset;
|
|
861
|
+
_containerView.footerBottomInset = inset;
|
|
789
862
|
|
|
790
863
|
// Publish the inset so a footer set later (e.g. navigation setOptions) is
|
|
791
864
|
// padded on its very first layout instead of resizing the sheet twice.
|
|
792
865
|
if (_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic) {
|
|
793
|
-
TrueSheetInsets::setBottomSafeArea(
|
|
866
|
+
TrueSheetInsets::setBottomSafeArea(inset);
|
|
794
867
|
}
|
|
795
868
|
}
|
|
796
869
|
|
|
@@ -801,6 +874,7 @@ using namespace facebook::react;
|
|
|
801
874
|
if (_pendingDetents) {
|
|
802
875
|
_controller.detents = _pendingDetents;
|
|
803
876
|
_pendingDetents = nil;
|
|
877
|
+
[self refreshFooterBottomInset];
|
|
804
878
|
}
|
|
805
879
|
|
|
806
880
|
UIView *presenterView = _controller.presentingViewController.view;
|
|
@@ -36,6 +36,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
36
36
|
detent:(CGFloat)detent
|
|
37
37
|
realtime:(BOOL)realtime;
|
|
38
38
|
- (void)viewControllerDidChangeSize:(CGSize)size;
|
|
39
|
+
- (void)viewControllerSafeAreaInsetsDidChange;
|
|
39
40
|
- (void)viewControllerWillFocus;
|
|
40
41
|
- (void)viewControllerDidFocus;
|
|
41
42
|
- (void)viewControllerWillBlur;
|
|
@@ -99,6 +100,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
99
100
|
*/
|
|
100
101
|
@property (nonatomic, readonly) CGFloat footerBottomInset;
|
|
101
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The inset currently baked into the footer's measured height, reported
|
|
105
|
+
* alongside its layout (see TrueSheetFooterView.appliedBottomInset). Detent
|
|
106
|
+
* resolution subtracts this exact value so the baked inset is never
|
|
107
|
+
* double-counted.
|
|
108
|
+
*/
|
|
109
|
+
@property (nonatomic, assign) CGFloat appliedFooterBottomInset;
|
|
110
|
+
|
|
102
111
|
- (void)setupAccessibilityContainer;
|
|
103
112
|
- (void)applyActiveDetent;
|
|
104
113
|
- (void)setupActiveDetentWithIndex:(NSInteger)index;
|
|
@@ -70,6 +70,11 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
70
70
|
BOOL _isDragging;
|
|
71
71
|
BOOL _isWillDismissEmitted;
|
|
72
72
|
|
|
73
|
+
// Last bottom safe area observed while presented — reused as the estimate
|
|
74
|
+
// for the next present (beats the float-threshold guess for re-presents).
|
|
75
|
+
BOOL _hasObservedBottomInset;
|
|
76
|
+
CGFloat _observedBottomInset;
|
|
77
|
+
|
|
73
78
|
BOOL _isInteractiveDismiss;
|
|
74
79
|
CGFloat _interactiveStartPosition;
|
|
75
80
|
UIView *_interactiveContainerView;
|
|
@@ -206,6 +211,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
206
211
|
return window ? window.bounds.size.height : UIScreen.mainScreen.bounds.size.height;
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
// Only phone sheets absorb the bottom safe-area inset — non-phone idioms
|
|
215
|
+
// (iPad form sheets) are inset from the screen edge by UIKit.
|
|
216
|
+
static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
217
|
+
return UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone;
|
|
218
|
+
}
|
|
219
|
+
|
|
209
220
|
- (CGFloat)detentBottomAdjustmentForHeight:(CGFloat)height {
|
|
210
221
|
if (_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic) {
|
|
211
222
|
return 0;
|
|
@@ -215,7 +226,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
215
226
|
}
|
|
216
227
|
|
|
217
228
|
- (CGFloat)bottomSafeAreaForHeight:(CGFloat)height {
|
|
218
|
-
if (
|
|
229
|
+
if (!TrueSheetIsPhoneIdiom()) {
|
|
219
230
|
return 0;
|
|
220
231
|
}
|
|
221
232
|
|
|
@@ -232,49 +243,101 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
246
|
+
* Largest height the sheet can reach across its configured detents, excluding
|
|
247
|
+
* the inset currently baked into the footer's height. Basis for the footer's
|
|
248
|
+
* inset absorption so it follows the same rule as bottomSafeAreaForHeight: —
|
|
249
|
+
* a sheet that only ever floats (small detents on iOS 26) keeps its footer
|
|
250
|
+
* unpadded.
|
|
238
251
|
*/
|
|
239
|
-
- (CGFloat)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
252
|
+
- (CGFloat)maxNaturalDetentHeight {
|
|
253
|
+
CGFloat applied = [self.footerHeight floatValue] > 0 ? _appliedFooterBottomInset : 0;
|
|
254
|
+
CGFloat maxHeight = 0;
|
|
255
|
+
|
|
256
|
+
for (id detent in self.detents) {
|
|
257
|
+
if (![detent isKindOfClass:[NSNumber class]]) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
CGFloat value = [detent doubleValue];
|
|
262
|
+
CGFloat height = 0;
|
|
263
|
+
if (value == -1) {
|
|
264
|
+
// Auto counts a relative footer's (padded) height
|
|
265
|
+
height = [_detentCalculator autoHeight] - (_absoluteFooter ? 0 : applied);
|
|
266
|
+
} else if (value == -2) {
|
|
267
|
+
// Peek counts an absolute footer's (padded) height
|
|
268
|
+
height = [_detentCalculator peekHeight] - (_absoluteFooter ? applied : 0);
|
|
269
|
+
} else if (value > 0 && value <= 1) {
|
|
270
|
+
height = value * self.screenHeight;
|
|
271
|
+
}
|
|
272
|
+
maxHeight = fmax(maxHeight, height);
|
|
243
273
|
}
|
|
244
274
|
|
|
245
|
-
if (
|
|
246
|
-
|
|
275
|
+
if (self.maxContentHeight) {
|
|
276
|
+
maxHeight = fmin(maxHeight, [self.maxContentHeight floatValue]);
|
|
247
277
|
}
|
|
248
278
|
|
|
249
|
-
return
|
|
279
|
+
return maxHeight > 0 ? maxHeight : self.screenHeight;
|
|
250
280
|
}
|
|
251
281
|
|
|
252
|
-
// The inset the footer absorbs as padding.
|
|
253
|
-
// available so it matches the auto detent's inset exclusion above.
|
|
282
|
+
// The inset the footer absorbs as padding.
|
|
254
283
|
- (CGFloat)footerBottomInset {
|
|
255
284
|
if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
|
|
256
285
|
return 0;
|
|
257
286
|
}
|
|
258
287
|
|
|
259
|
-
|
|
288
|
+
// Early out so the maxNaturalDetentHeight fallback below doesn't run per
|
|
289
|
+
// layout pass on iPad only to be zeroed by bottomSafeAreaForHeight:.
|
|
290
|
+
if (!TrueSheetIsPhoneIdiom()) {
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Once laid out, the sheet's own safe area is the authority — it reflects
|
|
295
|
+
// UIKit's actual float/anchored decision (a floating sheet gets 0), where
|
|
296
|
+
// bottomSafeAreaForHeight: can only guess the float threshold. Guessing
|
|
297
|
+
// wrong leaves the safe-area region unfilled (gap) or double-padded.
|
|
298
|
+
UIView *view = self.viewIfLoaded;
|
|
299
|
+
if (view.window) {
|
|
300
|
+
_hasObservedBottomInset = YES;
|
|
301
|
+
_observedBottomInset = view.safeAreaInsets.bottom;
|
|
302
|
+
return _observedBottomInset;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Off-window while presented (e.g. hidden behind a native screen): the last
|
|
306
|
+
// observed value is this sheet's own truth — prefer it over the
|
|
307
|
+
// float-threshold guess. Cleared on dismiss so a re-present observes fresh
|
|
308
|
+
// geometry (rotation or detent changes while dismissed would stale it).
|
|
309
|
+
if (_hasObservedBottomInset) {
|
|
310
|
+
return _observedBottomInset;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return [self bottomSafeAreaForHeight:[self maxNaturalDetentHeight]];
|
|
260
314
|
}
|
|
261
315
|
|
|
262
316
|
/**
|
|
263
|
-
* Bottom inset excluded from the
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
317
|
+
* Bottom inset excluded from the auto detent. A relative footer owns the
|
|
318
|
+
* sheet's bottom edge and carries the inset in its height (see
|
|
319
|
+
* footerBottomInset) — subtract exactly what was baked in so UIKit's own
|
|
320
|
+
* safe-area layout doesn't double it.
|
|
267
321
|
*/
|
|
268
|
-
- (CGFloat)
|
|
269
|
-
if (
|
|
322
|
+
- (CGFloat)autoDetentBottomInset {
|
|
323
|
+
if (_absoluteFooter || [self.footerHeight floatValue] <= 0) {
|
|
270
324
|
return 0;
|
|
271
325
|
}
|
|
272
326
|
|
|
327
|
+
return _appliedFooterBottomInset;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Bottom inset excluded from the peek detent. An absolute footer counts
|
|
332
|
+
* toward the peek height and carries the inset in its height — subtract
|
|
333
|
+
* exactly what was baked in so it isn't doubled.
|
|
334
|
+
*/
|
|
335
|
+
- (CGFloat)peekDetentBottomInset {
|
|
273
336
|
if (!_absoluteFooter || [self.footerHeight floatValue] <= 0) {
|
|
274
337
|
return 0;
|
|
275
338
|
}
|
|
276
339
|
|
|
277
|
-
return
|
|
340
|
+
return _appliedFooterBottomInset;
|
|
278
341
|
}
|
|
279
342
|
|
|
280
343
|
- (BOOL)isDesignCompatibilityMode {
|
|
@@ -520,6 +583,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
520
583
|
[self restoreWindowAccessibilityElements];
|
|
521
584
|
_isPresented = NO;
|
|
522
585
|
_isWillDismissEmitted = NO;
|
|
586
|
+
_hasObservedBottomInset = NO;
|
|
523
587
|
|
|
524
588
|
[_anchorView removeFromSuperview];
|
|
525
589
|
_anchorView = nil;
|
|
@@ -605,9 +669,30 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
605
669
|
}
|
|
606
670
|
}
|
|
607
671
|
|
|
672
|
+
// Fires when UIKit resolves the sheet's float/anchored placement (present,
|
|
673
|
+
// detent resize crossing the float threshold, rotation). The footer's
|
|
674
|
+
// absorbed inset follows the observed safe area — repush it right away so
|
|
675
|
+
// the footer pads/unpads in the same layout pass.
|
|
676
|
+
- (void)viewSafeAreaInsetsDidChange {
|
|
677
|
+
[super viewSafeAreaInsetsDidChange];
|
|
678
|
+
|
|
679
|
+
// Mid-dismiss the frame moves off-screen and reports transient insets —
|
|
680
|
+
// don't let them pollute the observed value (matches viewDidLayoutSubviews).
|
|
681
|
+
if (!self.isBeingDismissed) {
|
|
682
|
+
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
608
686
|
- (void)viewDidLayoutSubviews {
|
|
609
687
|
[super viewDidLayoutSubviews];
|
|
610
688
|
|
|
689
|
+
// viewSafeAreaInsetsDidChange doesn't fire on re-present when the observed
|
|
690
|
+
// inset matches the previous presentation's — refresh every layout pass
|
|
691
|
+
// instead (deduped downstream, so unchanged values are free).
|
|
692
|
+
if (!self.isBeingDismissed) {
|
|
693
|
+
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
694
|
+
}
|
|
695
|
+
|
|
611
696
|
// Skip size reports while backgrounded behind a stacked child — the push-back
|
|
612
697
|
// scaling changes the frame and would needlessly resize content.
|
|
613
698
|
// _lastReportedSize stays stale so the restore pass re-reports any real change.
|
|
@@ -1079,8 +1164,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1079
1164
|
return [self customDetentWithIdentifier:@"custom-auto"
|
|
1080
1165
|
atIndex:index
|
|
1081
1166
|
heightBlock:^CGFloat {
|
|
1082
|
-
|
|
1083
|
-
return height - [self autoDetentBottomInsetForHeight:height];
|
|
1167
|
+
return self->_autoDetentHeight - [self autoDetentBottomInset];
|
|
1084
1168
|
}];
|
|
1085
1169
|
} else {
|
|
1086
1170
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
@@ -1092,8 +1176,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1092
1176
|
return [self customDetentWithIdentifier:@"custom-peek"
|
|
1093
1177
|
atIndex:index
|
|
1094
1178
|
heightBlock:^CGFloat {
|
|
1095
|
-
|
|
1096
|
-
return height - [self peekDetentBottomInsetForHeight:height];
|
|
1179
|
+
return self->_peekDetentHeight - [self peekDetentBottomInset];
|
|
1097
1180
|
}];
|
|
1098
1181
|
} else {
|
|
1099
1182
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
@@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
17
17
|
- (void)setupEdgeInteractionWithScrollView:(nullable UIScrollView *)scrollView
|
|
18
18
|
edge:(UIRectEdge)edge API_AVAILABLE(ios(26.0));
|
|
19
19
|
- (void)cleanupEdgeInteraction API_AVAILABLE(ios(26.0));
|
|
20
|
+
- (nullable UIScrollView *)edgeInteractionScrollView API_AVAILABLE(ios(26.0));
|
|
20
21
|
|
|
21
22
|
@end
|
|
22
23
|
|
|
@@ -31,15 +31,37 @@ static const void *kEdgeEffectHintKey = &kEdgeEffectHintKey;
|
|
|
31
31
|
objc_setAssociatedObject(self, kEdgeEffectHintKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
- (nullable UIScrollView *)edgeInteractionScrollView API_AVAILABLE(ios(26.0)) {
|
|
35
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
|
36
|
+
for (id<UIInteraction> interaction in self.interactions) {
|
|
37
|
+
if ([interaction isKindOfClass:[UIScrollEdgeElementContainerInteraction class]]) {
|
|
38
|
+
return ((UIScrollEdgeElementContainerInteraction *)interaction).scrollView;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
#endif
|
|
42
|
+
return nil;
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
- (void)setupEdgeInteractionWithScrollView:(nullable UIScrollView *)scrollView
|
|
35
46
|
edge:(UIRectEdge)edge API_AVAILABLE(ios(26.0)) {
|
|
36
|
-
[self cleanupEdgeInteraction];
|
|
37
|
-
|
|
38
47
|
if (!scrollView) {
|
|
48
|
+
[self cleanupEdgeInteraction];
|
|
39
49
|
return;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
|
53
|
+
// Update in place — this re-runs on every footer/header size change, and
|
|
54
|
+
// tearing the interaction down mid keyboard animation flashes the edge
|
|
55
|
+
// effect at the footer's final position.
|
|
56
|
+
for (id<UIInteraction> interaction in self.interactions) {
|
|
57
|
+
if ([interaction isKindOfClass:[UIScrollEdgeElementContainerInteraction class]]) {
|
|
58
|
+
UIScrollEdgeElementContainerInteraction *existing = (UIScrollEdgeElementContainerInteraction *)interaction;
|
|
59
|
+
existing.scrollView = scrollView;
|
|
60
|
+
existing.edge = edge;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
// UIScrollEdgeElementContainerInteraction requires standard UIKit element
|
|
44
66
|
// descendants (UILabel, UIControl, etc.) to trigger the edge effect.
|
|
45
67
|
// RCTViewComponentView subviews are not recognized, so we add a
|