@lodev09/react-native-true-sheet 4.0.0-beta.2 → 4.0.0-beta.21
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 +25 -4
- package/android/build.gradle +22 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +303 -184
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +21 -110
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +116 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +18 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +124 -50
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +369 -163
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +59 -12
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +126 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +22 -9
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +84 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
- package/android/src/main/jni/TrueSheetSpec.h +1 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +110 -30
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +9 -3
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +3 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +4 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
- package/ios/TrueSheetContainerView.h +27 -7
- package/ios/TrueSheetContainerView.mm +31 -11
- package/ios/TrueSheetContentView.h +43 -15
- package/ios/TrueSheetContentView.mm +211 -189
- package/ios/TrueSheetFooterView.h +6 -0
- package/ios/TrueSheetFooterView.mm +93 -17
- package/ios/TrueSheetModule.h +2 -7
- package/ios/TrueSheetModule.mm +7 -16
- package/ios/TrueSheetOverlayView.h +32 -0
- package/ios/TrueSheetOverlayView.mm +161 -0
- package/ios/TrueSheetView.mm +275 -56
- package/ios/TrueSheetViewController.h +31 -3
- package/ios/TrueSheetViewController.mm +393 -94
- package/ios/core/TrueSheetBlurView.h +0 -2
- package/ios/core/TrueSheetBlurView.mm +3 -36
- package/ios/core/TrueSheetDetentCalculator.h +7 -1
- package/ios/core/TrueSheetDetentCalculator.mm +27 -4
- package/ios/core/TrueSheetGrabberView.mm +13 -3
- package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
- package/ios/core/TrueSheetOverlayContainerView.h +31 -0
- package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
- package/ios/events/TrueSheetLifecycleEvents.h +2 -0
- package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
- package/ios/tvos/TrueSheetStubs.mm +12 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
- package/lib/module/TrueSheet.js +175 -39
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +220 -243
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetBackground.js +15 -0
- package/lib/module/TrueSheetBackground.js.map +1 -0
- package/lib/module/TrueSheetOverlay.js +30 -0
- package/lib/module/TrueSheetOverlay.js.map +1 -0
- package/lib/module/TrueSheetOverlay.web.js +35 -0
- package/lib/module/TrueSheetOverlay.web.js.map +1 -0
- package/lib/module/TrueSheetPeek.web.js +20 -15
- package/lib/module/TrueSheetPeek.web.js.map +1 -1
- package/lib/module/detentBackgrounds.js +21 -0
- package/lib/module/detentBackgrounds.js.map +1 -0
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +15 -10
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/mocks/index.js +18 -2
- package/lib/module/mocks/index.js.map +1 -1
- package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
- package/lib/module/navigation/base-types.js +2 -0
- package/lib/module/navigation/base-types.js.map +1 -0
- package/lib/module/navigation/createTrueSheetRouter.js +0 -9
- package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
- package/lib/module/navigation/expo-router/base-types.js +2 -0
- package/lib/module/navigation/expo-router/base-types.js.map +1 -0
- package/lib/module/navigation/expo-router/index.js +10 -7
- package/lib/module/navigation/expo-router/index.js.map +1 -1
- package/lib/module/navigation/expo-router/types.js +4 -0
- package/lib/module/navigation/expo-router/types.js.map +1 -0
- package/lib/module/navigation/index.js.map +1 -1
- package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
- package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
- package/lib/module/web/constants.js +1 -1
- package/lib/module/web/constants.js.map +1 -1
- package/lib/module/web/layout.js +168 -0
- package/lib/module/web/layout.js.map +1 -0
- package/lib/module/web/vaul/constants.js +9 -0
- package/lib/module/web/vaul/constants.js.map +1 -1
- package/lib/module/web/vaul/context.js.map +1 -1
- package/lib/module/web/vaul/helpers.js +1 -0
- package/lib/module/web/vaul/helpers.js.map +1 -1
- package/lib/module/web/vaul/index.js +47 -7
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +21 -10
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +6 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +157 -34
- 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/TrueSheetBackground.d.ts +3 -0
- package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
- package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
- package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
- package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
- package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +12 -7
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +4 -0
- package/lib/typescript/src/mocks/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
- package/lib/typescript/src/navigation/base-types.d.ts +9 -0
- package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +2 -13
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts +8 -6
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
- package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/index.d.ts +3 -4
- package/lib/typescript/src/navigation/index.d.ts.map +1 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +41 -5
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/web/constants.d.ts +1 -1
- package/lib/typescript/src/web/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/layout.d.ts +19 -0
- package/lib/typescript/src/web/layout.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts +3 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/context.d.ts +1 -0
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +13 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +2 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +17 -8
- package/react-native.config.js +1 -0
- package/src/TrueSheet.tsx +208 -48
- package/src/TrueSheet.types.ts +166 -35
- package/src/TrueSheet.web.tsx +272 -290
- package/src/TrueSheetBackground.tsx +17 -0
- package/src/TrueSheetOverlay.tsx +24 -0
- package/src/TrueSheetOverlay.web.tsx +33 -0
- package/src/TrueSheetPeek.web.tsx +25 -23
- package/src/detentBackgrounds.ts +24 -0
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +15 -10
- package/src/index.ts +1 -0
- package/src/mocks/index.ts +17 -2
- package/src/mocks/navigationExpoRouter.ts +6 -5
- package/src/navigation/base-types.ts +20 -0
- package/src/navigation/createTrueSheetRouter.ts +5 -33
- package/src/navigation/expo-router/base-types.ts +18 -0
- package/src/navigation/expo-router/index.ts +28 -30
- package/src/navigation/expo-router/types.ts +116 -0
- package/src/navigation/index.ts +6 -7
- package/src/navigation/screen/useSheetScreenState.ts +2 -0
- package/src/navigation/types.ts +75 -22
- package/src/web/constants.ts +1 -1
- package/src/web/layout.ts +196 -0
- package/src/web/vaul/constants.ts +9 -0
- package/src/web/vaul/context.ts +1 -0
- package/src/web/vaul/helpers.ts +5 -0
- package/src/web/vaul/index.tsx +75 -5
- package/src/web/vaul/use-snap-points.ts +32 -15
|
@@ -45,6 +45,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
45
45
|
- (void)restoreWindowAccessibilityElements;
|
|
46
46
|
- (void)setSheetAccessibilityElementsHidden:(BOOL)hidden;
|
|
47
47
|
- (void)setAccessibilityContentElement:(UIView *)contentView;
|
|
48
|
+
- (NSArray *)accessibilityGrabberElements;
|
|
48
49
|
- (void)endInteractiveDismissState;
|
|
49
50
|
- (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
|
|
50
51
|
duration:(NSTimeInterval)duration
|
|
@@ -53,12 +54,28 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
53
54
|
|
|
54
55
|
@end
|
|
55
56
|
|
|
57
|
+
// VoiceOver delivers the two-finger Z (escape) gesture by walking up the view
|
|
58
|
+
// hierarchy from the focused element, so the sheet's root view handles it.
|
|
59
|
+
@interface TrueSheetControllerView : UIView
|
|
60
|
+
@property (nonatomic, weak) TrueSheetViewController *controller;
|
|
61
|
+
@end
|
|
62
|
+
|
|
63
|
+
@implementation TrueSheetControllerView
|
|
64
|
+
|
|
65
|
+
- (BOOL)accessibilityPerformEscape {
|
|
66
|
+
return [self.controller accessibilityPerformEscape];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@end
|
|
70
|
+
|
|
56
71
|
@implementation TrueSheetViewController {
|
|
57
72
|
TrueSheetPositionState _lastEmittedPositionState;
|
|
58
73
|
CGSize _lastReportedSize;
|
|
59
74
|
CGFloat _autoDetentHeight;
|
|
60
75
|
CGFloat _peekDetentHeight;
|
|
61
76
|
NSInteger _pendingDetentIndex;
|
|
77
|
+
NSInteger _displayedBackgroundIndex;
|
|
78
|
+
BOOL _usesBackgroundEffects;
|
|
62
79
|
|
|
63
80
|
// Present/dismiss tracking: a display link scoped to the transition
|
|
64
81
|
// coordinator — started alongside the transition, stopped in its completion.
|
|
@@ -70,6 +87,11 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
70
87
|
BOOL _isDragging;
|
|
71
88
|
BOOL _isWillDismissEmitted;
|
|
72
89
|
|
|
90
|
+
// Last bottom safe area observed while presented — reused as the estimate
|
|
91
|
+
// for the next present (beats the float-threshold guess for re-presents).
|
|
92
|
+
BOOL _hasObservedBottomInset;
|
|
93
|
+
CGFloat _observedBottomInset;
|
|
94
|
+
|
|
73
95
|
BOOL _isInteractiveDismiss;
|
|
74
96
|
CGFloat _interactiveStartPosition;
|
|
75
97
|
UIView *_interactiveContainerView;
|
|
@@ -92,6 +114,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
92
114
|
- (instancetype)init {
|
|
93
115
|
if (self = [super initWithNibName:nil bundle:nil]) {
|
|
94
116
|
_detents = @[ @0.5, @1 ];
|
|
117
|
+
_detentBackgrounds = @[];
|
|
118
|
+
_displayedBackgroundIndex = 0;
|
|
95
119
|
_contentHeight = @(0);
|
|
96
120
|
_headerHeight = @(0);
|
|
97
121
|
_footerHeight = @(0);
|
|
@@ -99,6 +123,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
99
123
|
_grabber = YES;
|
|
100
124
|
_draggable = YES;
|
|
101
125
|
_scrollingExpandsSheet = YES;
|
|
126
|
+
_footerAvoidsKeyboard = YES;
|
|
102
127
|
_dismissible = YES;
|
|
103
128
|
_dimmed = YES;
|
|
104
129
|
_dimmedDetentIndex = @(0);
|
|
@@ -113,7 +138,6 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
113
138
|
_activeDetentIndex = -1;
|
|
114
139
|
_pendingDetentIndex = -1;
|
|
115
140
|
|
|
116
|
-
_blurInteraction = YES;
|
|
117
141
|
_insetAdjustment = TrueSheetViewInsetAdjustment::Automatic;
|
|
118
142
|
_detentCalculator = [[TrueSheetDetentCalculator alloc] init];
|
|
119
143
|
_detentCalculator.delegate = self;
|
|
@@ -202,10 +226,19 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
202
226
|
}
|
|
203
227
|
|
|
204
228
|
- (CGFloat)screenHeight {
|
|
205
|
-
|
|
229
|
+
// In a windowed iPad app (Stage Manager, iPadOS 26 windowing) the window can be shorter than
|
|
230
|
+
// UIScreen.mainScreen, so prefer the window's height. Some callers run before this controller's
|
|
231
|
+
// view is attached (e.g. mid-presentation detent resolution), so fall back to the presenter's.
|
|
232
|
+
UIWindow *window = self.view.window ?: self.presentingViewController.view.window;
|
|
206
233
|
return window ? window.bounds.size.height : UIScreen.mainScreen.bounds.size.height;
|
|
207
234
|
}
|
|
208
235
|
|
|
236
|
+
// Only phone sheets absorb the bottom safe-area inset — non-phone idioms
|
|
237
|
+
// (iPad form sheets) are inset from the screen edge by UIKit.
|
|
238
|
+
static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
239
|
+
return UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone;
|
|
240
|
+
}
|
|
241
|
+
|
|
209
242
|
- (CGFloat)detentBottomAdjustmentForHeight:(CGFloat)height {
|
|
210
243
|
if (_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic) {
|
|
211
244
|
return 0;
|
|
@@ -215,7 +248,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
215
248
|
}
|
|
216
249
|
|
|
217
250
|
- (CGFloat)bottomSafeAreaForHeight:(CGFloat)height {
|
|
218
|
-
if (
|
|
251
|
+
if (!TrueSheetIsPhoneIdiom()) {
|
|
219
252
|
return 0;
|
|
220
253
|
}
|
|
221
254
|
|
|
@@ -232,52 +265,117 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
232
265
|
}
|
|
233
266
|
|
|
234
267
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
268
|
+
* Largest height the sheet can reach across its configured detents, excluding
|
|
269
|
+
* the inset currently baked into the footer's height. Basis for the footer's
|
|
270
|
+
* inset absorption so it follows the same rule as bottomSafeAreaForHeight: —
|
|
271
|
+
* a sheet that only ever floats (small detents on iOS 26) keeps its footer
|
|
272
|
+
* unpadded.
|
|
238
273
|
*/
|
|
239
|
-
- (CGFloat)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
274
|
+
- (CGFloat)maxNaturalDetentHeight {
|
|
275
|
+
CGFloat maxHeight = 0;
|
|
276
|
+
|
|
277
|
+
for (id detent in self.detents) {
|
|
278
|
+
if (![detent isKindOfClass:[NSNumber class]]) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
CGFloat value = [detent doubleValue];
|
|
283
|
+
CGFloat height = 0;
|
|
284
|
+
if (value == -1) {
|
|
285
|
+
height = [_detentCalculator autoHeightForKeyboardHeight:0] - [self autoDetentBottomInset];
|
|
286
|
+
} else if (value == -2) {
|
|
287
|
+
height = [_detentCalculator peekHeight] - [self peekDetentBottomInset];
|
|
288
|
+
} else if (value > 0 && value <= 1) {
|
|
289
|
+
height = value * self.screenHeight;
|
|
290
|
+
}
|
|
291
|
+
maxHeight = fmax(maxHeight, height);
|
|
243
292
|
}
|
|
244
293
|
|
|
245
|
-
if (
|
|
246
|
-
|
|
294
|
+
if (self.maxContentHeight) {
|
|
295
|
+
maxHeight = fmin(maxHeight, [self.maxContentHeight floatValue]);
|
|
247
296
|
}
|
|
248
297
|
|
|
249
|
-
return
|
|
298
|
+
return maxHeight > 0 ? maxHeight : self.screenHeight;
|
|
250
299
|
}
|
|
251
300
|
|
|
252
|
-
// The inset the footer absorbs as padding.
|
|
253
|
-
// available so it matches the auto detent's inset exclusion above.
|
|
301
|
+
// The inset the footer absorbs as padding.
|
|
254
302
|
- (CGFloat)footerBottomInset {
|
|
255
303
|
if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
|
|
256
304
|
return 0;
|
|
257
305
|
}
|
|
258
306
|
|
|
259
|
-
|
|
307
|
+
// Early out so the maxNaturalDetentHeight fallback below doesn't run per
|
|
308
|
+
// layout pass on iPad only to be zeroed by bottomSafeAreaForHeight:.
|
|
309
|
+
if (!TrueSheetIsPhoneIdiom()) {
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Once laid out, the sheet's own safe area is the authority — it reflects
|
|
314
|
+
// UIKit's actual float/anchored decision (a floating sheet gets 0), where
|
|
315
|
+
// bottomSafeAreaForHeight: can only guess the float threshold. Guessing
|
|
316
|
+
// wrong leaves the safe-area region unfilled (gap) or double-padded.
|
|
317
|
+
UIView *view = self.viewIfLoaded;
|
|
318
|
+
if (view.window) {
|
|
319
|
+
_hasObservedBottomInset = YES;
|
|
320
|
+
_observedBottomInset = view.safeAreaInsets.bottom;
|
|
321
|
+
return _observedBottomInset;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Off-window while presented (e.g. hidden behind a native screen): the last
|
|
325
|
+
// observed value is this sheet's own truth — prefer it over the
|
|
326
|
+
// float-threshold guess. Cleared on dismiss so a re-present observes fresh
|
|
327
|
+
// geometry (rotation or detent changes while dismissed would stale it).
|
|
328
|
+
if (_hasObservedBottomInset) {
|
|
329
|
+
return _observedBottomInset;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return [self bottomSafeAreaForHeight:[self maxNaturalDetentHeight]];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Re-resolve alongside the keyboard animation as its overlap with the pinned
|
|
336
|
+
// footer changes, including keyboard height changes while already visible.
|
|
337
|
+
- (void)setKeyboardHeight:(CGFloat)keyboardHeight {
|
|
338
|
+
if (_keyboardHeight == keyboardHeight) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
_keyboardHeight = keyboardHeight;
|
|
342
|
+
|
|
343
|
+
if (_footerInsetAdjustment && !_footerAvoidsKeyboard && self.isPresented) {
|
|
344
|
+
[self setupSheetDetentsForSizeChange];
|
|
345
|
+
}
|
|
260
346
|
}
|
|
261
347
|
|
|
262
348
|
/**
|
|
263
|
-
* Bottom inset excluded from the
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
349
|
+
* Bottom inset excluded from the auto detent. A relative footer (or an
|
|
350
|
+
* absolute one padding the scrollable) owns the sheet's bottom edge and
|
|
351
|
+
* carries the inset in its height (see footerBottomInset) — subtract exactly
|
|
352
|
+
* what was baked in so UIKit's own safe-area layout doesn't double it.
|
|
267
353
|
*/
|
|
268
|
-
- (CGFloat)
|
|
269
|
-
if (
|
|
354
|
+
- (CGFloat)autoDetentBottomInset {
|
|
355
|
+
if ((_absoluteFooter && !self.footerInsetAdjustment) || [self.footerHeight floatValue] <= 0) {
|
|
270
356
|
return 0;
|
|
271
357
|
}
|
|
272
358
|
|
|
359
|
+
return _appliedFooterBottomInset;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Bottom inset excluded from the peek detent. An absolute footer counts
|
|
364
|
+
* toward the peek height and carries the inset in its height — subtract
|
|
365
|
+
* exactly what was baked in so it isn't doubled.
|
|
366
|
+
*/
|
|
367
|
+
- (CGFloat)peekDetentBottomInset {
|
|
273
368
|
if (!_absoluteFooter || [self.footerHeight floatValue] <= 0) {
|
|
274
369
|
return 0;
|
|
275
370
|
}
|
|
276
371
|
|
|
277
|
-
return
|
|
372
|
+
return _appliedFooterBottomInset;
|
|
278
373
|
}
|
|
279
374
|
|
|
280
375
|
- (BOOL)isDesignCompatibilityMode {
|
|
376
|
+
if (@available(iOS 27.0, *)) {
|
|
377
|
+
return NO;
|
|
378
|
+
}
|
|
281
379
|
if (@available(iOS 26.0, *)) {
|
|
282
380
|
NSNumber *value = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"];
|
|
283
381
|
return value.boolValue;
|
|
@@ -314,6 +412,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
314
412
|
|
|
315
413
|
#pragma mark - View Lifecycle
|
|
316
414
|
|
|
415
|
+
- (void)loadView {
|
|
416
|
+
TrueSheetControllerView *view = [[TrueSheetControllerView alloc] init];
|
|
417
|
+
view.controller = self;
|
|
418
|
+
self.view = view;
|
|
419
|
+
}
|
|
420
|
+
|
|
317
421
|
- (void)viewDidLoad {
|
|
318
422
|
[super viewDidLoad];
|
|
319
423
|
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
|
|
@@ -421,6 +525,37 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
421
525
|
}
|
|
422
526
|
}
|
|
423
527
|
|
|
528
|
+
- (NSArray *)accessibilityGrabberElements {
|
|
529
|
+
if (_grabberView && !_grabberView.hidden) {
|
|
530
|
+
return @[ _grabberView ];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// The system grabber is an accessibility element UIKit adds to the presented view
|
|
534
|
+
// (a sibling of our root view). Expose it so VoiceOver can resize or dismiss with it.
|
|
535
|
+
// UIKit also keeps a transparent bottom grabber there, so skip invisible views.
|
|
536
|
+
NSMutableArray *elements = [NSMutableArray array];
|
|
537
|
+
for (UIView *subview in self.presentationController.presentedView.subviews) {
|
|
538
|
+
if (subview != self.view && subview.isAccessibilityElement && !subview.hidden && subview.alpha > 0) {
|
|
539
|
+
[elements addObject:subview];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return elements;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
- (BOOL)accessibilityPerformEscape {
|
|
546
|
+
if (!_isPresented) {
|
|
547
|
+
return NO;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!self.dismissible) {
|
|
551
|
+
[self.delegate viewControllerDidAttemptDismiss];
|
|
552
|
+
return NO;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
556
|
+
return YES;
|
|
557
|
+
}
|
|
558
|
+
|
|
424
559
|
- (void)setAccessibilityContentElement:(UIView *)contentView {
|
|
425
560
|
BOOL hasAccessibilityFooterElements = [contentView isKindOfClass:[TrueSheetContainerView class]] &&
|
|
426
561
|
[(TrueSheetContainerView *)contentView hasAccessibilityFooterElements];
|
|
@@ -433,12 +568,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
433
568
|
// its accessibilityElements getter recomputes live, so a footer/content subtree
|
|
434
569
|
// that remounts (e.g. swapping a footer button on a state change) stays
|
|
435
570
|
// discoverable instead of leaving this snapshot pointing at destroyed views.
|
|
436
|
-
|
|
571
|
+
NSMutableArray *accessibilityElements = [[self accessibilityGrabberElements] mutableCopy];
|
|
437
572
|
if (isAccessibilityModal) {
|
|
438
573
|
NSArray *contentElements = contentView.accessibilityElements;
|
|
439
|
-
accessibilityElements
|
|
574
|
+
[accessibilityElements addObjectsFromArray:contentElements.count > 0 ? contentElements : @[ contentView ]];
|
|
440
575
|
} else {
|
|
441
|
-
accessibilityElements
|
|
576
|
+
[accessibilityElements addObject:contentView];
|
|
442
577
|
}
|
|
443
578
|
|
|
444
579
|
self.view.isAccessibilityElement = NO;
|
|
@@ -451,6 +586,15 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
451
586
|
presentedView.accessibilityViewIsModal = isAccessibilityModal;
|
|
452
587
|
presentedView.accessibilityElements = accessibilityElements;
|
|
453
588
|
|
|
589
|
+
// The system grabber is a sibling of our root view, so an escape performed while it
|
|
590
|
+
// is focused walks presentedView → window and never reaches TrueSheetControllerView.
|
|
591
|
+
if (@available(iOS 17.0, *)) {
|
|
592
|
+
__weak __typeof(self) weakSelf = self;
|
|
593
|
+
presentedView.accessibilityPerformEscapeBlock = ^BOOL {
|
|
594
|
+
return [weakSelf accessibilityPerformEscape];
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
454
598
|
UIWindow *window = self.view.window;
|
|
455
599
|
UIViewController *presentingViewController = [self accessibilityPresentingViewController];
|
|
456
600
|
if (window && presentingViewController.view) {
|
|
@@ -520,6 +664,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
520
664
|
[self restoreWindowAccessibilityElements];
|
|
521
665
|
_isPresented = NO;
|
|
522
666
|
_isWillDismissEmitted = NO;
|
|
667
|
+
_hasObservedBottomInset = NO;
|
|
523
668
|
|
|
524
669
|
[_anchorView removeFromSuperview];
|
|
525
670
|
_anchorView = nil;
|
|
@@ -561,8 +706,10 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
561
706
|
}
|
|
562
707
|
|
|
563
708
|
// Dismissing with the keyboard up skips the hide notification — don't let
|
|
564
|
-
// the stale flag block learning on the next present
|
|
709
|
+
// the stale flag block learning on the next present, or a stale height keep
|
|
710
|
+
// the footer out of the auto detent.
|
|
565
711
|
_keyboardSheetGrown = NO;
|
|
712
|
+
_keyboardHeight = 0;
|
|
566
713
|
|
|
567
714
|
[self emitDidDismissEvents];
|
|
568
715
|
}
|
|
@@ -575,24 +722,26 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
575
722
|
return;
|
|
576
723
|
}
|
|
577
724
|
|
|
578
|
-
|
|
725
|
+
// Mid-drag the frame tracks the finger — never settle or learn from it;
|
|
726
|
+
// pending changes keep their flags and settle on the first post-drag layout.
|
|
727
|
+
if ((_pendingContentSizeChange || _pendingDetentsChange) && !_isDragging) {
|
|
579
728
|
_pendingContentSizeChange = NO;
|
|
580
729
|
_pendingDetentsChange = NO;
|
|
581
730
|
[self settleAtDetentIndex:self.currentDetentIndex debug:@"layout"];
|
|
582
|
-
} else
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
731
|
+
} else {
|
|
732
|
+
// A child on top moves this sheet two ways: an animated collapse/restore
|
|
733
|
+
// step (presenting/dismissing) — emit the target non-realtime so JS
|
|
734
|
+
// animates to it — or direct per-frame re-layouts while the child drags,
|
|
735
|
+
// which stay realtime. Mid-drag no child is on top, so drags emit realtime.
|
|
587
736
|
BOOL realtime = self.presentedViewController == nil || !self.isPresentedViewAnimating;
|
|
588
737
|
|
|
589
738
|
// A layout pass at rest can nudge the frame by a sub-pixel amount
|
|
590
739
|
// (pixel-grid snapping after present), leaving the interpolated index
|
|
591
740
|
// slightly off the whole number. Re-learn when the frame is effectively
|
|
592
741
|
// at the current detent so the drift is absorbed instead of emitted.
|
|
593
|
-
// The tight threshold keeps real movement (e.g. scroll-driven expansion
|
|
594
|
-
// from being absorbed.
|
|
595
|
-
if (self.presentedViewController == nil && !self.isPresentedViewAnimating) {
|
|
742
|
+
// The tight threshold keeps real movement (e.g. scroll-driven expansion
|
|
743
|
+
// or a drag hovering near the detent) from being absorbed.
|
|
744
|
+
if (!_isDragging && self.presentedViewController == nil && !self.isPresentedViewAnimating) {
|
|
596
745
|
NSInteger index = self.currentDetentIndex;
|
|
597
746
|
CGFloat expectedHeight = [_detentCalculator resolvedHeightForIndex:index];
|
|
598
747
|
CGFloat actualHeight = self.screenHeight - self.currentPosition;
|
|
@@ -605,9 +754,46 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
605
754
|
}
|
|
606
755
|
}
|
|
607
756
|
|
|
757
|
+
// Rotation or an iPad window resize changes the height fractional detents
|
|
758
|
+
// resolve against — re-resolve alongside the transition so the sheet lands on
|
|
759
|
+
// the new height instead of waiting for a content-driven rebuild.
|
|
760
|
+
- (void)viewWillTransitionToSize:(CGSize)size
|
|
761
|
+
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
|
762
|
+
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
|
763
|
+
|
|
764
|
+
if (@available(iOS 16.0, *)) {
|
|
765
|
+
[coordinator
|
|
766
|
+
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
767
|
+
[self.sheet invalidateDetents];
|
|
768
|
+
}
|
|
769
|
+
completion:nil];
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Fires when UIKit resolves the sheet's float/anchored placement (present,
|
|
774
|
+
// detent resize crossing the float threshold, rotation). The footer's
|
|
775
|
+
// absorbed inset follows the observed safe area — repush it right away so
|
|
776
|
+
// the footer pads/unpads in the same layout pass.
|
|
777
|
+
- (void)viewSafeAreaInsetsDidChange {
|
|
778
|
+
[super viewSafeAreaInsetsDidChange];
|
|
779
|
+
|
|
780
|
+
// Mid-dismiss the frame moves off-screen and reports transient insets —
|
|
781
|
+
// don't let them pollute the observed value (matches viewDidLayoutSubviews).
|
|
782
|
+
if (!self.isBeingDismissed) {
|
|
783
|
+
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
608
787
|
- (void)viewDidLayoutSubviews {
|
|
609
788
|
[super viewDidLayoutSubviews];
|
|
610
789
|
|
|
790
|
+
// viewSafeAreaInsetsDidChange doesn't fire on re-present when the observed
|
|
791
|
+
// inset matches the previous presentation's — refresh every layout pass
|
|
792
|
+
// instead (deduped downstream, so unchanged values are free).
|
|
793
|
+
if (!self.isBeingDismissed) {
|
|
794
|
+
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
795
|
+
}
|
|
796
|
+
|
|
611
797
|
// Skip size reports while backgrounded behind a stacked child — the push-back
|
|
612
798
|
// scaling changes the frame and would needlessly resize content.
|
|
613
799
|
// _lastReportedSize stays stale so the restore pass re-reports any real change.
|
|
@@ -695,7 +881,14 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
695
881
|
_isDragging = YES;
|
|
696
882
|
break;
|
|
697
883
|
case UIGestureRecognizerStateChanged:
|
|
698
|
-
|
|
884
|
+
// Layout owns the emit while the sheet resizes with the finger (a
|
|
885
|
+
// layout pass is already queued), and the transition tick owns it
|
|
886
|
+
// during an interactive dismiss. Translation-only movement (e.g.
|
|
887
|
+
// dragging down to the first detent) triggers neither — emit here as
|
|
888
|
+
// the backup.
|
|
889
|
+
if (!_isTransitioning && !self.presentedView.layer.needsLayout && !self.viewIfLoaded.layer.needsLayout) {
|
|
890
|
+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
|
|
891
|
+
}
|
|
699
892
|
break;
|
|
700
893
|
case UIGestureRecognizerStateEnded:
|
|
701
894
|
case UIGestureRecognizerStateCancelled: {
|
|
@@ -780,27 +973,26 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
780
973
|
}
|
|
781
974
|
|
|
782
975
|
- (void)handleTransitionTick {
|
|
783
|
-
//
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (self.isBeingDismissed) {
|
|
788
|
-
// Emit only once the dismiss is committed: on release UIKit moves the
|
|
789
|
-
// model frame off-screen, while a cancelled drag rewinds it back to the
|
|
790
|
-
// detent (isBeingDismissed stays YES during the rewind).
|
|
791
|
-
if (self.currentPosition >= self.screenHeight) {
|
|
792
|
-
[self emitWillDismissEvents];
|
|
793
|
-
}
|
|
976
|
+
// Also covers the interactive dismiss phase (finger down) — layout passes
|
|
977
|
+
// are skipped while transitioning, so the tick owns position there too.
|
|
978
|
+
CGFloat position = self.livePosition;
|
|
794
979
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
980
|
+
if (self.isBeingDismissed) {
|
|
981
|
+
// Emit only once the dismiss is committed: on release UIKit moves the
|
|
982
|
+
// model frame off-screen, while a cancelled drag rewinds it back to the
|
|
983
|
+
// detent (isBeingDismissed stays YES during the rewind).
|
|
984
|
+
if (self.currentPosition >= self.screenHeight) {
|
|
985
|
+
[self emitWillDismissEvents];
|
|
800
986
|
}
|
|
801
987
|
|
|
802
|
-
|
|
988
|
+
// Hide blur at the end of dismiss to prevent UIVisualEffectView
|
|
989
|
+
// from causing a flicker/flash at the bottom edge of the sheet.
|
|
990
|
+
if (self.screenHeight - position < 1) {
|
|
991
|
+
_blurView.alpha = 0;
|
|
992
|
+
}
|
|
803
993
|
}
|
|
994
|
+
|
|
995
|
+
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
|
|
804
996
|
}
|
|
805
997
|
|
|
806
998
|
/**
|
|
@@ -933,6 +1125,20 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
933
1125
|
}
|
|
934
1126
|
}
|
|
935
1127
|
|
|
1128
|
+
if (self.isPresented && !self.isBeingDismissed && !_isWillDismissEmitted && !_usesBackgroundEffects) {
|
|
1129
|
+
NSInteger index = [_detentCalculator backgroundIndexForPosition:position displayedIndex:_displayedBackgroundIndex];
|
|
1130
|
+
if (index != _displayedBackgroundIndex) {
|
|
1131
|
+
_displayedBackgroundIndex = index;
|
|
1132
|
+
[UIView animateWithDuration:0.2
|
|
1133
|
+
delay:0
|
|
1134
|
+
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
|
|
1135
|
+
animations:^{
|
|
1136
|
+
[self setupBackground];
|
|
1137
|
+
}
|
|
1138
|
+
completion:nil];
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
936
1142
|
TrueSheetPositionState state = {
|
|
937
1143
|
.position = position,
|
|
938
1144
|
.detent = [self interpolatedDetentForPosition:position],
|
|
@@ -998,7 +1204,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
998
1204
|
*/
|
|
999
1205
|
- (void)setupSheetDetentsForSizeChange {
|
|
1000
1206
|
if (@available(iOS 16.0, *)) {
|
|
1001
|
-
CGFloat autoHeight = [_detentCalculator
|
|
1207
|
+
CGFloat autoHeight = [_detentCalculator autoHeightForKeyboardHeight:_keyboardHeight];
|
|
1002
1208
|
CGFloat peekHeight = [_detentCalculator peekHeight];
|
|
1003
1209
|
|
|
1004
1210
|
if (fabs(autoHeight - _autoDetentHeight) < 0.5 && fabs(peekHeight - _peekDetentHeight) < 0.5) {
|
|
@@ -1031,7 +1237,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1031
1237
|
NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
|
|
1032
1238
|
[_detentCalculator clearResolvedHeights];
|
|
1033
1239
|
|
|
1034
|
-
_autoDetentHeight = [_detentCalculator
|
|
1240
|
+
_autoDetentHeight = [_detentCalculator autoHeightForKeyboardHeight:_keyboardHeight];
|
|
1035
1241
|
_peekDetentHeight = [_detentCalculator peekHeight];
|
|
1036
1242
|
|
|
1037
1243
|
for (NSInteger index = 0; index < self.detents.count; index++) {
|
|
@@ -1041,6 +1247,20 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1041
1247
|
}
|
|
1042
1248
|
|
|
1043
1249
|
[_detentCalculator setDetentCount:self.detents.count];
|
|
1250
|
+
_usesBackgroundEffects = [self canUseBackgroundEffects:detents];
|
|
1251
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1252
|
+
if (@available(iOS 26.1, *)) {
|
|
1253
|
+
if (_usesBackgroundEffects) {
|
|
1254
|
+
for (NSInteger index = 0; index < detents.count; index++) {
|
|
1255
|
+
NSDictionary *entry = [self backgroundForIndex:index];
|
|
1256
|
+
auto blur = entry[@"blur"] ? static_cast<TrueSheetViewBackgroundBlur>([entry[@"blur"] intValue])
|
|
1257
|
+
: TrueSheetViewBackgroundBlur::None;
|
|
1258
|
+
detents[index].backgroundEffect = [self backgroundEffectForColor:entry[@"color"] blur:blur];
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
#endif
|
|
1263
|
+
|
|
1044
1264
|
sheet.detents = detents;
|
|
1045
1265
|
|
|
1046
1266
|
if (self.dimmed && [self.dimmedDetentIndex integerValue] == 0) {
|
|
@@ -1079,8 +1299,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1079
1299
|
return [self customDetentWithIdentifier:@"custom-auto"
|
|
1080
1300
|
atIndex:index
|
|
1081
1301
|
heightBlock:^CGFloat {
|
|
1082
|
-
|
|
1083
|
-
return height - [self autoDetentBottomInsetForHeight:height];
|
|
1302
|
+
return self->_autoDetentHeight - [self autoDetentBottomInset];
|
|
1084
1303
|
}];
|
|
1085
1304
|
} else {
|
|
1086
1305
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
@@ -1092,8 +1311,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1092
1311
|
return [self customDetentWithIdentifier:@"custom-peek"
|
|
1093
1312
|
atIndex:index
|
|
1094
1313
|
heightBlock:^CGFloat {
|
|
1095
|
-
|
|
1096
|
-
return height - [self peekDetentBottomInsetForHeight:height];
|
|
1314
|
+
return self->_peekDetentHeight - [self peekDetentBottomInset];
|
|
1097
1315
|
}];
|
|
1098
1316
|
} else {
|
|
1099
1317
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
@@ -1107,8 +1325,13 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1107
1325
|
|
|
1108
1326
|
if (@available(iOS 16.0, *)) {
|
|
1109
1327
|
NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
|
|
1110
|
-
|
|
1111
|
-
|
|
1328
|
+
// Resolve lazily — detents are built before presentation (no window yet), and
|
|
1329
|
+
// UIKit re-resolves on container size changes (rotation, windowed iPad resize).
|
|
1330
|
+
return [self customDetentWithIdentifier:detentId
|
|
1331
|
+
atIndex:index
|
|
1332
|
+
heightBlock:^CGFloat {
|
|
1333
|
+
return value * self.screenHeight;
|
|
1334
|
+
}];
|
|
1112
1335
|
} else if (value >= 0.5) {
|
|
1113
1336
|
return [UISheetPresentationControllerDetent largeDetent];
|
|
1114
1337
|
} else {
|
|
@@ -1116,16 +1339,6 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1116
1339
|
}
|
|
1117
1340
|
}
|
|
1118
1341
|
|
|
1119
|
-
- (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
|
|
1120
|
-
height:(CGFloat)height
|
|
1121
|
-
atIndex:(NSInteger)index API_AVAILABLE(ios(16.0)) {
|
|
1122
|
-
return [self customDetentWithIdentifier:identifier
|
|
1123
|
-
atIndex:index
|
|
1124
|
-
heightBlock:^CGFloat {
|
|
1125
|
-
return height;
|
|
1126
|
-
}];
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
1342
|
- (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
|
|
1130
1343
|
atIndex:(NSInteger)index
|
|
1131
1344
|
heightBlock:(CGFloat (^)(void))heightBlock
|
|
@@ -1215,6 +1428,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1215
1428
|
|
|
1216
1429
|
- (void)setupActiveDetentWithIndex:(NSInteger)index {
|
|
1217
1430
|
_activeDetentIndex = index;
|
|
1431
|
+
_displayedBackgroundIndex = index;
|
|
1432
|
+
[self setupBackground];
|
|
1218
1433
|
[self applyActiveDetent];
|
|
1219
1434
|
}
|
|
1220
1435
|
|
|
@@ -1228,34 +1443,72 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1228
1443
|
[self applyActiveDetent];
|
|
1229
1444
|
}
|
|
1230
1445
|
|
|
1231
|
-
- (
|
|
1232
|
-
|
|
1233
|
-
if (@available(iOS 26.
|
|
1234
|
-
//
|
|
1235
|
-
|
|
1236
|
-
|
|
1446
|
+
- (BOOL)canUseBackgroundEffects:(NSArray<UISheetPresentationControllerDetent *> *)detents {
|
|
1447
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1448
|
+
if (@available(iOS 26.1, *)) {
|
|
1449
|
+
// Some 26.1 builds omit setters declared in the SDK.
|
|
1450
|
+
if (self.isDesignCompatibilityMode || ![self.sheet respondsToSelector:@selector(setBackgroundEffect:)]) {
|
|
1451
|
+
return NO;
|
|
1452
|
+
}
|
|
1453
|
+
for (UISheetPresentationControllerDetent *detent in detents) {
|
|
1454
|
+
if (![detent respondsToSelector:@selector(setBackgroundEffect:)]) {
|
|
1455
|
+
return NO;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return YES;
|
|
1237
1459
|
}
|
|
1460
|
+
#endif
|
|
1461
|
+
return NO;
|
|
1462
|
+
}
|
|
1238
1463
|
|
|
1239
|
-
|
|
1464
|
+
- (NSDictionary *)backgroundForIndex:(NSInteger)index {
|
|
1465
|
+
return index >= 0 && index < self.detentBackgrounds.count ? self.detentBackgrounds[index] : nil;
|
|
1466
|
+
}
|
|
1240
1467
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1468
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1469
|
+
- (UIVisualEffect *)backgroundEffectForColor:(UIColor *)color
|
|
1470
|
+
blur:(TrueSheetViewBackgroundBlur)blur API_AVAILABLE(ios(26.1)) {
|
|
1471
|
+
if (blur != TrueSheetViewBackgroundBlur::None) {
|
|
1472
|
+
return [UIBlurEffect effectWithStyle:[BlurUtil blurEffectStyleFromEnum:blur]];
|
|
1473
|
+
}
|
|
1474
|
+
return color ? [UIColorEffect effectWithColor:color] : nil;
|
|
1475
|
+
}
|
|
1476
|
+
#endif
|
|
1245
1477
|
|
|
1478
|
+
- (void)setupBackground {
|
|
1246
1479
|
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1247
1480
|
if (@available(iOS 26.1, *)) {
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1481
|
+
if (_usesBackgroundEffects) {
|
|
1482
|
+
self.sheet.backgroundEffect = [self backgroundEffectForColor:self.backgroundColor blur:self.backgroundBlur];
|
|
1483
|
+
self.view.backgroundColor = nil;
|
|
1484
|
+
_blurView.hidden = YES;
|
|
1485
|
+
_blurView.backgroundBlur = TrueSheetViewBackgroundBlur::None;
|
|
1486
|
+
[_blurView applyBlurEffect];
|
|
1487
|
+
return;
|
|
1254
1488
|
}
|
|
1255
1489
|
}
|
|
1256
1490
|
#endif
|
|
1257
1491
|
|
|
1258
|
-
|
|
1492
|
+
NSDictionary *entry = [self backgroundForIndex:_displayedBackgroundIndex];
|
|
1493
|
+
BOOL hasEntry = entry[@"color"] || entry[@"blur"];
|
|
1494
|
+
UIColor *color = hasEntry ? entry[@"color"] : self.backgroundColor;
|
|
1495
|
+
auto effectiveBackgroundBlur =
|
|
1496
|
+
hasEntry ? (entry[@"blur"] ? static_cast<TrueSheetViewBackgroundBlur>([entry[@"blur"] intValue])
|
|
1497
|
+
: TrueSheetViewBackgroundBlur::None)
|
|
1498
|
+
: self.backgroundBlur;
|
|
1499
|
+
if (@available(iOS 26.0, *)) {
|
|
1500
|
+
// iOS 26+ has default liquid glass effect
|
|
1501
|
+
} else if (effectiveBackgroundBlur == TrueSheetViewBackgroundBlur::None && !color) {
|
|
1502
|
+
effectiveBackgroundBlur = TrueSheetViewBackgroundBlur::SystemMaterial;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
BOOL hasBlur = effectiveBackgroundBlur != TrueSheetViewBackgroundBlur::None;
|
|
1506
|
+
|
|
1507
|
+
_blurView.hidden = NO;
|
|
1508
|
+
_blurView.backgroundBlur = effectiveBackgroundBlur;
|
|
1509
|
+
[_blurView applyBlurEffect];
|
|
1510
|
+
|
|
1511
|
+
self.view.backgroundColor = hasBlur ? nil : color;
|
|
1259
1512
|
}
|
|
1260
1513
|
|
|
1261
1514
|
- (void)setupGrabber {
|
|
@@ -1302,6 +1555,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1302
1555
|
}];
|
|
1303
1556
|
} else if (strongSelf.dismissible) {
|
|
1304
1557
|
[strongSelf.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
1558
|
+
} else {
|
|
1559
|
+
[strongSelf.delegate viewControllerDidAttemptDismiss];
|
|
1305
1560
|
}
|
|
1306
1561
|
};
|
|
1307
1562
|
|
|
@@ -1313,6 +1568,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1313
1568
|
_grabberView.onIncrement = nil;
|
|
1314
1569
|
_grabberView.onDecrement = nil;
|
|
1315
1570
|
}
|
|
1571
|
+
|
|
1572
|
+
// Only the topmost sheet may refresh, or a parent's prop update would steal the
|
|
1573
|
+
// window accessibility override from the child presented over it.
|
|
1574
|
+
if (_isPresented && self.isTopmostPresentedController) {
|
|
1575
|
+
[self setupAccessibilityContainer];
|
|
1576
|
+
}
|
|
1316
1577
|
}
|
|
1317
1578
|
|
|
1318
1579
|
- (void)handleGrabberTap {
|
|
@@ -1325,8 +1586,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1325
1586
|
return;
|
|
1326
1587
|
|
|
1327
1588
|
NSInteger nextIndex = (currentIndex + 1) % detentCount;
|
|
1328
|
-
if (nextIndex == 0 && detentCount == 1
|
|
1329
|
-
|
|
1589
|
+
if (nextIndex == 0 && detentCount == 1) {
|
|
1590
|
+
if (self.dismissible) {
|
|
1591
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
1592
|
+
} else {
|
|
1593
|
+
[self.delegate viewControllerDidAttemptDismiss];
|
|
1594
|
+
}
|
|
1330
1595
|
} else {
|
|
1331
1596
|
[self.sheet animateChanges:^{
|
|
1332
1597
|
[self resizeToDetentIndex:nextIndex];
|
|
@@ -1335,8 +1600,23 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1335
1600
|
}
|
|
1336
1601
|
|
|
1337
1602
|
- (BOOL)isAnchored {
|
|
1338
|
-
return self.
|
|
1603
|
+
return self.placement == TrueSheetViewPlacement::Leading || self.placement == TrueSheetViewPlacement::Trailing;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(27_0)
|
|
1607
|
+
- (UISheetPresentationControllerPlacement)preferredPlacement API_AVAILABLE(ios(27.0)) {
|
|
1608
|
+
switch (self.placement) {
|
|
1609
|
+
case TrueSheetViewPlacement::Leading:
|
|
1610
|
+
return UISheetPresentationControllerPlacementLeading;
|
|
1611
|
+
case TrueSheetViewPlacement::Center:
|
|
1612
|
+
return UISheetPresentationControllerPlacementCenter;
|
|
1613
|
+
case TrueSheetViewPlacement::Trailing:
|
|
1614
|
+
return UISheetPresentationControllerPlacementTrailing;
|
|
1615
|
+
case TrueSheetViewPlacement::Automatic:
|
|
1616
|
+
return UISheetPresentationControllerPlacementAutomatic;
|
|
1617
|
+
}
|
|
1339
1618
|
}
|
|
1619
|
+
#endif
|
|
1340
1620
|
|
|
1341
1621
|
- (void)setupAnchorViewInView:(UIView *)parentView {
|
|
1342
1622
|
if (!parentView)
|
|
@@ -1345,6 +1625,18 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1345
1625
|
[_anchorView removeFromSuperview];
|
|
1346
1626
|
_anchorView = nil;
|
|
1347
1627
|
|
|
1628
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(27_0)
|
|
1629
|
+
// iOS 27 exposes native placement. `sourceView` must stay nil since it overrides `preferredPlacement`.
|
|
1630
|
+
if (@available(iOS 27.0, *)) {
|
|
1631
|
+
if ([self.sheetPresentationController respondsToSelector:@selector(setPreferredPlacement:)]) {
|
|
1632
|
+
self.sheetPresentationController.sourceView = nil;
|
|
1633
|
+
self.sheetPresentationController.preferredPlacement = [self preferredPlacement];
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
#endif
|
|
1638
|
+
|
|
1639
|
+
// Pre-27 has no explicit center, so `center` falls back to the system default like `automatic`.
|
|
1348
1640
|
if (!self.isAnchored) {
|
|
1349
1641
|
self.sheetPresentationController.sourceView = nil;
|
|
1350
1642
|
return;
|
|
@@ -1356,7 +1648,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1356
1648
|
[parentView addSubview:_anchorView];
|
|
1357
1649
|
|
|
1358
1650
|
NSLayoutAnchor *horizontalAnchor =
|
|
1359
|
-
self.
|
|
1651
|
+
self.placement == TrueSheetViewPlacement::Trailing ? parentView.trailingAnchor : parentView.leadingAnchor;
|
|
1360
1652
|
|
|
1361
1653
|
[NSLayoutConstraint activateConstraints:@[
|
|
1362
1654
|
[_anchorView.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor],
|
|
@@ -1418,6 +1710,13 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1418
1710
|
return self.dismissible;
|
|
1419
1711
|
}
|
|
1420
1712
|
|
|
1713
|
+
// UIKit calls this after shouldDismiss returns NO — the sheet has already
|
|
1714
|
+
// bounced back, so JS can confirm and dismiss programmatically (Apple's
|
|
1715
|
+
// unsaved-changes pattern).
|
|
1716
|
+
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController {
|
|
1717
|
+
[self.delegate viewControllerDidAttemptDismiss];
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1421
1720
|
- (void)sheetPresentationControllerDidChangeSelectedDetentIdentifier:
|
|
1422
1721
|
(UISheetPresentationController *)sheetPresentationController {
|
|
1423
1722
|
dispatch_async(dispatch_get_main_queue(), ^{
|