@lodev09/react-native-true-sheet 4.0.0-beta.3 → 4.0.0-beta.30
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 +28 -5
- package/android/build.gradle +23 -1
- package/android/proguard-rules.pro +3 -0
- 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 +38 -109
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +127 -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 +29 -3
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +234 -66
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +491 -186
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +64 -10
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -14
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +135 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +23 -23
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +130 -6
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +14 -17
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +148 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +33 -40
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -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/android/src/main/jni/TrueSheetStateUpdater.cpp +37 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +114 -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 +14 -3
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +5 -6
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +10 -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 +23 -7
- package/ios/TrueSheetContainerView.mm +45 -25
- package/ios/TrueSheetContentView.h +44 -15
- package/ios/TrueSheetContentView.mm +221 -190
- package/ios/TrueSheetFooterView.mm +133 -28
- package/ios/TrueSheetModule.h +2 -7
- package/ios/TrueSheetModule.mm +18 -18
- package/ios/TrueSheetOverlayView.h +42 -0
- package/ios/TrueSheetOverlayView.mm +175 -0
- package/ios/TrueSheetView.h +2 -0
- package/ios/TrueSheetView.mm +268 -52
- package/ios/TrueSheetViewController.h +36 -3
- package/ios/TrueSheetViewController.mm +797 -102
- package/ios/core/RNScreensEventObserver.mm +31 -28
- package/ios/core/TrueSheetBlurView.h +0 -2
- package/ios/core/TrueSheetBlurView.mm +3 -36
- package/ios/core/TrueSheetDetentCalculator.h +18 -1
- package/ios/core/TrueSheetDetentCalculator.mm +44 -9
- package/ios/core/TrueSheetDimView.h +27 -0
- package/ios/core/TrueSheetDimView.mm +41 -0
- package/ios/core/TrueSheetGrabberView.mm +13 -3
- package/ios/core/TrueSheetKeyboardObserver.h +7 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +107 -15
- 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 +222 -54
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +364 -275
- 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/TrueSheetProvider.web.js +13 -9
- package/lib/module/TrueSheetProvider.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/TrueSheetFooterViewNativeComponent.ts +2 -3
- package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +89 -73
- 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 +158 -100
- 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/extendTrueSheetRouter.js +71 -0
- package/lib/module/navigation/expo-router/extendTrueSheetRouter.js.map +1 -0
- package/lib/module/navigation/expo-router/index.js +18 -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/scrollable.js +28 -0
- package/lib/module/scrollable.js.map +1 -0
- package/lib/module/web/constants.js +1 -1
- package/lib/module/web/constants.js.map +1 -1
- package/lib/module/web/keyboard.js +69 -0
- package/lib/module/web/keyboard.js.map +1 -0
- 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 +4 -0
- 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 +83 -21
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +57 -28
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +18 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +209 -35
- 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/TrueSheetProvider.web.d.ts +5 -1
- package/lib/typescript/src/TrueSheetProvider.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/TrueSheetFooterViewNativeComponent.d.ts +2 -3
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- 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 +76 -65
- 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 +36 -10
- 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/extendTrueSheetRouter.d.ts +33 -0
- package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts +9 -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 +43 -6
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/scrollable.d.ts +2 -0
- package/lib/typescript/src/scrollable.d.ts.map +1 -0
- 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/keyboard.d.ts +15 -0
- package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
- package/lib/typescript/src/web/layout.d.ts +20 -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 +5 -1
- 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 +20 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/types.d.ts +9 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +6 -4
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +18 -8
- package/react-native.config.js +1 -0
- package/src/TrueSheet.tsx +257 -62
- package/src/TrueSheet.types.ts +225 -35
- package/src/TrueSheet.web.tsx +445 -333
- 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/TrueSheetProvider.web.tsx +22 -13
- package/src/detentBackgrounds.ts +24 -0
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
- package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +89 -73
- 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 +220 -144
- package/src/navigation/expo-router/base-types.ts +18 -0
- package/src/navigation/expo-router/extendTrueSheetRouter.ts +99 -0
- package/src/navigation/expo-router/index.ts +41 -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 +81 -23
- package/src/scrollable.ts +25 -0
- package/src/web/constants.ts +1 -1
- package/src/web/keyboard.ts +79 -0
- package/src/web/layout.ts +196 -0
- package/src/web/vaul/constants.ts +9 -0
- package/src/web/vaul/context.ts +7 -1
- package/src/web/vaul/helpers.ts +5 -0
- package/src/web/vaul/index.tsx +113 -16
- package/src/web/vaul/types.ts +10 -0
- package/src/web/vaul/use-snap-points.ts +86 -33
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
#import "TrueSheetViewController.h"
|
|
10
10
|
#import "TrueSheetContainerView.h"
|
|
11
11
|
#import "TrueSheetContentView.h"
|
|
12
|
+
#import "TrueSheetOverlayView.h"
|
|
12
13
|
#import "core/TrueSheetBlurView.h"
|
|
13
14
|
#import "core/TrueSheetDetentCalculator.h"
|
|
15
|
+
#import "core/TrueSheetDimView.h"
|
|
14
16
|
#import "core/TrueSheetGrabberView.h"
|
|
15
17
|
#import "utils/BlurUtil.h"
|
|
16
18
|
#import "utils/GestureUtil.h"
|
|
17
19
|
#import "utils/PlatformUtil.h"
|
|
20
|
+
#import "utils/UIView+FirstResponder.h"
|
|
18
21
|
#import "utils/WindowUtil.h"
|
|
19
22
|
|
|
20
23
|
#import <React/RCTLog.h>
|
|
@@ -45,7 +48,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
45
48
|
- (void)restoreWindowAccessibilityElements;
|
|
46
49
|
- (void)setSheetAccessibilityElementsHidden:(BOOL)hidden;
|
|
47
50
|
- (void)setAccessibilityContentElement:(UIView *)contentView;
|
|
51
|
+
- (NSArray *)accessibilityGrabberElements;
|
|
48
52
|
- (void)endInteractiveDismissState;
|
|
53
|
+
- (void)updateHingeState;
|
|
54
|
+
- (void)correctHingeOffsetForIndex:(NSInteger)index;
|
|
55
|
+
- (CGFloat)tabletopHingeEdge;
|
|
56
|
+
- (CGFloat)hingeOffsetForHeight:(CGFloat)height;
|
|
49
57
|
- (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
|
|
50
58
|
duration:(NSTimeInterval)duration
|
|
51
59
|
allowUserInteraction:(BOOL)allowUserInteraction
|
|
@@ -53,17 +61,39 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
53
61
|
|
|
54
62
|
@end
|
|
55
63
|
|
|
64
|
+
// VoiceOver delivers the two-finger Z (escape) gesture by walking up the view
|
|
65
|
+
// hierarchy from the focused element, so the sheet's root view handles it.
|
|
66
|
+
@interface TrueSheetControllerView : UIView
|
|
67
|
+
@property (nonatomic, weak) TrueSheetViewController *controller;
|
|
68
|
+
@end
|
|
69
|
+
|
|
70
|
+
@implementation TrueSheetControllerView
|
|
71
|
+
|
|
72
|
+
- (BOOL)accessibilityPerformEscape {
|
|
73
|
+
return [self.controller accessibilityPerformEscape];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@end
|
|
77
|
+
|
|
56
78
|
@implementation TrueSheetViewController {
|
|
57
79
|
TrueSheetPositionState _lastEmittedPositionState;
|
|
58
80
|
CGSize _lastReportedSize;
|
|
81
|
+
// Net of the footer's baked-in inset, snapshotted together: the inset updates
|
|
82
|
+
// with the footer's layout, but a size change mid-present is deferred to
|
|
83
|
+
// didPresent, so subtracting it live pairs a new inset with a stale height.
|
|
59
84
|
CGFloat _autoDetentHeight;
|
|
60
85
|
CGFloat _peekDetentHeight;
|
|
61
86
|
NSInteger _pendingDetentIndex;
|
|
87
|
+
NSInteger _displayedBackgroundIndex;
|
|
88
|
+
BOOL _usesBackgroundEffects;
|
|
89
|
+
BOOL _refusedPanDismiss;
|
|
62
90
|
|
|
63
91
|
// Present/dismiss tracking: a display link scoped to the transition
|
|
64
92
|
// coordinator — started alongside the transition, stopped in its completion.
|
|
65
93
|
CADisplayLink *_transitionLink;
|
|
66
94
|
BOOL _isTransitioning;
|
|
95
|
+
// Where the sheet rests during the transition: the target on present, the start on dismiss
|
|
96
|
+
CGFloat _transitionRestPosition;
|
|
67
97
|
|
|
68
98
|
BOOL _pendingContentSizeChange;
|
|
69
99
|
BOOL _pendingDetentsChange;
|
|
@@ -74,6 +104,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
74
104
|
// for the next present (beats the float-threshold guess for re-presents).
|
|
75
105
|
BOOL _hasObservedBottomInset;
|
|
76
106
|
CGFloat _observedBottomInset;
|
|
107
|
+
BOOL _isRefreshingSafeArea;
|
|
77
108
|
|
|
78
109
|
BOOL _isInteractiveDismiss;
|
|
79
110
|
CGFloat _interactiveStartPosition;
|
|
@@ -82,14 +113,28 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
82
113
|
CADisplayLink *_interactivePositionLink;
|
|
83
114
|
|
|
84
115
|
__weak TrueSheetViewController *_parentSheetController;
|
|
116
|
+
__weak UIView *_presenterFirstResponder;
|
|
85
117
|
|
|
86
118
|
UIView *_anchorView;
|
|
87
119
|
|
|
88
120
|
__weak UIWindow *_accessibilityWindow;
|
|
121
|
+
BOOL _appliedAccessibilityFooterElements;
|
|
89
122
|
|
|
90
123
|
TrueSheetBlurView *_blurView;
|
|
91
124
|
TrueSheetGrabberView *_grabberView;
|
|
92
125
|
TrueSheetDetentCalculator *_detentCalculator;
|
|
126
|
+
TrueSheetDimView *_dimView;
|
|
127
|
+
|
|
128
|
+
// Foldable hinge (iOS 27.1+). Tabletop resolves on demand from the window's
|
|
129
|
+
// division region; these track the last applied posture to detect changes.
|
|
130
|
+
BOOL _hingeStateKnown;
|
|
131
|
+
CGFloat _hingeEdge;
|
|
132
|
+
NSInteger _hingeRestoreIndex;
|
|
133
|
+
NSInteger _hingeCorrections;
|
|
134
|
+
// Offset learned at the hinge, shared by every hinge-placed detent
|
|
135
|
+
CGFloat _hingeOffset;
|
|
136
|
+
// Offset each hinge-placed detent last resolved with, checked on settle
|
|
137
|
+
NSMutableDictionary<NSNumber *, NSNumber *> *_hingeOffsets;
|
|
93
138
|
}
|
|
94
139
|
|
|
95
140
|
#pragma mark - Initialization
|
|
@@ -97,6 +142,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
97
142
|
- (instancetype)init {
|
|
98
143
|
if (self = [super initWithNibName:nil bundle:nil]) {
|
|
99
144
|
_detents = @[ @0.5, @1 ];
|
|
145
|
+
_detentBackgrounds = @[];
|
|
146
|
+
_displayedBackgroundIndex = 0;
|
|
100
147
|
_contentHeight = @(0);
|
|
101
148
|
_headerHeight = @(0);
|
|
102
149
|
_footerHeight = @(0);
|
|
@@ -104,8 +151,11 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
104
151
|
_grabber = YES;
|
|
105
152
|
_draggable = YES;
|
|
106
153
|
_scrollingExpandsSheet = YES;
|
|
154
|
+
_footerAvoidsKeyboard = YES;
|
|
107
155
|
_dismissible = YES;
|
|
108
156
|
_dimmed = YES;
|
|
157
|
+
_dimColor = nil;
|
|
158
|
+
_dimOpacity = nil;
|
|
109
159
|
_dimmedDetentIndex = @(0);
|
|
110
160
|
_presentation = facebook::react::TrueSheetViewPresentation::Page;
|
|
111
161
|
_lastEmittedPositionState = (TrueSheetPositionState){0, 0, 0};
|
|
@@ -118,10 +168,16 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
118
168
|
_activeDetentIndex = -1;
|
|
119
169
|
_pendingDetentIndex = -1;
|
|
120
170
|
|
|
121
|
-
_blurInteraction = YES;
|
|
122
171
|
_insetAdjustment = TrueSheetViewInsetAdjustment::Automatic;
|
|
172
|
+
_hingeRestoreIndex = -1;
|
|
173
|
+
_hingeOffsets = [NSMutableDictionary dictionary];
|
|
123
174
|
_detentCalculator = [[TrueSheetDetentCalculator alloc] init];
|
|
124
175
|
_detentCalculator.delegate = self;
|
|
176
|
+
|
|
177
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
178
|
+
selector:@selector(overlayWindowDidChange)
|
|
179
|
+
name:TrueSheetOverlayWindowDidChangeNotification
|
|
180
|
+
object:nil];
|
|
125
181
|
}
|
|
126
182
|
return self;
|
|
127
183
|
}
|
|
@@ -206,11 +262,22 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
206
262
|
return self.currentPosition + (presentationY - modelY) + containerTy;
|
|
207
263
|
}
|
|
208
264
|
|
|
265
|
+
// Some callers run before this controller's view is attached (e.g. mid-presentation
|
|
266
|
+
// detent resolution), so fall back to the presenter's window.
|
|
267
|
+
- (UIWindow *)hostWindow {
|
|
268
|
+
return self.view.window ?: self.presentingViewController.view.window;
|
|
269
|
+
}
|
|
270
|
+
|
|
209
271
|
- (CGFloat)screenHeight {
|
|
210
|
-
|
|
272
|
+
// In a windowed iPad app (Stage Manager, iPadOS 26 windowing) the window can be shorter than
|
|
273
|
+
// UIScreen.mainScreen, so prefer the window's height.
|
|
274
|
+
UIWindow *window = self.hostWindow;
|
|
211
275
|
return window ? window.bounds.size.height : UIScreen.mainScreen.bounds.size.height;
|
|
212
276
|
}
|
|
213
277
|
|
|
278
|
+
// Gap UIKit leaves between a floating sheet (iOS 26+) and the screen's bottom edge
|
|
279
|
+
static const CGFloat TrueSheetFloatingSheetInset = 8;
|
|
280
|
+
|
|
214
281
|
// Only phone sheets absorb the bottom safe-area inset — non-phone idioms
|
|
215
282
|
// (iPad form sheets) are inset from the screen edge by UIKit.
|
|
216
283
|
static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
@@ -250,7 +317,6 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
250
317
|
* unpadded.
|
|
251
318
|
*/
|
|
252
319
|
- (CGFloat)maxNaturalDetentHeight {
|
|
253
|
-
CGFloat applied = [self.footerHeight floatValue] > 0 ? _appliedFooterBottomInset : 0;
|
|
254
320
|
CGFloat maxHeight = 0;
|
|
255
321
|
|
|
256
322
|
for (id detent in self.detents) {
|
|
@@ -261,11 +327,9 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
261
327
|
CGFloat value = [detent doubleValue];
|
|
262
328
|
CGFloat height = 0;
|
|
263
329
|
if (value == -1) {
|
|
264
|
-
|
|
265
|
-
height = [_detentCalculator autoHeight] - (_absoluteFooter ? 0 : applied);
|
|
330
|
+
height = [_detentCalculator autoHeightForKeyboardHeight:0] - [self autoDetentBottomInset];
|
|
266
331
|
} else if (value == -2) {
|
|
267
|
-
|
|
268
|
-
height = [_detentCalculator peekHeight] - (_absoluteFooter ? applied : 0);
|
|
332
|
+
height = [_detentCalculator peekHeight] - [self peekDetentBottomInset];
|
|
269
333
|
} else if (value > 0 && value <= 1) {
|
|
270
334
|
height = value * self.screenHeight;
|
|
271
335
|
}
|
|
@@ -297,6 +361,12 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
297
361
|
// wrong leaves the safe-area region unfilled (gap) or double-padded.
|
|
298
362
|
UIView *view = self.viewIfLoaded;
|
|
299
363
|
if (view.window) {
|
|
364
|
+
// Mid-present, hold the expected inset over a stale zero so the footer
|
|
365
|
+
// doesn't shrink and regrow — viewDidAppear forces the real value.
|
|
366
|
+
if (!_isPresented && [self hasStaleBottomSafeArea]) {
|
|
367
|
+
return [self bottomSafeAreaForHeight:view.bounds.size.height];
|
|
368
|
+
}
|
|
369
|
+
|
|
300
370
|
_hasObservedBottomInset = YES;
|
|
301
371
|
_observedBottomInset = view.safeAreaInsets.bottom;
|
|
302
372
|
return _observedBottomInset;
|
|
@@ -313,14 +383,27 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
313
383
|
return [self bottomSafeAreaForHeight:[self maxNaturalDetentHeight]];
|
|
314
384
|
}
|
|
315
385
|
|
|
386
|
+
// Re-resolve alongside the keyboard animation as its overlap with the pinned
|
|
387
|
+
// footer changes, including keyboard height changes while already visible.
|
|
388
|
+
- (void)setKeyboardHeight:(CGFloat)keyboardHeight {
|
|
389
|
+
if (_keyboardHeight == keyboardHeight) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
_keyboardHeight = keyboardHeight;
|
|
393
|
+
|
|
394
|
+
if (_footerInsetAdjustment && !_footerAvoidsKeyboard && self.isPresented) {
|
|
395
|
+
[self setupSheetDetentsForSizeChange];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
316
399
|
/**
|
|
317
|
-
* Bottom inset excluded from the auto detent. A relative footer
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* safe-area layout doesn't double it.
|
|
400
|
+
* Bottom inset excluded from the auto detent. A relative footer (or an
|
|
401
|
+
* absolute one padding the scrollable) owns the sheet's bottom edge and
|
|
402
|
+
* carries the inset in its height (see footerBottomInset) — subtract exactly
|
|
403
|
+
* what was baked in so UIKit's own safe-area layout doesn't double it.
|
|
321
404
|
*/
|
|
322
405
|
- (CGFloat)autoDetentBottomInset {
|
|
323
|
-
if (_absoluteFooter || [self.footerHeight floatValue] <= 0) {
|
|
406
|
+
if ((_absoluteFooter && !self.footerInsetAdjustment) || [self.footerHeight floatValue] <= 0) {
|
|
324
407
|
return 0;
|
|
325
408
|
}
|
|
326
409
|
|
|
@@ -341,6 +424,9 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
341
424
|
}
|
|
342
425
|
|
|
343
426
|
- (BOOL)isDesignCompatibilityMode {
|
|
427
|
+
if (@available(iOS 27.0, *)) {
|
|
428
|
+
return NO;
|
|
429
|
+
}
|
|
344
430
|
if (@available(iOS 26.0, *)) {
|
|
345
431
|
NSNumber *value = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"];
|
|
346
432
|
return value.boolValue;
|
|
@@ -377,6 +463,12 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
377
463
|
|
|
378
464
|
#pragma mark - View Lifecycle
|
|
379
465
|
|
|
466
|
+
- (void)loadView {
|
|
467
|
+
TrueSheetControllerView *view = [[TrueSheetControllerView alloc] init];
|
|
468
|
+
view.controller = self;
|
|
469
|
+
self.view = view;
|
|
470
|
+
}
|
|
471
|
+
|
|
380
472
|
- (void)viewDidLoad {
|
|
381
473
|
[super viewDidLoad];
|
|
382
474
|
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
|
|
@@ -387,6 +479,81 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
387
479
|
_grabberView = [[TrueSheetGrabberView alloc] init];
|
|
388
480
|
_grabberView.hidden = YES;
|
|
389
481
|
[_grabberView addToView:self.view];
|
|
482
|
+
|
|
483
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(27_1)
|
|
484
|
+
if (@available(iOS 27.1, *)) {
|
|
485
|
+
__weak __typeof(self) weakSelf = self;
|
|
486
|
+
UIHingeInteraction *hingeInteraction =
|
|
487
|
+
[[UIHingeInteraction alloc] initWithUpdateHandler:^(UIHingeInteraction *_, UIHingeInteractionUpdate *__) {
|
|
488
|
+
// Next run loop, so the division region reflects the new posture
|
|
489
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
490
|
+
[weakSelf updateHingeState];
|
|
491
|
+
});
|
|
492
|
+
}];
|
|
493
|
+
[self.view addInteraction:hingeInteraction];
|
|
494
|
+
}
|
|
495
|
+
#endif
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#pragma mark - Custom Dimming
|
|
499
|
+
|
|
500
|
+
// UISheetPresentationController has no API for the dim's color or opacity, so a custom
|
|
501
|
+
// color/opacity turns the native dim off and fades in our own view behind the sheet.
|
|
502
|
+
- (BOOL)useCustomDimming {
|
|
503
|
+
return self.dimmed && (self.dimColor != nil || self.dimOpacity != nil);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
- (BOOL)isDimmedAtEveryDetent {
|
|
507
|
+
return self.dimmed && [self.dimmedDetentIndex integerValue] == 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Like the system dim, fades in between the detent below `dimmedDetentIndex` and the dimmed detent.
|
|
511
|
+
- (CGFloat)dimAlphaForIndex:(CGFloat)index {
|
|
512
|
+
if (![self useCustomDimming]) {
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
CGFloat progress = index - [self.dimmedDetentIndex integerValue] + 1;
|
|
517
|
+
return fmax(0, fmin(1, progress));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Present and dismiss fade the dim over the whole travel below the resting position.
|
|
521
|
+
- (CGFloat)dimAlphaForPosition:(CGFloat)position index:(CGFloat)index {
|
|
522
|
+
if (!_isTransitioning || position <= _transitionRestPosition) {
|
|
523
|
+
return [self dimAlphaForIndex:index];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
CGFloat travel = self.screenHeight - _transitionRestPosition;
|
|
527
|
+
CGFloat progress = travel > 0 ? fmax(0, (self.screenHeight - position) / travel) : 0;
|
|
528
|
+
return [self dimAlphaForIndex:[self interpolatedIndexForPosition:_transitionRestPosition]] * progress;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
- (void)setupDimView {
|
|
532
|
+
UIView *container = self.presentationController.containerView;
|
|
533
|
+
if (!container || (!_dimView && ![self useCustomDimming])) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!_dimView) {
|
|
538
|
+
_dimView = [[TrueSheetDimView alloc] init];
|
|
539
|
+
|
|
540
|
+
__weak __typeof(self) weakSelf = self;
|
|
541
|
+
_dimView.onTap = ^{
|
|
542
|
+
[weakSelf handleDimViewTap];
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Keep opacity in the color so a transparent dim still receives touches.
|
|
547
|
+
UIColor *color = self.dimColor ?: [UIColor blackColor];
|
|
548
|
+
CGFloat opacity = self.dimOpacity ? [self.dimOpacity doubleValue] : 0.5;
|
|
549
|
+
_dimView.backgroundColor = [color colorWithAlphaComponent:CGColorGetAlpha(color.CGColor) * opacity];
|
|
550
|
+
[_dimView addToView:container];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
- (void)handleDimViewTap {
|
|
554
|
+
if (self.dismissible) {
|
|
555
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
556
|
+
}
|
|
390
557
|
}
|
|
391
558
|
|
|
392
559
|
- (void)viewWillAppear:(BOOL)animated {
|
|
@@ -402,6 +569,22 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
402
569
|
[_parentSheetController setAccessibilityContentElement:self.accessibilityContentView ?: self.view];
|
|
403
570
|
}
|
|
404
571
|
|
|
572
|
+
[self setupDimView];
|
|
573
|
+
|
|
574
|
+
// A new presentation records the posture it starts in
|
|
575
|
+
_hingeStateKnown = NO;
|
|
576
|
+
_hingeRestoreIndex = -1;
|
|
577
|
+
_hingeOffset = 0;
|
|
578
|
+
|
|
579
|
+
// UIKit hides the presenter's keyboard under a sheet dimmed at every detent and
|
|
580
|
+
// brings it back on dismiss, but custom dimming leaves the native sheet undimmed.
|
|
581
|
+
if ([self useCustomDimming] && [self isDimmedAtEveryDetent]) {
|
|
582
|
+
UIView *firstResponder = [presenter.view findFirstResponder];
|
|
583
|
+
if ([firstResponder resignFirstResponder]) {
|
|
584
|
+
_presenterFirstResponder = firstResponder;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
405
588
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
406
589
|
NSInteger index = self.currentDetentIndex;
|
|
407
590
|
CGFloat position = self.currentPosition;
|
|
@@ -430,15 +613,48 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
430
613
|
|
|
431
614
|
[self->_grabberView updateAccessibilityValueWithIndex:index detentCount:self->_detents.count];
|
|
432
615
|
[self settleAtDetentIndex:index debug:@"did present"];
|
|
616
|
+
|
|
617
|
+
// Apply a posture change that happened while presenting
|
|
618
|
+
[self updateHingeState];
|
|
433
619
|
});
|
|
434
620
|
|
|
435
621
|
[self setupGestureRecognizer];
|
|
436
622
|
_isPresented = YES;
|
|
623
|
+
[self refreshStaleBottomSafeArea];
|
|
437
624
|
}
|
|
438
625
|
|
|
439
626
|
[self setupAccessibilityContainer];
|
|
440
627
|
}
|
|
441
628
|
|
|
629
|
+
/**
|
|
630
|
+
* A present that overlaps another transition (e.g. re-presenting while a
|
|
631
|
+
* native stack pops) can leave the root view's bottom safe area at zero, and
|
|
632
|
+
* UIKit doesn't recompute it until the sheet's frame next changes. Suspect a
|
|
633
|
+
* zero the presented height says shouldn't be there — a small floating sheet
|
|
634
|
+
* legitimately has none.
|
|
635
|
+
*/
|
|
636
|
+
- (BOOL)hasStaleBottomSafeArea {
|
|
637
|
+
UIView *view = self.viewIfLoaded;
|
|
638
|
+
return view.window && view.safeAreaInsets.bottom <= 0 && [self bottomSafeAreaForHeight:view.bounds.size.height] > 0;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Toggling additionalSafeAreaInsets forces UIKit to recompute the safe area.
|
|
642
|
+
// The toggle's intermediate value isn't reported, so the footer only sees the
|
|
643
|
+
// settled one.
|
|
644
|
+
- (void)refreshStaleBottomSafeArea {
|
|
645
|
+
if (![self hasStaleBottomSafeArea]) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
_isRefreshingSafeArea = YES;
|
|
650
|
+
UIEdgeInsets insets = self.additionalSafeAreaInsets;
|
|
651
|
+
self.additionalSafeAreaInsets = UIEdgeInsetsMake(insets.top, insets.left, insets.bottom + 1, insets.right);
|
|
652
|
+
self.additionalSafeAreaInsets = insets;
|
|
653
|
+
_isRefreshingSafeArea = NO;
|
|
654
|
+
|
|
655
|
+
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
656
|
+
}
|
|
657
|
+
|
|
442
658
|
- (void)setupAccessibilityContainer {
|
|
443
659
|
UIView *contentView = self.accessibilityContentView;
|
|
444
660
|
if (!contentView) {
|
|
@@ -448,6 +664,43 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
448
664
|
[self setAccessibilityContentElement:contentView];
|
|
449
665
|
}
|
|
450
666
|
|
|
667
|
+
// Only the topmost sheet may refresh, or it would steal the window
|
|
668
|
+
// accessibility override from the child presented over it.
|
|
669
|
+
- (BOOL)canRefreshAccessibilityContainer {
|
|
670
|
+
return _isPresented && !self.isBeingDismissed && self.isTopmostPresentedController;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
- (void)refreshAccessibilityContainer {
|
|
674
|
+
if (self.canRefreshAccessibilityContainer) {
|
|
675
|
+
[self setupAccessibilityContainer];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
- (void)refreshAccessibilityContainerIfFooterChanged {
|
|
680
|
+
// Only a dimmed sheet's boundary depends on the footer
|
|
681
|
+
if (!_dimmed || !self.canRefreshAccessibilityContainer) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
UIView *contentView = self.accessibilityContentView;
|
|
686
|
+
if (![contentView isKindOfClass:[TrueSheetContainerView class]] ||
|
|
687
|
+
[(TrueSheetContainerView *)contentView hasAccessibilityFooterElements] == _appliedAccessibilityFooterElements) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
[self setAccessibilityContentElement:contentView];
|
|
692
|
+
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
- (void)overlayWindowDidChange {
|
|
696
|
+
if (!self.canRefreshAccessibilityContainer) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
[self setupAccessibilityContainer];
|
|
701
|
+
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
|
|
702
|
+
}
|
|
703
|
+
|
|
451
704
|
- (UIViewController *)accessibilityPresentingViewController {
|
|
452
705
|
UIViewController *presentingViewController = self.presentingViewController;
|
|
453
706
|
while ([presentingViewController isKindOfClass:[TrueSheetViewController class]] &&
|
|
@@ -484,24 +737,56 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
484
737
|
}
|
|
485
738
|
}
|
|
486
739
|
|
|
740
|
+
- (NSArray *)accessibilityGrabberElements {
|
|
741
|
+
if (_grabberView && !_grabberView.hidden) {
|
|
742
|
+
return @[ _grabberView ];
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// The system grabber is an accessibility element UIKit adds to the presented view
|
|
746
|
+
// (a sibling of our root view). Expose it so VoiceOver can resize or dismiss with it.
|
|
747
|
+
// UIKit also keeps a transparent bottom grabber there, so skip invisible views.
|
|
748
|
+
NSMutableArray *elements = [NSMutableArray array];
|
|
749
|
+
for (UIView *subview in self.presentationController.presentedView.subviews) {
|
|
750
|
+
if (subview != self.view && subview.isAccessibilityElement && !subview.hidden && subview.alpha > 0) {
|
|
751
|
+
[elements addObject:subview];
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return elements;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
- (BOOL)accessibilityPerformEscape {
|
|
758
|
+
if (!_isPresented) {
|
|
759
|
+
return NO;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (!self.dismissible) {
|
|
763
|
+
[self.delegate viewControllerDidAttemptDismiss];
|
|
764
|
+
return NO;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
768
|
+
return YES;
|
|
769
|
+
}
|
|
770
|
+
|
|
487
771
|
- (void)setAccessibilityContentElement:(UIView *)contentView {
|
|
488
772
|
BOOL hasAccessibilityFooterElements = [contentView isKindOfClass:[TrueSheetContainerView class]] &&
|
|
489
773
|
[(TrueSheetContainerView *)contentView hasAccessibilityFooterElements];
|
|
490
774
|
// Footer controls exposed as separate accessibility elements can be skipped
|
|
491
775
|
// by XCTest when the presented sheet is a hard modal accessibility boundary.
|
|
492
776
|
BOOL isAccessibilityModal = _dimmed && !hasAccessibilityFooterElements;
|
|
777
|
+
_appliedAccessibilityFooterElements = hasAccessibilityFooterElements;
|
|
493
778
|
|
|
494
779
|
// At a hard modal boundary XCTest skips nested elements, so flatten the
|
|
495
780
|
// container's children into the array. Otherwise expose the container itself:
|
|
496
781
|
// its accessibilityElements getter recomputes live, so a footer/content subtree
|
|
497
782
|
// that remounts (e.g. swapping a footer button on a state change) stays
|
|
498
783
|
// discoverable instead of leaving this snapshot pointing at destroyed views.
|
|
499
|
-
|
|
784
|
+
NSMutableArray *accessibilityElements = [[self accessibilityGrabberElements] mutableCopy];
|
|
500
785
|
if (isAccessibilityModal) {
|
|
501
786
|
NSArray *contentElements = contentView.accessibilityElements;
|
|
502
|
-
accessibilityElements
|
|
787
|
+
[accessibilityElements addObjectsFromArray:contentElements.count > 0 ? contentElements : @[ contentView ]];
|
|
503
788
|
} else {
|
|
504
|
-
accessibilityElements
|
|
789
|
+
[accessibilityElements addObject:contentView];
|
|
505
790
|
}
|
|
506
791
|
|
|
507
792
|
self.view.isAccessibilityElement = NO;
|
|
@@ -514,6 +799,15 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
514
799
|
presentedView.accessibilityViewIsModal = isAccessibilityModal;
|
|
515
800
|
presentedView.accessibilityElements = accessibilityElements;
|
|
516
801
|
|
|
802
|
+
// The system grabber is a sibling of our root view, so an escape performed while it
|
|
803
|
+
// is focused walks presentedView → window and never reaches TrueSheetControllerView.
|
|
804
|
+
if (@available(iOS 17.0, *)) {
|
|
805
|
+
__weak __typeof(self) weakSelf = self;
|
|
806
|
+
presentedView.accessibilityPerformEscapeBlock = ^BOOL {
|
|
807
|
+
return [weakSelf accessibilityPerformEscape];
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
517
811
|
UIWindow *window = self.view.window;
|
|
518
812
|
UIViewController *presentingViewController = [self accessibilityPresentingViewController];
|
|
519
813
|
if (window && presentingViewController.view) {
|
|
@@ -528,8 +822,12 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
528
822
|
}
|
|
529
823
|
objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, [NSValue valueWithNonretainedObject:self],
|
|
530
824
|
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
531
|
-
|
|
825
|
+
NSArray *sheetElements =
|
|
532
826
|
isAccessibilityModal ? @[ presentedView ] : @[ presentingViewController.view, presentedView ];
|
|
827
|
+
// Overlays render above every sheet. Listed first, since accessibility
|
|
828
|
+
// hit testing takes the first element containing the point.
|
|
829
|
+
window.accessibilityElements =
|
|
830
|
+
[[TrueSheetOverlayView overlayContainersInWindow:window] arrayByAddingObjectsFromArray:sheetElements];
|
|
533
831
|
return;
|
|
534
832
|
}
|
|
535
833
|
}
|
|
@@ -575,6 +873,13 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
575
873
|
[self.delegate viewControllerWillBlur];
|
|
576
874
|
[self.delegate viewControllerWillDismiss];
|
|
577
875
|
[_parentSheetController.delegate viewControllerWillFocus];
|
|
876
|
+
|
|
877
|
+
// Restored once the dismiss commits, like UIKit's own keyboard preservation.
|
|
878
|
+
UIView *firstResponder = _presenterFirstResponder;
|
|
879
|
+
_presenterFirstResponder = nil;
|
|
880
|
+
if (!self.presentingViewController.isBeingDismissed) {
|
|
881
|
+
[firstResponder becomeFirstResponder];
|
|
882
|
+
}
|
|
578
883
|
}
|
|
579
884
|
}
|
|
580
885
|
|
|
@@ -585,6 +890,9 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
585
890
|
_isWillDismissEmitted = NO;
|
|
586
891
|
_hasObservedBottomInset = NO;
|
|
587
892
|
|
|
893
|
+
[_dimView removeFromSuperview];
|
|
894
|
+
_dimView = nil;
|
|
895
|
+
|
|
588
896
|
[_anchorView removeFromSuperview];
|
|
589
897
|
_anchorView = nil;
|
|
590
898
|
|
|
@@ -603,13 +911,29 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
603
911
|
[self restoreWindowAccessibilityElements];
|
|
604
912
|
[self setSheetAccessibilityElementsHidden:YES];
|
|
605
913
|
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
914
|
+
// Dismissing with a child on top only animates the child out — this sheet
|
|
915
|
+
// doesn't move and isn't part of that transition, so fade its dim over it.
|
|
916
|
+
if (self.isBeingDismissed && self.presentedViewController) {
|
|
917
|
+
[UIView animateWithDuration:self.transitionCoordinator.transitionDuration
|
|
918
|
+
animations:^{
|
|
919
|
+
self->_dimView.alpha = 0;
|
|
920
|
+
}];
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// A swipe can still be cancelled, so emit once it commits on release. Any
|
|
924
|
+
// other dismiss (programmatic, outside tap, navigation) is already committed.
|
|
925
|
+
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
|
|
926
|
+
if (coordinator.isInteractive) {
|
|
927
|
+
__weak __typeof(self) weakSelf = self;
|
|
928
|
+
[coordinator
|
|
929
|
+
notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
930
|
+
if (!context.isCancelled) {
|
|
931
|
+
[weakSelf emitWillDismissEvents];
|
|
932
|
+
}
|
|
933
|
+
}];
|
|
934
|
+
} else {
|
|
935
|
+
[self emitWillDismissEvents];
|
|
936
|
+
}
|
|
613
937
|
|
|
614
938
|
[self setupTransitionTracker];
|
|
615
939
|
}
|
|
@@ -625,8 +949,11 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
625
949
|
}
|
|
626
950
|
|
|
627
951
|
// Dismissing with the keyboard up skips the hide notification — don't let
|
|
628
|
-
// the stale flag block learning on the next present
|
|
952
|
+
// the stale flag block learning on the next present, or a stale height keep
|
|
953
|
+
// the footer out of the auto detent.
|
|
629
954
|
_keyboardSheetGrown = NO;
|
|
955
|
+
_keyboardHeight = 0;
|
|
956
|
+
_minimizedKeyboardOverlap = 0;
|
|
630
957
|
|
|
631
958
|
[self emitDidDismissEvents];
|
|
632
959
|
}
|
|
@@ -639,24 +966,26 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
639
966
|
return;
|
|
640
967
|
}
|
|
641
968
|
|
|
642
|
-
|
|
969
|
+
// Mid-drag the frame tracks the finger — never settle or learn from it;
|
|
970
|
+
// pending changes keep their flags and settle on the first post-drag layout.
|
|
971
|
+
if ((_pendingContentSizeChange || _pendingDetentsChange) && !_isDragging) {
|
|
643
972
|
_pendingContentSizeChange = NO;
|
|
644
973
|
_pendingDetentsChange = NO;
|
|
645
974
|
[self settleAtDetentIndex:self.currentDetentIndex debug:@"layout"];
|
|
646
|
-
} else
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
975
|
+
} else {
|
|
976
|
+
// A child on top moves this sheet two ways: an animated collapse/restore
|
|
977
|
+
// step (presenting/dismissing) — emit the target non-realtime so JS
|
|
978
|
+
// animates to it — or direct per-frame re-layouts while the child drags,
|
|
979
|
+
// which stay realtime. Mid-drag no child is on top, so drags emit realtime.
|
|
651
980
|
BOOL realtime = self.presentedViewController == nil || !self.isPresentedViewAnimating;
|
|
652
981
|
|
|
653
982
|
// A layout pass at rest can nudge the frame by a sub-pixel amount
|
|
654
983
|
// (pixel-grid snapping after present), leaving the interpolated index
|
|
655
984
|
// slightly off the whole number. Re-learn when the frame is effectively
|
|
656
985
|
// at the current detent so the drift is absorbed instead of emitted.
|
|
657
|
-
// The tight threshold keeps real movement (e.g. scroll-driven expansion
|
|
658
|
-
// from being absorbed.
|
|
659
|
-
if (self.presentedViewController == nil && !self.isPresentedViewAnimating) {
|
|
986
|
+
// The tight threshold keeps real movement (e.g. scroll-driven expansion
|
|
987
|
+
// or a drag hovering near the detent) from being absorbed.
|
|
988
|
+
if (!_isDragging && self.presentedViewController == nil && !self.isPresentedViewAnimating) {
|
|
660
989
|
NSInteger index = self.currentDetentIndex;
|
|
661
990
|
CGFloat expectedHeight = [_detentCalculator resolvedHeightForIndex:index];
|
|
662
991
|
CGFloat actualHeight = self.screenHeight - self.currentPosition;
|
|
@@ -669,6 +998,25 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
669
998
|
}
|
|
670
999
|
}
|
|
671
1000
|
|
|
1001
|
+
// Rotation or an iPad window resize changes the height fractional detents
|
|
1002
|
+
// resolve against — re-resolve alongside the transition so the sheet lands on
|
|
1003
|
+
// the new height instead of waiting for a content-driven rebuild.
|
|
1004
|
+
- (void)viewWillTransitionToSize:(CGSize)size
|
|
1005
|
+
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
|
1006
|
+
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
|
1007
|
+
|
|
1008
|
+
if (@available(iOS 16.0, *)) {
|
|
1009
|
+
[coordinator
|
|
1010
|
+
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
1011
|
+
[self.sheet invalidateDetents];
|
|
1012
|
+
}
|
|
1013
|
+
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
1014
|
+
// Rotating a half-open foldable swaps between book and tabletop
|
|
1015
|
+
[self updateHingeState];
|
|
1016
|
+
}];
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
672
1020
|
// Fires when UIKit resolves the sheet's float/anchored placement (present,
|
|
673
1021
|
// detent resize crossing the float threshold, rotation). The footer's
|
|
674
1022
|
// absorbed inset follows the observed safe area — repush it right away so
|
|
@@ -678,7 +1026,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
678
1026
|
|
|
679
1027
|
// Mid-dismiss the frame moves off-screen and reports transient insets —
|
|
680
1028
|
// don't let them pollute the observed value (matches viewDidLayoutSubviews).
|
|
681
|
-
if (!self.isBeingDismissed) {
|
|
1029
|
+
if (!self.isBeingDismissed && !_isRefreshingSafeArea) {
|
|
682
1030
|
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
683
1031
|
}
|
|
684
1032
|
}
|
|
@@ -706,6 +1054,10 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
706
1054
|
}
|
|
707
1055
|
}
|
|
708
1056
|
|
|
1057
|
+
// The hinge callback can run before the division region updates; the
|
|
1058
|
+
// relayout UIKit does for the posture change always sees the new region.
|
|
1059
|
+
[self updateHingeState];
|
|
1060
|
+
|
|
709
1061
|
if (_pendingDetentIndex >= 0) {
|
|
710
1062
|
NSInteger pendingIndex = _pendingDetentIndex;
|
|
711
1063
|
_pendingDetentIndex = -1;
|
|
@@ -780,7 +1132,14 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
780
1132
|
_isDragging = YES;
|
|
781
1133
|
break;
|
|
782
1134
|
case UIGestureRecognizerStateChanged:
|
|
783
|
-
|
|
1135
|
+
// Layout owns the emit while the sheet resizes with the finger (a
|
|
1136
|
+
// layout pass is already queued), and the transition tick owns it
|
|
1137
|
+
// during an interactive dismiss. Translation-only movement (e.g.
|
|
1138
|
+
// dragging down to the first detent) triggers neither — emit here as
|
|
1139
|
+
// the backup.
|
|
1140
|
+
if (!_isTransitioning && !self.presentedView.layer.needsLayout && !self.viewIfLoaded.layer.needsLayout) {
|
|
1141
|
+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
|
|
1142
|
+
}
|
|
784
1143
|
break;
|
|
785
1144
|
case UIGestureRecognizerStateEnded:
|
|
786
1145
|
case UIGestureRecognizerStateCancelled: {
|
|
@@ -820,6 +1179,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
820
1179
|
}
|
|
821
1180
|
|
|
822
1181
|
_isTransitioning = YES;
|
|
1182
|
+
_transitionRestPosition = self.isBeingDismissed ? self.livePosition : self.currentPosition;
|
|
823
1183
|
|
|
824
1184
|
// Learn the resolver-vs-actual offset before emitting transition positions so
|
|
825
1185
|
// the interpolated index lands exactly on the target detent. The presented
|
|
@@ -855,37 +1215,32 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
855
1215
|
// Settle after a present, cancelled dismiss, or detent-snap transition.
|
|
856
1216
|
// Delayed because the presentedView frame isn't final until UIKit
|
|
857
1217
|
// completes its layout pass after the transition animation — learning
|
|
858
|
-
// here absorbs any sub-pixel drift since the earlier learns.
|
|
1218
|
+
// here absorbs any sub-pixel drift since the earlier learns. A dismiss
|
|
1219
|
+
// cancelled mid-drag leaves the finger down, so the drag end settles instead.
|
|
859
1220
|
if (strongSelf->_isPresented && !strongSelf.isBeingDismissed) {
|
|
860
1221
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
861
|
-
|
|
1222
|
+
if (!strongSelf->_isDragging) {
|
|
1223
|
+
[strongSelf settleAtDetentIndex:strongSelf.currentDetentIndex debug:@"transition end"];
|
|
1224
|
+
}
|
|
862
1225
|
});
|
|
863
1226
|
}
|
|
864
1227
|
}];
|
|
865
1228
|
}
|
|
866
1229
|
|
|
867
1230
|
- (void)handleTransitionTick {
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (self.isBeingDismissed) {
|
|
873
|
-
// Emit only once the dismiss is committed: on release UIKit moves the
|
|
874
|
-
// model frame off-screen, while a cancelled drag rewinds it back to the
|
|
875
|
-
// detent (isBeingDismissed stays YES during the rewind).
|
|
876
|
-
if (self.currentPosition >= self.screenHeight) {
|
|
877
|
-
[self emitWillDismissEvents];
|
|
878
|
-
}
|
|
1231
|
+
// Also covers the interactive dismiss phase (finger down) — layout passes
|
|
1232
|
+
// are skipped while transitioning, so the tick owns position there too.
|
|
1233
|
+
CGFloat position = self.livePosition;
|
|
879
1234
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1235
|
+
if (self.isBeingDismissed) {
|
|
1236
|
+
// Hide blur at the end of dismiss to prevent UIVisualEffectView
|
|
1237
|
+
// from causing a flicker/flash at the bottom edge of the sheet.
|
|
1238
|
+
if (self.screenHeight - position < 1) {
|
|
1239
|
+
_blurView.alpha = 0;
|
|
885
1240
|
}
|
|
886
|
-
|
|
887
|
-
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
|
|
888
1241
|
}
|
|
1242
|
+
|
|
1243
|
+
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
|
|
889
1244
|
}
|
|
890
1245
|
|
|
891
1246
|
/**
|
|
@@ -898,6 +1253,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
898
1253
|
// measured frame; the next unobstructed settle learns.
|
|
899
1254
|
if (self.presentedViewController == nil) {
|
|
900
1255
|
[self learnOffsetForDetentIndex:index];
|
|
1256
|
+
[self correctHingeOffsetForIndex:index];
|
|
901
1257
|
}
|
|
902
1258
|
|
|
903
1259
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:debug];
|
|
@@ -1018,12 +1374,31 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1018
1374
|
}
|
|
1019
1375
|
}
|
|
1020
1376
|
|
|
1377
|
+
if (self.isPresented && !self.isBeingDismissed && !_isWillDismissEmitted && !_usesBackgroundEffects) {
|
|
1378
|
+
NSInteger index = [_detentCalculator backgroundIndexForPosition:position displayedIndex:_displayedBackgroundIndex];
|
|
1379
|
+
if (index != _displayedBackgroundIndex) {
|
|
1380
|
+
_displayedBackgroundIndex = index;
|
|
1381
|
+
[UIView animateWithDuration:0.2
|
|
1382
|
+
delay:0
|
|
1383
|
+
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
|
|
1384
|
+
animations:^{
|
|
1385
|
+
[self setupBackground];
|
|
1386
|
+
}
|
|
1387
|
+
completion:nil];
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1021
1391
|
TrueSheetPositionState state = {
|
|
1022
1392
|
.position = position,
|
|
1023
1393
|
.detent = [self interpolatedDetentForPosition:position],
|
|
1024
1394
|
.index = [self interpolatedIndexForPosition:position],
|
|
1025
1395
|
};
|
|
1026
1396
|
|
|
1397
|
+
// A child on top only pushes this sheet back, its detent stays.
|
|
1398
|
+
if (!presented) {
|
|
1399
|
+
_dimView.alpha = [self dimAlphaForPosition:position index:state.index];
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1027
1402
|
// Settle (non-realtime) emits are authoritative and bypass the dedupe — a
|
|
1028
1403
|
// learn right before them can correct the interpolated index by less than
|
|
1029
1404
|
// the tolerance, and the corrected value must still reach JS.
|
|
@@ -1038,7 +1413,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1038
1413
|
}
|
|
1039
1414
|
|
|
1040
1415
|
- (void)learnOffsetForDetentIndex:(NSInteger)index {
|
|
1041
|
-
if (_keyboardSheetGrown) {
|
|
1416
|
+
if (_keyboardSheetGrown || _minimizedKeyboardOverlap > 0) {
|
|
1042
1417
|
return;
|
|
1043
1418
|
}
|
|
1044
1419
|
[_detentCalculator learnOffsetForDetentIndex:index];
|
|
@@ -1072,6 +1447,146 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1072
1447
|
return [_detentCalculator detentValueForIndex:index];
|
|
1073
1448
|
}
|
|
1074
1449
|
|
|
1450
|
+
#pragma mark - Hinge
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Bottom edge (window coordinates) of an active horizontal fold, or 0 when the
|
|
1454
|
+
* device isn't in tabletop. The division region is only active while the hinge
|
|
1455
|
+
* is partially open, so this also excludes flat and closed postures.
|
|
1456
|
+
*/
|
|
1457
|
+
- (CGFloat)tabletopHingeEdge {
|
|
1458
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(27_1)
|
|
1459
|
+
if (@available(iOS 27.1, *)) {
|
|
1460
|
+
UIWindow *window = self.hostWindow;
|
|
1461
|
+
for (UIViewReservedRegion *region in [window reservedRegionsOfKind:[UIViewReservedRegionKind divisionRegionKind]]) {
|
|
1462
|
+
CGRect frame = region.frame;
|
|
1463
|
+
if (region.isActive && frame.size.width > frame.size.height) {
|
|
1464
|
+
return CGRectGetMaxY(frame);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
#endif
|
|
1469
|
+
return 0;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Gap between a hinge-placed detent's resolved height and the sheet's actual top
|
|
1474
|
+
* edge: UIKit adds the bottom safe area, plus the floating sheet's inset. Every
|
|
1475
|
+
* hinge-placed detent rests at the same height, so they share one offset — the
|
|
1476
|
+
* first settle at the hinge learns it.
|
|
1477
|
+
*/
|
|
1478
|
+
- (CGFloat)hingeOffsetForHeight:(CGFloat)height {
|
|
1479
|
+
if (_hingeOffset > 0) {
|
|
1480
|
+
return _hingeOffset;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
return [self bottomSafeAreaForHeight:height] + TrueSheetFloatingSheetInset;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// A hinge-placed detent resolved with an off estimate lands a few points off
|
|
1487
|
+
// the hinge — re-resolve once the settle has learned the real offset.
|
|
1488
|
+
- (void)correctHingeOffsetForIndex:(NSInteger)index {
|
|
1489
|
+
NSNumber *usedOffset = _hingeOffsets[@(index)];
|
|
1490
|
+
if (!usedOffset || _hingeCorrections >= 2) {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
CGFloat learnedOffset = [_detentCalculator learnedOffsetForIndex:index];
|
|
1495
|
+
if (learnedOffset == 0 || fabs(learnedOffset - usedOffset.doubleValue) <= 1) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
_hingeOffset = learnedOffset;
|
|
1500
|
+
_hingeCorrections++;
|
|
1501
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1502
|
+
[self invalidateDetentsAnimated];
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
- (void)invalidateDetentsAnimated {
|
|
1507
|
+
if (@available(iOS 16.0, *)) {
|
|
1508
|
+
[self.sheet animateChanges:^{
|
|
1509
|
+
self->_pendingContentSizeChange = YES;
|
|
1510
|
+
[self.sheet invalidateDetents];
|
|
1511
|
+
}];
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* Re-resolves hinge-placed detents when the posture changes, and runs 'snap':
|
|
1517
|
+
* entering tabletop moves the sheet to the hinge detent, leaving it restores the
|
|
1518
|
+
* previous detent if the sheet is still on the hinge. The posture a presentation
|
|
1519
|
+
* starts in only records state, so presenting while already in tabletop doesn't
|
|
1520
|
+
* jump the sheet. A change while presenting, dismissing, or dragging stays
|
|
1521
|
+
* pending until the sheet is at rest again.
|
|
1522
|
+
*/
|
|
1523
|
+
- (void)updateHingeState {
|
|
1524
|
+
NSInteger hingeIndex = self.foldOptions.hingeDetentIndex;
|
|
1525
|
+
if (hingeIndex < 0 && self.foldOptions.hingeBehavior == TrueSheetViewHingeBehavior::None) {
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// Covered by a full-screen presentation, the sheet has no window to read the posture from
|
|
1530
|
+
if (!self.viewIfLoaded.window) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
CGFloat edge = [self tabletopHingeEdge];
|
|
1535
|
+
if (!_hingeStateKnown) {
|
|
1536
|
+
_hingeStateKnown = YES;
|
|
1537
|
+
_hingeEdge = edge;
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
if (!self.isPresented || self.isBeingDismissed || _isDragging || fabs(edge - _hingeEdge) < 0.5) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
BOOL entered = edge > 0 && _hingeEdge == 0;
|
|
1546
|
+
BOOL left = edge == 0 && _hingeEdge > 0;
|
|
1547
|
+
_hingeEdge = edge;
|
|
1548
|
+
_hingeOffset = 0;
|
|
1549
|
+
_hingeCorrections = 0;
|
|
1550
|
+
|
|
1551
|
+
NSInteger targetIndex = -1;
|
|
1552
|
+
NSInteger detentCount = (NSInteger)self.detents.count;
|
|
1553
|
+
BOOL snaps =
|
|
1554
|
+
self.foldOptions.hingeBehavior == TrueSheetViewHingeBehavior::Snap && hingeIndex >= 0 && hingeIndex < detentCount;
|
|
1555
|
+
if (entered && snaps) {
|
|
1556
|
+
_hingeRestoreIndex = self.currentDetentIndex;
|
|
1557
|
+
targetIndex = hingeIndex;
|
|
1558
|
+
} else if (left && _hingeRestoreIndex >= 0) {
|
|
1559
|
+
// Keep a detent the user picked while folded
|
|
1560
|
+
if (snaps && self.currentDetentIndex == hingeIndex) {
|
|
1561
|
+
targetIndex = MIN(_hingeRestoreIndex, detentCount - 1);
|
|
1562
|
+
}
|
|
1563
|
+
_hingeRestoreIndex = -1;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// A resize settles through the pending detent change
|
|
1567
|
+
BOOL resizes = targetIndex >= 0 && targetIndex != self.activeDetentIndex;
|
|
1568
|
+
|
|
1569
|
+
if (@available(iOS 16.0, *)) {
|
|
1570
|
+
[self.sheet animateChanges:^{
|
|
1571
|
+
[self.sheet invalidateDetents];
|
|
1572
|
+
if (resizes) {
|
|
1573
|
+
[self resizeToDetentIndex:targetIndex];
|
|
1574
|
+
}
|
|
1575
|
+
}];
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// UIKit often re-resolves detents for the posture change on its own before
|
|
1579
|
+
// this runs, so the invalidation above triggers no layout pass to settle in.
|
|
1580
|
+
// Settle once the frame is final, same as a pending detent change.
|
|
1581
|
+
if (!resizes) {
|
|
1582
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
1583
|
+
if (self.isPresented && !self.isBeingDismissed && !self->_isDragging && !self->_isTransitioning) {
|
|
1584
|
+
[self settleAtDetentIndex:self.currentDetentIndex debug:@"hinge"];
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1075
1590
|
#pragma mark - Sheet Configuration
|
|
1076
1591
|
|
|
1077
1592
|
/**
|
|
@@ -1083,8 +1598,8 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1083
1598
|
*/
|
|
1084
1599
|
- (void)setupSheetDetentsForSizeChange {
|
|
1085
1600
|
if (@available(iOS 16.0, *)) {
|
|
1086
|
-
CGFloat autoHeight = [_detentCalculator
|
|
1087
|
-
CGFloat peekHeight = [_detentCalculator peekHeight];
|
|
1601
|
+
CGFloat autoHeight = [_detentCalculator autoHeightForKeyboardHeight:_keyboardHeight] - [self autoDetentBottomInset];
|
|
1602
|
+
CGFloat peekHeight = [_detentCalculator peekHeight] - [self peekDetentBottomInset];
|
|
1088
1603
|
|
|
1089
1604
|
if (fabs(autoHeight - _autoDetentHeight) < 0.5 && fabs(peekHeight - _peekDetentHeight) < 0.5) {
|
|
1090
1605
|
return;
|
|
@@ -1093,12 +1608,19 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1093
1608
|
_autoDetentHeight = autoHeight;
|
|
1094
1609
|
_peekDetentHeight = peekHeight;
|
|
1095
1610
|
|
|
1611
|
+
[self invalidateDetentsAnimated];
|
|
1612
|
+
}
|
|
1613
|
+
// iOS 15: detents are fixed medium/large — size changes can't affect them.
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Re-resolves the detents in place, e.g. when an input to the resolver
|
|
1617
|
+
// changes after UIKit has already laid out the sheet.
|
|
1618
|
+
- (void)invalidateDetents {
|
|
1619
|
+
if (@available(iOS 16.0, *)) {
|
|
1096
1620
|
[self.sheet animateChanges:^{
|
|
1097
|
-
self->_pendingContentSizeChange = YES;
|
|
1098
1621
|
[self.sheet invalidateDetents];
|
|
1099
1622
|
}];
|
|
1100
1623
|
}
|
|
1101
|
-
// iOS 15: detents are fixed medium/large — size changes can't affect them.
|
|
1102
1624
|
}
|
|
1103
1625
|
|
|
1104
1626
|
- (void)setupSheetDetentsForDetentsChange {
|
|
@@ -1115,9 +1637,10 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1115
1637
|
|
|
1116
1638
|
NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
|
|
1117
1639
|
[_detentCalculator clearResolvedHeights];
|
|
1640
|
+
_hingeCorrections = 0;
|
|
1118
1641
|
|
|
1119
|
-
_autoDetentHeight = [_detentCalculator
|
|
1120
|
-
_peekDetentHeight = [_detentCalculator peekHeight];
|
|
1642
|
+
_autoDetentHeight = [_detentCalculator autoHeightForKeyboardHeight:_keyboardHeight] - [self autoDetentBottomInset];
|
|
1643
|
+
_peekDetentHeight = [_detentCalculator peekHeight] - [self peekDetentBottomInset];
|
|
1121
1644
|
|
|
1122
1645
|
for (NSInteger index = 0; index < self.detents.count; index++) {
|
|
1123
1646
|
id detent = self.detents[index];
|
|
@@ -1126,9 +1649,25 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1126
1649
|
}
|
|
1127
1650
|
|
|
1128
1651
|
[_detentCalculator setDetentCount:self.detents.count];
|
|
1652
|
+
_usesBackgroundEffects = [self canUseBackgroundEffects:detents];
|
|
1653
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1654
|
+
if (@available(iOS 26.1, *)) {
|
|
1655
|
+
if (_usesBackgroundEffects) {
|
|
1656
|
+
for (NSInteger index = 0; index < detents.count; index++) {
|
|
1657
|
+
NSDictionary *entry = [self backgroundForIndex:index];
|
|
1658
|
+
auto blur = entry[@"blur"] ? static_cast<TrueSheetViewBackgroundBlur>([entry[@"blur"] intValue])
|
|
1659
|
+
: TrueSheetViewBackgroundBlur::None;
|
|
1660
|
+
detents[index].backgroundEffect = [self backgroundEffectForColor:entry[@"color"] blur:blur];
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
#endif
|
|
1665
|
+
|
|
1129
1666
|
sheet.detents = detents;
|
|
1130
1667
|
|
|
1131
|
-
if (
|
|
1668
|
+
if ([self useCustomDimming]) {
|
|
1669
|
+
sheet.largestUndimmedDetentIdentifier = sheet.detents.lastObject.identifier;
|
|
1670
|
+
} else if ([self isDimmedAtEveryDetent]) {
|
|
1132
1671
|
sheet.largestUndimmedDetentIdentifier = nil;
|
|
1133
1672
|
} else {
|
|
1134
1673
|
sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
|
|
@@ -1164,7 +1703,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1164
1703
|
return [self customDetentWithIdentifier:@"custom-auto"
|
|
1165
1704
|
atIndex:index
|
|
1166
1705
|
heightBlock:^CGFloat {
|
|
1167
|
-
return self->_autoDetentHeight
|
|
1706
|
+
return self->_autoDetentHeight;
|
|
1168
1707
|
}];
|
|
1169
1708
|
} else {
|
|
1170
1709
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
@@ -1176,7 +1715,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1176
1715
|
return [self customDetentWithIdentifier:@"custom-peek"
|
|
1177
1716
|
atIndex:index
|
|
1178
1717
|
heightBlock:^CGFloat {
|
|
1179
|
-
return self->_peekDetentHeight
|
|
1718
|
+
return self->_peekDetentHeight;
|
|
1180
1719
|
}];
|
|
1181
1720
|
} else {
|
|
1182
1721
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
@@ -1190,8 +1729,13 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1190
1729
|
|
|
1191
1730
|
if (@available(iOS 16.0, *)) {
|
|
1192
1731
|
NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
|
|
1193
|
-
|
|
1194
|
-
|
|
1732
|
+
// Resolve lazily — detents are built before presentation (no window yet), and
|
|
1733
|
+
// UIKit re-resolves on container size changes (rotation, windowed iPad resize).
|
|
1734
|
+
return [self customDetentWithIdentifier:detentId
|
|
1735
|
+
atIndex:index
|
|
1736
|
+
heightBlock:^CGFloat {
|
|
1737
|
+
return value * self.screenHeight;
|
|
1738
|
+
}];
|
|
1195
1739
|
} else if (value >= 0.5) {
|
|
1196
1740
|
return [UISheetPresentationControllerDetent largeDetent];
|
|
1197
1741
|
} else {
|
|
@@ -1199,16 +1743,6 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1199
1743
|
}
|
|
1200
1744
|
}
|
|
1201
1745
|
|
|
1202
|
-
- (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
|
|
1203
|
-
height:(CGFloat)height
|
|
1204
|
-
atIndex:(NSInteger)index API_AVAILABLE(ios(16.0)) {
|
|
1205
|
-
return [self customDetentWithIdentifier:identifier
|
|
1206
|
-
atIndex:index
|
|
1207
|
-
heightBlock:^CGFloat {
|
|
1208
|
-
return height;
|
|
1209
|
-
}];
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
1746
|
- (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
|
|
1213
1747
|
atIndex:(NSInteger)index
|
|
1214
1748
|
heightBlock:(CGFloat (^)(void))heightBlock
|
|
@@ -1226,24 +1760,66 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1226
1760
|
: maxDetentValue;
|
|
1227
1761
|
|
|
1228
1762
|
CGFloat adjustedHeight = height - bottomAdjustment;
|
|
1763
|
+
NSInteger lastIndex = (NSInteger)self.detents.count - 1;
|
|
1764
|
+
|
|
1765
|
+
// Tabletop: the hinge detent rests on the hinge, and 'avoid'/'snap'
|
|
1766
|
+
// cap every detent there.
|
|
1767
|
+
BOOL avoidsHinge = self.foldOptions.hingeBehavior != TrueSheetViewHingeBehavior::None;
|
|
1768
|
+
NSInteger hingeIndex = self.foldOptions.hingeDetentIndex;
|
|
1769
|
+
BOOL hasHingeDetent = hingeIndex >= 0 && hingeIndex <= lastIndex;
|
|
1770
|
+
CGFloat hingeEdge = (hasHingeDetent || avoidsHinge) ? [self tabletopHingeEdge] : 0;
|
|
1771
|
+
CGFloat hingeOffset = 0;
|
|
1772
|
+
CGFloat hingeHeight = 0;
|
|
1773
|
+
if (hingeEdge > 0) {
|
|
1774
|
+
CGFloat panelHeight = self.screenHeight - hingeEdge;
|
|
1775
|
+
hingeOffset = [self hingeOffsetForHeight:panelHeight];
|
|
1776
|
+
hingeHeight = panelHeight - hingeOffset;
|
|
1777
|
+
if (hasHingeDetent) {
|
|
1778
|
+
// Lower detents stay below the hinge detent and higher ones above
|
|
1779
|
+
// it, so the detents keep their order
|
|
1780
|
+
CGFloat hingeDetentHeight =
|
|
1781
|
+
avoidsHinge ? hingeHeight - (lastIndex - hingeIndex) : hingeHeight;
|
|
1782
|
+
if (index == hingeIndex) {
|
|
1783
|
+
adjustedHeight = hingeHeight;
|
|
1784
|
+
} else if (index < hingeIndex) {
|
|
1785
|
+
adjustedHeight = fmin(adjustedHeight, hingeDetentHeight - (hingeIndex - index));
|
|
1786
|
+
} else {
|
|
1787
|
+
adjustedHeight = fmax(adjustedHeight, hingeDetentHeight + (index - hingeIndex));
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (avoidsHinge) {
|
|
1791
|
+
maxValue = fmin(maxValue, hingeHeight);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1229
1794
|
|
|
1230
1795
|
// Only the last detent may occupy the full maximum. Two detents
|
|
1231
1796
|
// resolving to the same height (e.g. a capped auto and 1) make
|
|
1232
1797
|
// UIKit skip the stack's deck animation when presenting a child
|
|
1233
1798
|
// sheet and snap the sheet behind into place instead.
|
|
1234
|
-
NSInteger lastIndex = (NSInteger)self.detents.count - 1;
|
|
1235
1799
|
if (adjustedHeight >= maxValue && index < lastIndex) {
|
|
1236
1800
|
maxValue -= lastIndex - index;
|
|
1237
1801
|
}
|
|
1238
1802
|
|
|
1239
1803
|
CGFloat resolved = fmin(adjustedHeight, maxValue);
|
|
1240
1804
|
|
|
1805
|
+
// Detents within the collapse spacing of the hinge are hinge-placed.
|
|
1806
|
+
// They rest with the hinge offset, so position events map them right
|
|
1807
|
+
// before the first settle there.
|
|
1808
|
+
if (hingeHeight > 0 && fabs(resolved - hingeHeight) <= lastIndex + 0.5) {
|
|
1809
|
+
self->_hingeOffsets[@(index)] = @(hingeOffset);
|
|
1810
|
+
[self->_detentCalculator assumeOffset:hingeOffset forIndex:index];
|
|
1811
|
+
} else {
|
|
1812
|
+
[self->_hingeOffsets removeObjectForKey:@(index)];
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1241
1815
|
NSMutableArray *heights = self->_detentCalculator.resolvedDetentHeights;
|
|
1242
1816
|
if (heights && index >= 0 && index < (NSInteger)heights.count) {
|
|
1243
1817
|
heights[index] = @(resolved);
|
|
1244
1818
|
}
|
|
1245
1819
|
|
|
1246
|
-
|
|
1820
|
+
// Recorded uncompensated above: UIKit adds the overlap back on
|
|
1821
|
+
// top. A sheet shorter than the overlap still grows by the rest.
|
|
1822
|
+
return fmax(resolved - self.minimizedKeyboardOverlap, 0);
|
|
1247
1823
|
}];
|
|
1248
1824
|
}
|
|
1249
1825
|
|
|
@@ -1298,6 +1874,8 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1298
1874
|
|
|
1299
1875
|
- (void)setupActiveDetentWithIndex:(NSInteger)index {
|
|
1300
1876
|
_activeDetentIndex = index;
|
|
1877
|
+
_displayedBackgroundIndex = index;
|
|
1878
|
+
[self setupBackground];
|
|
1301
1879
|
[self applyActiveDetent];
|
|
1302
1880
|
}
|
|
1303
1881
|
|
|
@@ -1311,34 +1889,72 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1311
1889
|
[self applyActiveDetent];
|
|
1312
1890
|
}
|
|
1313
1891
|
|
|
1314
|
-
- (
|
|
1315
|
-
|
|
1316
|
-
if (@available(iOS 26.
|
|
1317
|
-
//
|
|
1318
|
-
|
|
1319
|
-
|
|
1892
|
+
- (BOOL)canUseBackgroundEffects:(NSArray<UISheetPresentationControllerDetent *> *)detents {
|
|
1893
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1894
|
+
if (@available(iOS 26.1, *)) {
|
|
1895
|
+
// Some 26.1 builds omit setters declared in the SDK.
|
|
1896
|
+
if (self.isDesignCompatibilityMode || ![self.sheet respondsToSelector:@selector(setBackgroundEffect:)]) {
|
|
1897
|
+
return NO;
|
|
1898
|
+
}
|
|
1899
|
+
for (UISheetPresentationControllerDetent *detent in detents) {
|
|
1900
|
+
if (![detent respondsToSelector:@selector(setBackgroundEffect:)]) {
|
|
1901
|
+
return NO;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return YES;
|
|
1320
1905
|
}
|
|
1906
|
+
#endif
|
|
1907
|
+
return NO;
|
|
1908
|
+
}
|
|
1321
1909
|
|
|
1322
|
-
|
|
1910
|
+
- (NSDictionary *)backgroundForIndex:(NSInteger)index {
|
|
1911
|
+
return index >= 0 && index < self.detentBackgrounds.count ? self.detentBackgrounds[index] : nil;
|
|
1912
|
+
}
|
|
1323
1913
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1914
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1915
|
+
- (UIVisualEffect *)backgroundEffectForColor:(UIColor *)color
|
|
1916
|
+
blur:(TrueSheetViewBackgroundBlur)blur API_AVAILABLE(ios(26.1)) {
|
|
1917
|
+
if (blur != TrueSheetViewBackgroundBlur::None) {
|
|
1918
|
+
return [UIBlurEffect effectWithStyle:[BlurUtil blurEffectStyleFromEnum:blur]];
|
|
1919
|
+
}
|
|
1920
|
+
return color ? [UIColorEffect effectWithColor:color] : nil;
|
|
1921
|
+
}
|
|
1922
|
+
#endif
|
|
1328
1923
|
|
|
1924
|
+
- (void)setupBackground {
|
|
1329
1925
|
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1330
1926
|
if (@available(iOS 26.1, *)) {
|
|
1331
|
-
if (
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1927
|
+
if (_usesBackgroundEffects) {
|
|
1928
|
+
self.sheet.backgroundEffect = [self backgroundEffectForColor:self.backgroundColor blur:self.backgroundBlur];
|
|
1929
|
+
self.view.backgroundColor = nil;
|
|
1930
|
+
_blurView.hidden = YES;
|
|
1931
|
+
_blurView.backgroundBlur = TrueSheetViewBackgroundBlur::None;
|
|
1932
|
+
[_blurView applyBlurEffect];
|
|
1933
|
+
return;
|
|
1337
1934
|
}
|
|
1338
1935
|
}
|
|
1339
1936
|
#endif
|
|
1340
1937
|
|
|
1341
|
-
|
|
1938
|
+
NSDictionary *entry = [self backgroundForIndex:_displayedBackgroundIndex];
|
|
1939
|
+
BOOL hasEntry = entry[@"color"] || entry[@"blur"];
|
|
1940
|
+
UIColor *color = hasEntry ? entry[@"color"] : self.backgroundColor;
|
|
1941
|
+
auto effectiveBackgroundBlur =
|
|
1942
|
+
hasEntry ? (entry[@"blur"] ? static_cast<TrueSheetViewBackgroundBlur>([entry[@"blur"] intValue])
|
|
1943
|
+
: TrueSheetViewBackgroundBlur::None)
|
|
1944
|
+
: self.backgroundBlur;
|
|
1945
|
+
if (@available(iOS 26.0, *)) {
|
|
1946
|
+
// iOS 26+ has default liquid glass effect
|
|
1947
|
+
} else if (effectiveBackgroundBlur == TrueSheetViewBackgroundBlur::None && !color) {
|
|
1948
|
+
effectiveBackgroundBlur = TrueSheetViewBackgroundBlur::SystemMaterial;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
BOOL hasBlur = effectiveBackgroundBlur != TrueSheetViewBackgroundBlur::None;
|
|
1952
|
+
|
|
1953
|
+
_blurView.hidden = NO;
|
|
1954
|
+
_blurView.backgroundBlur = effectiveBackgroundBlur;
|
|
1955
|
+
[_blurView applyBlurEffect];
|
|
1956
|
+
|
|
1957
|
+
self.view.backgroundColor = hasBlur ? nil : color;
|
|
1342
1958
|
}
|
|
1343
1959
|
|
|
1344
1960
|
- (void)setupGrabber {
|
|
@@ -1385,6 +2001,8 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1385
2001
|
}];
|
|
1386
2002
|
} else if (strongSelf.dismissible) {
|
|
1387
2003
|
[strongSelf.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
2004
|
+
} else {
|
|
2005
|
+
[strongSelf.delegate viewControllerDidAttemptDismiss];
|
|
1388
2006
|
}
|
|
1389
2007
|
};
|
|
1390
2008
|
|
|
@@ -1396,6 +2014,8 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1396
2014
|
_grabberView.onIncrement = nil;
|
|
1397
2015
|
_grabberView.onDecrement = nil;
|
|
1398
2016
|
}
|
|
2017
|
+
|
|
2018
|
+
[self refreshAccessibilityContainer];
|
|
1399
2019
|
}
|
|
1400
2020
|
|
|
1401
2021
|
- (void)handleGrabberTap {
|
|
@@ -1408,8 +2028,12 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1408
2028
|
return;
|
|
1409
2029
|
|
|
1410
2030
|
NSInteger nextIndex = (currentIndex + 1) % detentCount;
|
|
1411
|
-
if (nextIndex == 0 && detentCount == 1
|
|
1412
|
-
|
|
2031
|
+
if (nextIndex == 0 && detentCount == 1) {
|
|
2032
|
+
if (self.dismissible) {
|
|
2033
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
2034
|
+
} else {
|
|
2035
|
+
[self.delegate viewControllerDidAttemptDismiss];
|
|
2036
|
+
}
|
|
1413
2037
|
} else {
|
|
1414
2038
|
[self.sheet animateChanges:^{
|
|
1415
2039
|
[self resizeToDetentIndex:nextIndex];
|
|
@@ -1418,9 +2042,24 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1418
2042
|
}
|
|
1419
2043
|
|
|
1420
2044
|
- (BOOL)isAnchored {
|
|
1421
|
-
return self.
|
|
2045
|
+
return self.placement == TrueSheetViewPlacement::Leading || self.placement == TrueSheetViewPlacement::Trailing;
|
|
1422
2046
|
}
|
|
1423
2047
|
|
|
2048
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(27_0)
|
|
2049
|
+
- (UISheetPresentationControllerPlacement)preferredPlacement API_AVAILABLE(ios(27.0)) {
|
|
2050
|
+
switch (self.placement) {
|
|
2051
|
+
case TrueSheetViewPlacement::Leading:
|
|
2052
|
+
return UISheetPresentationControllerPlacementLeading;
|
|
2053
|
+
case TrueSheetViewPlacement::Center:
|
|
2054
|
+
return UISheetPresentationControllerPlacementCenter;
|
|
2055
|
+
case TrueSheetViewPlacement::Trailing:
|
|
2056
|
+
return UISheetPresentationControllerPlacementTrailing;
|
|
2057
|
+
case TrueSheetViewPlacement::Automatic:
|
|
2058
|
+
return UISheetPresentationControllerPlacementAutomatic;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
#endif
|
|
2062
|
+
|
|
1424
2063
|
- (void)setupAnchorViewInView:(UIView *)parentView {
|
|
1425
2064
|
if (!parentView)
|
|
1426
2065
|
return;
|
|
@@ -1428,6 +2067,18 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1428
2067
|
[_anchorView removeFromSuperview];
|
|
1429
2068
|
_anchorView = nil;
|
|
1430
2069
|
|
|
2070
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(27_0)
|
|
2071
|
+
// iOS 27 exposes native placement. `sourceView` must stay nil since it overrides `preferredPlacement`.
|
|
2072
|
+
if (@available(iOS 27.0, *)) {
|
|
2073
|
+
if ([self.sheetPresentationController respondsToSelector:@selector(setPreferredPlacement:)]) {
|
|
2074
|
+
self.sheetPresentationController.sourceView = nil;
|
|
2075
|
+
self.sheetPresentationController.preferredPlacement = [self preferredPlacement];
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
#endif
|
|
2080
|
+
|
|
2081
|
+
// Pre-27 has no explicit center, so `center` falls back to the system default like `automatic`.
|
|
1431
2082
|
if (!self.isAnchored) {
|
|
1432
2083
|
self.sheetPresentationController.sourceView = nil;
|
|
1433
2084
|
return;
|
|
@@ -1439,7 +2090,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1439
2090
|
[parentView addSubview:_anchorView];
|
|
1440
2091
|
|
|
1441
2092
|
NSLayoutAnchor *horizontalAnchor =
|
|
1442
|
-
self.
|
|
2093
|
+
self.placement == TrueSheetViewPlacement::Trailing ? parentView.trailingAnchor : parentView.leadingAnchor;
|
|
1443
2094
|
|
|
1444
2095
|
[NSLayoutConstraint activateConstraints:@[
|
|
1445
2096
|
[_anchorView.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor],
|
|
@@ -1493,12 +2144,56 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1493
2144
|
|
|
1494
2145
|
[self setupBackground];
|
|
1495
2146
|
[self setupGrabber];
|
|
2147
|
+
|
|
2148
|
+
// Dim props changed while presented. A hidden custom dim stays until dismiss;
|
|
2149
|
+
// at alpha 0 it no longer takes touches.
|
|
2150
|
+
if (_isPresented) {
|
|
2151
|
+
[self setupDimView];
|
|
2152
|
+
_dimView.alpha = [self dimAlphaForIndex:self.currentDetentIndex];
|
|
2153
|
+
}
|
|
1496
2154
|
}
|
|
1497
2155
|
|
|
1498
2156
|
#pragma mark - UISheetPresentationControllerDelegate
|
|
1499
2157
|
|
|
1500
2158
|
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController {
|
|
1501
|
-
|
|
2159
|
+
// A sheet that can't be dragged can't be swiped away either, including by
|
|
2160
|
+
// pulling down its scroll view: refuse a dismissal while any pan is under
|
|
2161
|
+
// way, and keep the one from a tap outside the sheet. Reset on every call:
|
|
2162
|
+
// UIKit skips didAttemptToDismiss for a short pull.
|
|
2163
|
+
_refusedPanDismiss =
|
|
2164
|
+
self.dismissible && !self.draggable && [self hasActivePanInView:presentationController.containerView];
|
|
2165
|
+
return self.dismissible && !_refusedPanDismiss;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
- (BOOL)hasActivePanInView:(UIView *)view {
|
|
2169
|
+
if (!view) {
|
|
2170
|
+
return NO;
|
|
2171
|
+
}
|
|
2172
|
+
for (UIGestureRecognizer *recognizer in view.gestureRecognizers ?: @[]) {
|
|
2173
|
+
if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]] &&
|
|
2174
|
+
(recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged)) {
|
|
2175
|
+
return YES;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
for (UIView *subview in view.subviews) {
|
|
2179
|
+
if ([self hasActivePanInView:subview]) {
|
|
2180
|
+
return YES;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return NO;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// UIKit calls this after shouldDismiss returns NO — the sheet has already
|
|
2187
|
+
// bounced back, so JS can confirm and dismiss programmatically (Apple's
|
|
2188
|
+
// unsaved-changes pattern).
|
|
2189
|
+
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController {
|
|
2190
|
+
// A swipe refused only because the sheet can't be dragged isn't an attempt
|
|
2191
|
+
// to dismiss a non-dismissible sheet.
|
|
2192
|
+
if (_refusedPanDismiss) {
|
|
2193
|
+
_refusedPanDismiss = NO;
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
[self.delegate viewControllerDidAttemptDismiss];
|
|
1502
2197
|
}
|
|
1503
2198
|
|
|
1504
2199
|
- (void)sheetPresentationControllerDidChangeSelectedDetentIdentifier:
|