@lodev09/react-native-true-sheet 4.0.0-beta.0 → 4.0.0-beta.10
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/LICENSE +1 -1
- package/README.md +62 -30
- package/RNTrueSheet.podspec +10 -2
- package/android/build.gradle +24 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +189 -47
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +401 -19
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +18 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +145 -21
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +14 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderView.kt +38 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderViewManager.kt +28 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +84 -24
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +106 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +6 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekView.kt +72 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekViewManager.kt +33 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +63 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +564 -226
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +1399 -598
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +161 -29
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +63 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +36 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +255 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +375 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +299 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +162 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +180 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +207 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +220 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetDragEvents.kt +71 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetFocusEvents.kt +65 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +127 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetStateEvents.kt +56 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +84 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +93 -67
- package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +42 -0
- package/android/src/main/jni/CMakeLists.txt +63 -0
- package/android/src/main/jni/TrueSheetSpec.h +20 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +16 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +68 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +33 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +39 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +63 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +46 -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 +36 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +30 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +45 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +23 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +52 -0
- package/ios/TrueSheetContainerView.h +88 -6
- package/ios/TrueSheetContainerView.mm +278 -21
- package/ios/TrueSheetContentView.h +54 -1
- package/ios/TrueSheetContentView.mm +371 -47
- package/ios/TrueSheetFooterView.h +33 -2
- package/ios/TrueSheetFooterView.mm +187 -35
- package/ios/{events/OnWillDismissEvent.h → TrueSheetHeaderView.h} +10 -7
- package/ios/TrueSheetHeaderView.mm +64 -0
- package/ios/TrueSheetModule.h +2 -7
- package/ios/TrueSheetModule.mm +98 -20
- package/ios/TrueSheetOverlayView.h +32 -0
- package/ios/TrueSheetOverlayView.mm +161 -0
- package/ios/TrueSheetPeekView.h +32 -0
- package/ios/TrueSheetPeekView.mm +99 -0
- package/ios/TrueSheetView.h +7 -15
- package/ios/TrueSheetView.mm +775 -215
- package/ios/TrueSheetViewController.h +94 -16
- package/ios/TrueSheetViewController.mm +1316 -331
- package/ios/core/RNScreensEventObserver.h +46 -0
- package/ios/core/RNScreensEventObserver.mm +280 -0
- package/ios/core/TrueSheetBlurView.h +27 -0
- package/ios/core/TrueSheetBlurView.mm +82 -0
- package/ios/core/TrueSheetDetentCalculator.h +107 -0
- package/ios/core/TrueSheetDetentCalculator.mm +197 -0
- package/ios/core/TrueSheetGrabberView.h +72 -0
- package/ios/core/TrueSheetGrabberView.mm +188 -0
- package/ios/core/TrueSheetKeyboardObserver.h +39 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +112 -0
- package/ios/core/TrueSheetOverlayContainerView.h +31 -0
- package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
- package/ios/events/TrueSheetDragEvents.h +39 -0
- package/ios/events/TrueSheetDragEvents.mm +62 -0
- package/ios/events/{OnPositionChangeEvent.h → TrueSheetFocusEvents.h} +8 -5
- package/ios/events/TrueSheetFocusEvents.mm +49 -0
- package/ios/events/TrueSheetLifecycleEvents.h +42 -0
- package/ios/events/TrueSheetLifecycleEvents.mm +79 -0
- package/ios/events/TrueSheetStateEvents.h +35 -0
- package/ios/events/TrueSheetStateEvents.mm +49 -0
- package/ios/tvos/TrueSheetStubs.mm +166 -0
- package/ios/{events/OnMountEvent.h → utils/BlurUtil.h} +5 -7
- package/ios/utils/BlurUtil.mm +69 -0
- package/ios/utils/GestureUtil.h +7 -0
- package/ios/utils/GestureUtil.mm +12 -0
- package/ios/utils/PlatformUtil.h +15 -0
- package/ios/utils/UIView+FirstResponder.h +15 -0
- package/ios/utils/UIView+FirstResponder.mm +26 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.h +26 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +84 -0
- package/ios/utils/WindowUtil.mm +17 -1
- package/lib/module/TrueSheet.js +349 -74
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +1270 -0
- package/lib/module/TrueSheet.web.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.js +15 -0
- package/lib/module/TrueSheetPeek.js.map +1 -0
- package/lib/module/TrueSheetPeek.web.js +55 -0
- package/lib/module/TrueSheetPeek.web.js.map +1 -0
- package/lib/module/TrueSheetProvider.js +28 -0
- package/lib/module/TrueSheetProvider.js.map +1 -0
- package/lib/module/TrueSheetProvider.web.js +157 -0
- package/lib/module/TrueSheetProvider.web.js.map +1 -0
- package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/lib/module/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/lib/module/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +110 -18
- package/lib/module/index.js +4 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/mocks/index.js +103 -0
- package/lib/module/mocks/index.js.map +1 -0
- package/lib/module/mocks/navigation.js +95 -0
- package/lib/module/mocks/navigation.js.map +1 -0
- package/lib/module/mocks/navigationExpoRouter.js +21 -0
- package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
- package/lib/module/mocks/reanimated.js +91 -0
- package/lib/module/mocks/reanimated.js.map +1 -0
- package/lib/module/navigation/TrueSheetNavigatorContent.js +73 -0
- package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
- package/lib/module/navigation/actions.js +50 -0
- package/lib/module/navigation/actions.js.map +1 -0
- 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 +157 -0
- package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
- 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 +50 -0
- package/lib/module/navigation/expo-router/index.js.map +1 -0
- 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 +47 -0
- package/lib/module/navigation/index.js.map +1 -0
- package/lib/module/navigation/navigator.js +10 -0
- package/lib/module/navigation/navigator.js.map +1 -0
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +56 -0
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -0
- package/lib/module/navigation/screen/TrueSheetScreen.js +49 -0
- package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -0
- package/lib/module/navigation/screen/index.js +5 -0
- package/lib/module/navigation/screen/index.js.map +1 -0
- package/lib/module/navigation/screen/types.js +4 -0
- package/lib/module/navigation/screen/types.js.map +1 -0
- package/lib/module/navigation/screen/useSheetScreenState.js +69 -0
- package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -0
- package/lib/module/navigation/types.js +4 -0
- package/lib/module/navigation/types.js.map +1 -0
- package/lib/module/navigation/useTrueSheetNavigation.js +24 -0
- package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -0
- package/lib/module/reanimated/ReanimatedTrueSheet.js +15 -9
- package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
- package/lib/module/reanimated/ReanimatedTrueSheet.web.js +81 -0
- package/lib/module/reanimated/ReanimatedTrueSheet.web.js.map +1 -0
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +4 -2
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -1
- package/lib/module/reanimated/index.js +2 -2
- package/lib/module/reanimated/index.js.map +1 -1
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js +20 -0
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js.map +1 -0
- package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
- package/lib/module/web/constants.js +16 -0
- package/lib/module/web/constants.js.map +1 -0
- package/lib/module/web/dom.js +10 -0
- package/lib/module/web/dom.js.map +1 -0
- package/lib/module/web/vaul/README.md +9 -0
- package/lib/module/web/vaul/browser.js +30 -0
- package/lib/module/web/vaul/browser.js.map +1 -0
- package/lib/module/web/vaul/constants.js +19 -0
- package/lib/module/web/vaul/constants.js.map +1 -0
- package/lib/module/web/vaul/context.js +59 -0
- package/lib/module/web/vaul/context.js.map +1 -0
- package/lib/module/web/vaul/helpers.js +99 -0
- package/lib/module/web/vaul/helpers.js.map +1 -0
- package/lib/module/web/vaul/index.js +1342 -0
- package/lib/module/web/vaul/index.js.map +1 -0
- package/lib/module/web/vaul/style.css +287 -0
- package/lib/module/web/vaul/style.css.d.js +2 -0
- package/lib/module/web/vaul/style.css.d.js.map +1 -0
- package/lib/module/web/vaul/types.js +2 -0
- package/lib/module/web/vaul/types.js.map +1 -0
- package/lib/module/web/vaul/use-composed-refs.js +34 -0
- package/lib/module/web/vaul/use-composed-refs.js.map +1 -0
- package/lib/module/web/vaul/use-controllable-state.js +54 -0
- package/lib/module/web/vaul/use-controllable-state.js.map +1 -0
- package/lib/module/web/vaul/use-position-fixed.js +123 -0
- package/lib/module/web/vaul/use-position-fixed.js.map +1 -0
- package/lib/module/web/vaul/use-prevent-scroll.js +258 -0
- package/lib/module/web/vaul/use-prevent-scroll.js.map +1 -0
- package/lib/module/web/vaul/use-scale-background.js +57 -0
- package/lib/module/web/vaul/use-scale-background.js.map +1 -0
- package/lib/module/web/vaul/use-snap-points.js +305 -0
- package/lib/module/web/vaul/use-snap-points.js.map +1 -0
- package/lib/typescript/src/TrueSheet.d.ts +57 -14
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +535 -55
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts +5 -0
- package/lib/typescript/src/TrueSheet.web.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.d.ts +9 -0
- package/lib/typescript/src/TrueSheetPeek.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts +28 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetProvider.d.ts +18 -0
- package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +27 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.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/TrueSheetPeekViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +71 -16
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +47 -0
- package/lib/typescript/src/mocks/index.d.ts.map +1 -0
- package/lib/typescript/src/mocks/navigation.d.ts +70 -0
- package/lib/typescript/src/mocks/navigation.d.ts.map +1 -0
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
- package/lib/typescript/src/mocks/reanimated.d.ts +48 -0
- package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -0
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
- package/lib/typescript/src/navigation/actions.d.ts +71 -0
- package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
- 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 +3 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
- 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 +37 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
- 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 +45 -0
- package/lib/typescript/src/navigation/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/navigator.d.ts +7 -0
- package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +3 -0
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +3 -0
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/index.d.ts +4 -0
- package/lib/typescript/src/navigation/screen/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/types.d.ts +14 -0
- package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +34 -0
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -0
- package/lib/typescript/src/navigation/types.d.ts +213 -0
- package/lib/typescript/src/navigation/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +23 -0
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts +41 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +8 -2
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -1
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts +15 -0
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +25 -3
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
- package/lib/typescript/src/web/constants.d.ts +14 -0
- package/lib/typescript/src/web/constants.d.ts.map +1 -0
- package/lib/typescript/src/web/dom.d.ts +7 -0
- package/lib/typescript/src/web/dom.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/browser.d.ts +8 -0
- package/lib/typescript/src/web/vaul/browser.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts +14 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/context.d.ts +54 -0
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts +17 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/index.d.ts +234 -0
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/types.d.ts +7 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-composed-refs.d.ts +14 -0
- package/lib/typescript/src/web/vaul/use-composed-refs.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-controllable-state.d.ts +9 -0
- package/lib/typescript/src/web/vaul/use-controllable-state.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-position-fixed.d.ts +19 -0
- package/lib/typescript/src/web/vaul/use-position-fixed.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts +18 -0
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-scale-background.d.ts +2 -0
- package/lib/typescript/src/web/vaul/use-scale-background.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +38 -0
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -0
- package/package.json +104 -37
- package/react-native.config.js +10 -2
- package/src/TrueSheet.tsx +424 -70
- package/src/TrueSheet.types.ts +602 -72
- package/src/TrueSheet.web.tsx +1437 -0
- package/src/TrueSheetOverlay.tsx +24 -0
- package/src/TrueSheetOverlay.web.tsx +33 -0
- package/src/TrueSheetPeek.tsx +11 -0
- package/src/TrueSheetPeek.web.tsx +64 -0
- package/src/TrueSheetProvider.tsx +33 -0
- package/src/TrueSheetProvider.web.tsx +220 -0
- package/src/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/src/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/src/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +110 -18
- package/src/index.ts +3 -2
- package/src/mocks/index.ts +96 -0
- package/src/mocks/navigation.ts +79 -0
- package/src/mocks/navigationExpoRouter.ts +41 -0
- package/src/mocks/reanimated.ts +100 -0
- package/src/navigation/TrueSheetNavigatorContent.tsx +106 -0
- package/src/navigation/actions.ts +78 -0
- package/src/navigation/base-types.ts +20 -0
- package/src/navigation/createTrueSheetRouter.ts +194 -0
- package/src/navigation/expo-router/base-types.ts +18 -0
- package/src/navigation/expo-router/index.ts +90 -0
- package/src/navigation/expo-router/types.ts +116 -0
- package/src/navigation/index.ts +80 -0
- package/src/navigation/navigator.ts +18 -0
- package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +61 -0
- package/src/navigation/screen/TrueSheetScreen.tsx +54 -0
- package/src/navigation/screen/index.ts +3 -0
- package/src/navigation/screen/types.ts +19 -0
- package/src/navigation/screen/useSheetScreenState.ts +102 -0
- package/src/navigation/types.ts +302 -0
- package/src/navigation/useTrueSheetNavigation.ts +26 -0
- package/src/reanimated/ReanimatedTrueSheet.tsx +12 -7
- package/src/reanimated/ReanimatedTrueSheet.web.tsx +75 -0
- package/src/reanimated/ReanimatedTrueSheetProvider.tsx +12 -8
- package/src/reanimated/useReanimatedPositionChangeHandler.web.ts +31 -0
- package/src/specs/NativeTrueSheetModule.ts +28 -3
- package/src/web/constants.ts +13 -0
- package/src/web/dom.ts +9 -0
- package/src/web/vaul/README.md +9 -0
- package/src/web/vaul/browser.ts +38 -0
- package/src/web/vaul/constants.ts +24 -0
- package/src/web/vaul/context.ts +102 -0
- package/src/web/vaul/helpers.ts +131 -0
- package/src/web/vaul/index.tsx +1785 -0
- package/src/web/vaul/style.css +287 -0
- package/src/web/vaul/style.css.d.ts +1 -0
- package/src/web/vaul/types.ts +7 -0
- package/src/web/vaul/use-composed-refs.ts +35 -0
- package/src/web/vaul/use-controllable-state.ts +66 -0
- package/src/web/vaul/use-position-fixed.ts +147 -0
- package/src/web/vaul/use-prevent-scroll.ts +309 -0
- package/src/web/vaul/use-scale-background.ts +64 -0
- package/src/web/vaul/use-snap-points.ts +455 -0
- package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +0 -32
- package/android/src/main/java/com/lodev09/truesheet/events/SizeChangeEvent.kt +0 -27
- package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +0 -26
- package/android/src/main/res/values/styles.xml +0 -8
- package/ios/events/OnDetentChangeEvent.h +0 -28
- package/ios/events/OnDetentChangeEvent.mm +0 -30
- package/ios/events/OnDidDismissEvent.h +0 -26
- package/ios/events/OnDidDismissEvent.mm +0 -25
- package/ios/events/OnDidPresentEvent.h +0 -28
- package/ios/events/OnDidPresentEvent.mm +0 -30
- package/ios/events/OnDragBeginEvent.h +0 -28
- package/ios/events/OnDragBeginEvent.mm +0 -30
- package/ios/events/OnDragChangeEvent.h +0 -28
- package/ios/events/OnDragChangeEvent.mm +0 -30
- package/ios/events/OnDragEndEvent.h +0 -28
- package/ios/events/OnDragEndEvent.mm +0 -30
- package/ios/events/OnMountEvent.mm +0 -25
- package/ios/events/OnPositionChangeEvent.mm +0 -32
- package/ios/events/OnSizeChangeEvent.h +0 -28
- package/ios/events/OnSizeChangeEvent.mm +0 -30
- package/ios/events/OnWillDismissEvent.mm +0 -25
- package/ios/events/OnWillPresentEvent.h +0 -28
- package/ios/events/OnWillPresentEvent.mm +0 -30
- package/ios/utils/LayoutUtil.h +0 -44
- package/ios/utils/LayoutUtil.mm +0 -50
- package/lib/module/TrueSheetGrabber.js +0 -51
- package/lib/module/TrueSheetGrabber.js.map +0 -1
- package/lib/typescript/src/TrueSheetGrabber.d.ts +0 -39
- package/lib/typescript/src/TrueSheetGrabber.d.ts.map +0 -1
- package/src/TrueSheetGrabber.tsx +0 -82
- package/src/__mocks__/index.js +0 -67
|
@@ -7,200 +7,809 @@
|
|
|
7
7
|
//
|
|
8
8
|
|
|
9
9
|
#import "TrueSheetViewController.h"
|
|
10
|
+
#import "TrueSheetContainerView.h"
|
|
11
|
+
#import "TrueSheetContentView.h"
|
|
12
|
+
#import "core/TrueSheetBlurView.h"
|
|
13
|
+
#import "core/TrueSheetDetentCalculator.h"
|
|
14
|
+
#import "core/TrueSheetGrabberView.h"
|
|
15
|
+
#import "utils/BlurUtil.h"
|
|
16
|
+
#import "utils/GestureUtil.h"
|
|
17
|
+
#import "utils/PlatformUtil.h"
|
|
10
18
|
#import "utils/WindowUtil.h"
|
|
11
19
|
|
|
12
20
|
#import <React/RCTLog.h>
|
|
13
21
|
#import <React/RCTScrollViewComponentView.h>
|
|
14
|
-
#import
|
|
15
|
-
#import
|
|
22
|
+
#import <objc/runtime.h>
|
|
23
|
+
#import <react/renderer/components/TrueSheetSpec/Props.h>
|
|
24
|
+
|
|
25
|
+
using namespace facebook::react;
|
|
26
|
+
|
|
27
|
+
typedef struct {
|
|
28
|
+
CGFloat position;
|
|
29
|
+
CGFloat detent;
|
|
30
|
+
CGFloat index;
|
|
31
|
+
} TrueSheetPositionState;
|
|
32
|
+
|
|
33
|
+
static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPositionState b) {
|
|
34
|
+
return fabs(a.position - b.position) <= 0.01 && fabs(a.detent - b.detent) <= 0.01 && fabs(a.index - b.index) <= 0.01;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static char TrueSheetAccessibilityWindowOwnerKey;
|
|
38
|
+
static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
16
39
|
|
|
17
40
|
@interface TrueSheetViewController ()
|
|
18
41
|
|
|
42
|
+
- (UIViewController *)accessibilityPresentingViewController;
|
|
43
|
+
- (void)setupTransitionTracker;
|
|
44
|
+
- (void)settleAtDetentIndex:(NSInteger)index debug:(NSString *)debug;
|
|
45
|
+
- (void)restoreWindowAccessibilityElements;
|
|
46
|
+
- (void)setSheetAccessibilityElementsHidden:(BOOL)hidden;
|
|
47
|
+
- (void)setAccessibilityContentElement:(UIView *)contentView;
|
|
48
|
+
- (NSArray *)accessibilityGrabberElements;
|
|
49
|
+
- (void)endInteractiveDismissState;
|
|
50
|
+
- (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
|
|
51
|
+
duration:(NSTimeInterval)duration
|
|
52
|
+
allowUserInteraction:(BOOL)allowUserInteraction
|
|
53
|
+
completion:(void (^)(void))completion;
|
|
54
|
+
|
|
55
|
+
@end
|
|
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
|
+
|
|
19
69
|
@end
|
|
20
70
|
|
|
21
71
|
@implementation TrueSheetViewController {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
CGFloat
|
|
72
|
+
TrueSheetPositionState _lastEmittedPositionState;
|
|
73
|
+
CGSize _lastReportedSize;
|
|
74
|
+
CGFloat _autoDetentHeight;
|
|
75
|
+
CGFloat _peekDetentHeight;
|
|
76
|
+
NSInteger _pendingDetentIndex;
|
|
77
|
+
|
|
78
|
+
// Present/dismiss tracking: a display link scoped to the transition
|
|
79
|
+
// coordinator — started alongside the transition, stopped in its completion.
|
|
80
|
+
CADisplayLink *_transitionLink;
|
|
26
81
|
BOOL _isTransitioning;
|
|
82
|
+
|
|
83
|
+
BOOL _pendingContentSizeChange;
|
|
84
|
+
BOOL _pendingDetentsChange;
|
|
27
85
|
BOOL _isDragging;
|
|
28
|
-
BOOL
|
|
29
|
-
|
|
30
|
-
|
|
86
|
+
BOOL _isWillDismissEmitted;
|
|
87
|
+
|
|
88
|
+
// Last bottom safe area observed while presented — reused as the estimate
|
|
89
|
+
// for the next present (beats the float-threshold guess for re-presents).
|
|
90
|
+
BOOL _hasObservedBottomInset;
|
|
91
|
+
CGFloat _observedBottomInset;
|
|
92
|
+
|
|
93
|
+
BOOL _isInteractiveDismiss;
|
|
94
|
+
CGFloat _interactiveStartPosition;
|
|
95
|
+
UIView *_interactiveContainerView;
|
|
96
|
+
NSUInteger _interactiveGeneration;
|
|
97
|
+
CADisplayLink *_interactivePositionLink;
|
|
98
|
+
|
|
99
|
+
__weak TrueSheetViewController *_parentSheetController;
|
|
100
|
+
|
|
101
|
+
UIView *_anchorView;
|
|
102
|
+
|
|
103
|
+
__weak UIWindow *_accessibilityWindow;
|
|
104
|
+
|
|
105
|
+
TrueSheetBlurView *_blurView;
|
|
106
|
+
TrueSheetGrabberView *_grabberView;
|
|
107
|
+
TrueSheetDetentCalculator *_detentCalculator;
|
|
31
108
|
}
|
|
32
109
|
|
|
110
|
+
#pragma mark - Initialization
|
|
111
|
+
|
|
33
112
|
- (instancetype)init {
|
|
34
113
|
if (self = [super initWithNibName:nil bundle:nil]) {
|
|
35
114
|
_detents = @[ @0.5, @1 ];
|
|
36
115
|
_contentHeight = @(0);
|
|
116
|
+
_headerHeight = @(0);
|
|
117
|
+
_footerHeight = @(0);
|
|
118
|
+
_peekContentHeight = @(0);
|
|
37
119
|
_grabber = YES;
|
|
120
|
+
_draggable = YES;
|
|
121
|
+
_scrollingExpandsSheet = YES;
|
|
122
|
+
_dismissible = YES;
|
|
38
123
|
_dimmed = YES;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
_isTransitioning = NO;
|
|
124
|
+
_dimmedDetentIndex = @(0);
|
|
125
|
+
_presentation = facebook::react::TrueSheetViewPresentation::Page;
|
|
126
|
+
_lastEmittedPositionState = (TrueSheetPositionState){0, 0, 0};
|
|
43
127
|
_isDragging = NO;
|
|
44
|
-
_isTrackingPositionFromLayout = NO;
|
|
45
|
-
_layoutTransitioning = NO;
|
|
46
128
|
_isPresented = NO;
|
|
129
|
+
_isWillDismissEmitted = NO;
|
|
130
|
+
_isTransitioning = NO;
|
|
131
|
+
_pendingContentSizeChange = NO;
|
|
132
|
+
_pendingDetentsChange = NO;
|
|
47
133
|
_activeDetentIndex = -1;
|
|
134
|
+
_pendingDetentIndex = -1;
|
|
48
135
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
_fakeTransitionView = [[UIView alloc] init];
|
|
54
|
-
_fakeTransitionView.hidden = YES;
|
|
55
|
-
_fakeTransitionView.userInteractionEnabled = NO;
|
|
56
|
-
|
|
57
|
-
// Get bottom safe area inset from the window's safe area
|
|
58
|
-
// The sheet's view has smaller insets, so we need the actual device insets
|
|
59
|
-
UIWindow *window = [WindowUtil keyWindow];
|
|
60
|
-
_bottomInset = window ? window.safeAreaInsets.bottom : 0;
|
|
136
|
+
_blurInteraction = YES;
|
|
137
|
+
_insetAdjustment = TrueSheetViewInsetAdjustment::Automatic;
|
|
138
|
+
_detentCalculator = [[TrueSheetDetentCalculator alloc] init];
|
|
139
|
+
_detentCalculator.delegate = self;
|
|
61
140
|
}
|
|
62
141
|
return self;
|
|
63
142
|
}
|
|
64
143
|
|
|
65
144
|
- (void)dealloc {
|
|
145
|
+
[self restoreWindowAccessibilityElements];
|
|
146
|
+
[_transitionLink invalidate];
|
|
147
|
+
_transitionLink = nil;
|
|
148
|
+
[_interactivePositionLink invalidate];
|
|
149
|
+
_interactivePositionLink = nil;
|
|
66
150
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
67
151
|
}
|
|
68
152
|
|
|
69
|
-
#pragma mark -
|
|
153
|
+
#pragma mark - Computed Properties
|
|
154
|
+
|
|
155
|
+
- (UISheetPresentationController *)sheet {
|
|
156
|
+
return self.sheetPresentationController;
|
|
157
|
+
}
|
|
70
158
|
|
|
71
159
|
- (BOOL)isTopmostPresentedController {
|
|
72
|
-
// Check if we're in the window hierarchy and visible
|
|
73
160
|
if (!self.isViewLoaded || self.view.window == nil) {
|
|
74
161
|
return NO;
|
|
75
162
|
}
|
|
76
|
-
|
|
77
|
-
// Check if another controller is presented on top of this sheet
|
|
78
163
|
return self.presentedViewController == nil;
|
|
79
164
|
}
|
|
80
165
|
|
|
81
|
-
|
|
82
|
-
|
|
166
|
+
// YES while a child controller is stacked on top (e.g. a nested sheet) —
|
|
167
|
+
// UIKit's push-back scaling mutates the frame while the sheet is backgrounded.
|
|
168
|
+
- (BOOL)isStackedBehindChild {
|
|
169
|
+
UIViewController *presented = self.presentedViewController;
|
|
170
|
+
return presented != nil && !presented.isBeingDismissed;
|
|
83
171
|
}
|
|
84
172
|
|
|
85
173
|
- (UIView *)presentedView {
|
|
86
|
-
return self.
|
|
174
|
+
return self.sheet.presentedView;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
- (CGFloat)currentPosition {
|
|
178
|
+
UIView *presentedView = self.presentedView;
|
|
179
|
+
return presentedView ? presentedView.frame.origin.y : 0.0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// YES when any layer from the presented view up to the window has in-flight
|
|
183
|
+
// animations (detent snap, present/dismiss transition, container settle).
|
|
184
|
+
- (BOOL)isPresentedViewAnimating {
|
|
185
|
+
for (CALayer *layer = self.presentedView.layer; layer; layer = layer.superlayer) {
|
|
186
|
+
if (layer.animationKeys.count > 0) {
|
|
187
|
+
return YES;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return NO;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The sheet's live on-screen position. At rest and during drags (direct frame
|
|
195
|
+
* sets) this is the model position; during animations it adds the
|
|
196
|
+
* presentation-vs-model delta measured in window space, so ancestor animations
|
|
197
|
+
* compose and persistent transforms (e.g. floating-sheet inset) cancel out.
|
|
198
|
+
*/
|
|
199
|
+
- (CGFloat)livePosition {
|
|
200
|
+
UIView *presentedView = self.presentedView;
|
|
201
|
+
UIWindow *window = presentedView.window;
|
|
202
|
+
UIView *containerView = self.sheet.containerView;
|
|
203
|
+
|
|
204
|
+
// Direct (non-animated) translation applied during an interactive nav dismiss.
|
|
205
|
+
CGFloat containerTy = containerView ? containerView.transform.ty : 0;
|
|
206
|
+
|
|
207
|
+
if (!presentedView || !window || !self.isPresentedViewAnimating) {
|
|
208
|
+
return self.currentPosition + containerTy;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
CALayer *modelLayer = presentedView.layer;
|
|
212
|
+
CALayer *presentationLayer = modelLayer.presentationLayer;
|
|
213
|
+
CALayer *rootModel = window.layer;
|
|
214
|
+
CALayer *rootPresentation = rootModel.presentationLayer;
|
|
215
|
+
if (!presentationLayer || !rootPresentation) {
|
|
216
|
+
return self.currentPosition + containerTy;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
CGFloat modelY = [modelLayer convertPoint:CGPointZero toLayer:rootModel].y;
|
|
220
|
+
CGFloat presentationY = [presentationLayer convertPoint:CGPointZero toLayer:rootPresentation].y;
|
|
221
|
+
return self.currentPosition + (presentationY - modelY) + containerTy;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
- (CGFloat)screenHeight {
|
|
225
|
+
// In a windowed iPad app (Stage Manager, iPadOS 26 windowing) the window can be shorter than
|
|
226
|
+
// UIScreen.mainScreen, so prefer the window's height. Some callers run before this controller's
|
|
227
|
+
// view is attached (e.g. mid-presentation detent resolution), so fall back to the presenter's.
|
|
228
|
+
UIWindow *window = self.view.window ?: self.presentingViewController.view.window;
|
|
229
|
+
return window ? window.bounds.size.height : UIScreen.mainScreen.bounds.size.height;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Only phone sheets absorb the bottom safe-area inset — non-phone idioms
|
|
233
|
+
// (iPad form sheets) are inset from the screen edge by UIKit.
|
|
234
|
+
static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
235
|
+
return UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
- (CGFloat)detentBottomAdjustmentForHeight:(CGFloat)height {
|
|
239
|
+
if (_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic) {
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return [self bottomSafeAreaForHeight:height];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
- (CGFloat)bottomSafeAreaForHeight:(CGFloat)height {
|
|
247
|
+
if (!TrueSheetIsPhoneIdiom()) {
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// On iOS 26+, returns 0 for small detents (height <= 150)
|
|
252
|
+
// Floating sheets don't need adjustment
|
|
253
|
+
if (@available(iOS 26.0, *)) {
|
|
254
|
+
if (height <= 150) {
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
UIWindow *window = [WindowUtil keyWindow];
|
|
260
|
+
return window ? window.safeAreaInsets.bottom : 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Largest height the sheet can reach across its configured detents, excluding
|
|
265
|
+
* the inset currently baked into the footer's height. Basis for the footer's
|
|
266
|
+
* inset absorption so it follows the same rule as bottomSafeAreaForHeight: —
|
|
267
|
+
* a sheet that only ever floats (small detents on iOS 26) keeps its footer
|
|
268
|
+
* unpadded.
|
|
269
|
+
*/
|
|
270
|
+
- (CGFloat)maxNaturalDetentHeight {
|
|
271
|
+
CGFloat applied = [self.footerHeight floatValue] > 0 ? _appliedFooterBottomInset : 0;
|
|
272
|
+
CGFloat maxHeight = 0;
|
|
273
|
+
|
|
274
|
+
for (id detent in self.detents) {
|
|
275
|
+
if (![detent isKindOfClass:[NSNumber class]]) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
CGFloat value = [detent doubleValue];
|
|
280
|
+
CGFloat height = 0;
|
|
281
|
+
if (value == -1) {
|
|
282
|
+
// Auto counts a relative footer's (padded) height
|
|
283
|
+
height = [_detentCalculator autoHeight] - (_absoluteFooter ? 0 : applied);
|
|
284
|
+
} else if (value == -2) {
|
|
285
|
+
// Peek counts an absolute footer's (padded) height
|
|
286
|
+
height = [_detentCalculator peekHeight] - (_absoluteFooter ? applied : 0);
|
|
287
|
+
} else if (value > 0 && value <= 1) {
|
|
288
|
+
height = value * self.screenHeight;
|
|
289
|
+
}
|
|
290
|
+
maxHeight = fmax(maxHeight, height);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (self.maxContentHeight) {
|
|
294
|
+
maxHeight = fmin(maxHeight, [self.maxContentHeight floatValue]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return maxHeight > 0 ? maxHeight : self.screenHeight;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The inset the footer absorbs as padding.
|
|
301
|
+
- (CGFloat)footerBottomInset {
|
|
302
|
+
if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Early out so the maxNaturalDetentHeight fallback below doesn't run per
|
|
307
|
+
// layout pass on iPad only to be zeroed by bottomSafeAreaForHeight:.
|
|
308
|
+
if (!TrueSheetIsPhoneIdiom()) {
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Once laid out, the sheet's own safe area is the authority — it reflects
|
|
313
|
+
// UIKit's actual float/anchored decision (a floating sheet gets 0), where
|
|
314
|
+
// bottomSafeAreaForHeight: can only guess the float threshold. Guessing
|
|
315
|
+
// wrong leaves the safe-area region unfilled (gap) or double-padded.
|
|
316
|
+
UIView *view = self.viewIfLoaded;
|
|
317
|
+
if (view.window) {
|
|
318
|
+
_hasObservedBottomInset = YES;
|
|
319
|
+
_observedBottomInset = view.safeAreaInsets.bottom;
|
|
320
|
+
return _observedBottomInset;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Off-window while presented (e.g. hidden behind a native screen): the last
|
|
324
|
+
// observed value is this sheet's own truth — prefer it over the
|
|
325
|
+
// float-threshold guess. Cleared on dismiss so a re-present observes fresh
|
|
326
|
+
// geometry (rotation or detent changes while dismissed would stale it).
|
|
327
|
+
if (_hasObservedBottomInset) {
|
|
328
|
+
return _observedBottomInset;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return [self bottomSafeAreaForHeight:[self maxNaturalDetentHeight]];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Bottom inset excluded from the auto detent. A relative footer owns the
|
|
336
|
+
* sheet's bottom edge and carries the inset in its height (see
|
|
337
|
+
* footerBottomInset) — subtract exactly what was baked in so UIKit's own
|
|
338
|
+
* safe-area layout doesn't double it.
|
|
339
|
+
*/
|
|
340
|
+
- (CGFloat)autoDetentBottomInset {
|
|
341
|
+
if (_absoluteFooter || [self.footerHeight floatValue] <= 0) {
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return _appliedFooterBottomInset;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Bottom inset excluded from the peek detent. An absolute footer counts
|
|
350
|
+
* toward the peek height and carries the inset in its height — subtract
|
|
351
|
+
* exactly what was baked in so it isn't doubled.
|
|
352
|
+
*/
|
|
353
|
+
- (CGFloat)peekDetentBottomInset {
|
|
354
|
+
if (!_absoluteFooter || [self.footerHeight floatValue] <= 0) {
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return _appliedFooterBottomInset;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
- (BOOL)isDesignCompatibilityMode {
|
|
362
|
+
if (@available(iOS 26.0, *)) {
|
|
363
|
+
NSNumber *value = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"];
|
|
364
|
+
return value.boolValue;
|
|
365
|
+
}
|
|
366
|
+
return NO;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
- (NSInteger)currentDetentIndex {
|
|
370
|
+
UISheetPresentationController *sheet = self.sheet;
|
|
371
|
+
if (!sheet)
|
|
372
|
+
return -1;
|
|
373
|
+
|
|
374
|
+
UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
|
|
375
|
+
if (!selectedIdentifier)
|
|
376
|
+
return -1;
|
|
377
|
+
|
|
378
|
+
NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
|
|
379
|
+
for (NSInteger i = 0; i < detents.count; i++) {
|
|
380
|
+
if (@available(iOS 16.0, *)) {
|
|
381
|
+
if ([detents[i].identifier isEqualToString:selectedIdentifier]) {
|
|
382
|
+
return i;
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierMedium]) {
|
|
386
|
+
return 0;
|
|
387
|
+
} else if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierLarge]) {
|
|
388
|
+
return detents.count - 1;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return -1;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#pragma mark - View Lifecycle
|
|
397
|
+
|
|
398
|
+
- (void)loadView {
|
|
399
|
+
TrueSheetControllerView *view = [[TrueSheetControllerView alloc] init];
|
|
400
|
+
view.controller = self;
|
|
401
|
+
self.view = view;
|
|
87
402
|
}
|
|
88
403
|
|
|
89
404
|
- (void)viewDidLoad {
|
|
90
405
|
[super viewDidLoad];
|
|
91
|
-
|
|
92
406
|
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
|
|
93
|
-
|
|
94
|
-
[
|
|
407
|
+
|
|
408
|
+
_blurView = [[TrueSheetBlurView alloc] init];
|
|
409
|
+
[_blurView addToView:self.view];
|
|
410
|
+
|
|
411
|
+
_grabberView = [[TrueSheetGrabberView alloc] init];
|
|
412
|
+
_grabberView.hidden = YES;
|
|
413
|
+
[_grabberView addToView:self.view];
|
|
95
414
|
}
|
|
96
415
|
|
|
97
416
|
- (void)viewWillAppear:(BOOL)animated {
|
|
98
417
|
[super viewWillAppear:animated];
|
|
99
418
|
|
|
100
|
-
|
|
419
|
+
_blurView.alpha = 1;
|
|
420
|
+
|
|
101
421
|
if (!_isPresented) {
|
|
102
|
-
|
|
103
|
-
|
|
422
|
+
UIViewController *presenter = self.presentingViewController;
|
|
423
|
+
if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
|
|
424
|
+
_parentSheetController = (TrueSheetViewController *)presenter;
|
|
425
|
+
[_parentSheetController.delegate viewControllerWillBlur];
|
|
426
|
+
[_parentSheetController setAccessibilityContentElement:self.accessibilityContentView ?: self.view];
|
|
104
427
|
}
|
|
105
428
|
|
|
106
|
-
|
|
107
|
-
|
|
429
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
430
|
+
NSInteger index = self.currentDetentIndex;
|
|
431
|
+
CGFloat position = self.currentPosition;
|
|
432
|
+
CGFloat detent = [self detentValueForIndex:index];
|
|
433
|
+
|
|
434
|
+
[self.delegate viewControllerWillPresentAtIndex:index position:position detent:detent];
|
|
435
|
+
[self.delegate viewControllerWillFocus];
|
|
436
|
+
});
|
|
108
437
|
}
|
|
438
|
+
|
|
439
|
+
[self setupTransitionTracker];
|
|
109
440
|
}
|
|
110
441
|
|
|
111
442
|
- (void)viewDidAppear:(BOOL)animated {
|
|
112
443
|
[super viewDidAppear:animated];
|
|
444
|
+
[self setSheetAccessibilityElementsHidden:NO];
|
|
113
445
|
|
|
114
|
-
// Only trigger didPresent on the initial presentation, not on repositioning
|
|
115
446
|
if (!_isPresented) {
|
|
116
|
-
|
|
117
|
-
[self.delegate viewControllerDidPresent];
|
|
118
|
-
}
|
|
447
|
+
[_parentSheetController.delegate viewControllerDidBlur];
|
|
119
448
|
|
|
120
|
-
|
|
121
|
-
|
|
449
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
450
|
+
NSInteger index = [self currentDetentIndex];
|
|
451
|
+
CGFloat detent = [self detentValueForIndex:index];
|
|
452
|
+
[self.delegate viewControllerDidPresentAtIndex:index position:self.currentPosition detent:detent];
|
|
453
|
+
[self.delegate viewControllerDidFocus];
|
|
122
454
|
|
|
455
|
+
[self->_grabberView updateAccessibilityValueWithIndex:index detentCount:self->_detents.count];
|
|
456
|
+
[self settleAtDetentIndex:index debug:@"did present"];
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
[self setupGestureRecognizer];
|
|
123
460
|
_isPresented = YES;
|
|
124
461
|
}
|
|
462
|
+
|
|
463
|
+
[self setupAccessibilityContainer];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
- (void)setupAccessibilityContainer {
|
|
467
|
+
UIView *contentView = self.accessibilityContentView;
|
|
468
|
+
if (!contentView) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
[self setAccessibilityContentElement:contentView];
|
|
125
473
|
}
|
|
126
474
|
|
|
127
|
-
- (
|
|
128
|
-
|
|
475
|
+
- (UIViewController *)accessibilityPresentingViewController {
|
|
476
|
+
UIViewController *presentingViewController = self.presentingViewController;
|
|
477
|
+
while ([presentingViewController isKindOfClass:[TrueSheetViewController class]] &&
|
|
478
|
+
presentingViewController.presentingViewController) {
|
|
479
|
+
presentingViewController = presentingViewController.presentingViewController;
|
|
480
|
+
}
|
|
481
|
+
return presentingViewController;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
- (void)restoreWindowAccessibilityElements {
|
|
485
|
+
UIWindow *window = _accessibilityWindow;
|
|
486
|
+
if (window) {
|
|
487
|
+
// The active sheet may temporarily own window accessibility traversal. Only
|
|
488
|
+
// restore the previous value when this controller still owns that override.
|
|
489
|
+
NSValue *ownerValue = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey);
|
|
490
|
+
if (ownerValue && ownerValue.nonretainedObjectValue == self) {
|
|
491
|
+
id previousElements = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey);
|
|
492
|
+
window.accessibilityElements = previousElements == [NSNull null] ? nil : previousElements;
|
|
493
|
+
objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
494
|
+
objc_setAssociatedObject(
|
|
495
|
+
window, &TrueSheetAccessibilityWindowPreviousElementsKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
_accessibilityWindow = nil;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
- (void)setSheetAccessibilityElementsHidden:(BOOL)hidden {
|
|
503
|
+
self.view.accessibilityElementsHidden = hidden;
|
|
504
|
+
|
|
505
|
+
UIView *presentedView = self.presentationController.presentedView;
|
|
506
|
+
if (presentedView) {
|
|
507
|
+
presentedView.accessibilityElementsHidden = hidden;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
- (NSArray *)accessibilityGrabberElements {
|
|
512
|
+
if (_grabberView && !_grabberView.hidden) {
|
|
513
|
+
return @[ _grabberView ];
|
|
514
|
+
}
|
|
129
515
|
|
|
130
|
-
|
|
516
|
+
// The system grabber is an accessibility element UIKit adds to the presented view
|
|
517
|
+
// (a sibling of our root view). Expose it so VoiceOver can resize or dismiss with it.
|
|
518
|
+
// UIKit also keeps a transparent bottom grabber there, so skip invisible views.
|
|
519
|
+
NSMutableArray *elements = [NSMutableArray array];
|
|
520
|
+
for (UIView *subview in self.presentationController.presentedView.subviews) {
|
|
521
|
+
if (subview != self.view && subview.isAccessibilityElement && !subview.hidden && subview.alpha > 0) {
|
|
522
|
+
[elements addObject:subview];
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return elements;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
- (BOOL)accessibilityPerformEscape {
|
|
529
|
+
if (!_isPresented) {
|
|
530
|
+
return NO;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (!self.dismissible) {
|
|
534
|
+
[self.delegate viewControllerDidAttemptDismiss];
|
|
535
|
+
return NO;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
539
|
+
return YES;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
- (void)setAccessibilityContentElement:(UIView *)contentView {
|
|
543
|
+
BOOL hasAccessibilityFooterElements = [contentView isKindOfClass:[TrueSheetContainerView class]] &&
|
|
544
|
+
[(TrueSheetContainerView *)contentView hasAccessibilityFooterElements];
|
|
545
|
+
// Footer controls exposed as separate accessibility elements can be skipped
|
|
546
|
+
// by XCTest when the presented sheet is a hard modal accessibility boundary.
|
|
547
|
+
BOOL isAccessibilityModal = _dimmed && !hasAccessibilityFooterElements;
|
|
548
|
+
|
|
549
|
+
// At a hard modal boundary XCTest skips nested elements, so flatten the
|
|
550
|
+
// container's children into the array. Otherwise expose the container itself:
|
|
551
|
+
// its accessibilityElements getter recomputes live, so a footer/content subtree
|
|
552
|
+
// that remounts (e.g. swapping a footer button on a state change) stays
|
|
553
|
+
// discoverable instead of leaving this snapshot pointing at destroyed views.
|
|
554
|
+
NSMutableArray *accessibilityElements = [[self accessibilityGrabberElements] mutableCopy];
|
|
555
|
+
if (isAccessibilityModal) {
|
|
556
|
+
NSArray *contentElements = contentView.accessibilityElements;
|
|
557
|
+
[accessibilityElements addObjectsFromArray:contentElements.count > 0 ? contentElements : @[ contentView ]];
|
|
558
|
+
} else {
|
|
559
|
+
[accessibilityElements addObject:contentView];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
self.view.isAccessibilityElement = NO;
|
|
563
|
+
self.view.accessibilityViewIsModal = YES;
|
|
564
|
+
self.view.accessibilityElements = accessibilityElements;
|
|
565
|
+
|
|
566
|
+
UIView *presentedView = self.presentationController.presentedView;
|
|
567
|
+
if (presentedView) {
|
|
568
|
+
presentedView.isAccessibilityElement = NO;
|
|
569
|
+
presentedView.accessibilityViewIsModal = isAccessibilityModal;
|
|
570
|
+
presentedView.accessibilityElements = accessibilityElements;
|
|
571
|
+
|
|
572
|
+
// The system grabber is a sibling of our root view, so an escape performed while it
|
|
573
|
+
// is focused walks presentedView → window and never reaches TrueSheetControllerView.
|
|
574
|
+
if (@available(iOS 17.0, *)) {
|
|
575
|
+
__weak __typeof(self) weakSelf = self;
|
|
576
|
+
presentedView.accessibilityPerformEscapeBlock = ^BOOL {
|
|
577
|
+
return [weakSelf accessibilityPerformEscape];
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
UIWindow *window = self.view.window;
|
|
582
|
+
UIViewController *presentingViewController = [self accessibilityPresentingViewController];
|
|
583
|
+
if (window && presentingViewController.view) {
|
|
584
|
+
if (!_accessibilityWindow) {
|
|
585
|
+
_accessibilityWindow = window;
|
|
586
|
+
id previousElements = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey);
|
|
587
|
+
if (!previousElements) {
|
|
588
|
+
previousElements = window.accessibilityElements ?: [NSNull null];
|
|
589
|
+
objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey, previousElements,
|
|
590
|
+
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, [NSValue valueWithNonretainedObject:self],
|
|
594
|
+
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
595
|
+
window.accessibilityElements =
|
|
596
|
+
isAccessibilityModal ? @[ presentedView ] : @[ presentingViewController.view, presentedView ];
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
[self restoreWindowAccessibilityElements];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#pragma mark - Presentation
|
|
605
|
+
|
|
606
|
+
- (void)presentViewController:(UIViewController *)viewControllerToPresent
|
|
607
|
+
animated:(BOOL)flag
|
|
608
|
+
completion:(void (^)(void))completion {
|
|
609
|
+
// A non-sheet controller (e.g. an action sheet, alert, or image picker) is about
|
|
610
|
+
// to present over us. Clear our window accessibility override so UIKit's default
|
|
611
|
+
// traversal surfaces it for XCTest and assistive technologies; we reclaim the
|
|
612
|
+
// override when it dismisses. Nested sheets claim their own override.
|
|
613
|
+
if (![viewControllerToPresent isKindOfClass:[TrueSheetViewController class]]) {
|
|
614
|
+
self.view.window.accessibilityElements = nil;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
[super presentViewController:viewControllerToPresent animated:flag completion:completion];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
|
|
621
|
+
[super dismissViewControllerAnimated:flag
|
|
622
|
+
completion:^{
|
|
623
|
+
if (completion) {
|
|
624
|
+
completion();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Reclaim the window accessibility override once nothing
|
|
628
|
+
// else is presented over us.
|
|
629
|
+
if (self.isPresented && self.presentedViewController == nil) {
|
|
630
|
+
[self setupAccessibilityContainer];
|
|
631
|
+
}
|
|
632
|
+
}];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
- (void)emitWillDismissEvents {
|
|
636
|
+
if (self.isBeingDismissed && !_isWillDismissEmitted) {
|
|
637
|
+
_isWillDismissEmitted = YES;
|
|
638
|
+
|
|
639
|
+
[self.delegate viewControllerWillBlur];
|
|
131
640
|
[self.delegate viewControllerWillDismiss];
|
|
641
|
+
[_parentSheetController.delegate viewControllerWillFocus];
|
|
132
642
|
}
|
|
643
|
+
}
|
|
133
644
|
|
|
134
|
-
|
|
135
|
-
|
|
645
|
+
- (void)emitDidDismissEvents {
|
|
646
|
+
if (self.isBeingDismissed) {
|
|
647
|
+
[self restoreWindowAccessibilityElements];
|
|
648
|
+
_isPresented = NO;
|
|
649
|
+
_isWillDismissEmitted = NO;
|
|
650
|
+
_hasObservedBottomInset = NO;
|
|
651
|
+
|
|
652
|
+
[_anchorView removeFromSuperview];
|
|
653
|
+
_anchorView = nil;
|
|
654
|
+
|
|
655
|
+
[_parentSheetController.delegate viewControllerDidFocus];
|
|
656
|
+
[_parentSheetController setSheetAccessibilityElementsHidden:NO];
|
|
657
|
+
[_parentSheetController setupAccessibilityContainer];
|
|
658
|
+
_parentSheetController = nil;
|
|
659
|
+
|
|
660
|
+
[self.delegate viewControllerDidBlur];
|
|
661
|
+
[self.delegate viewControllerDidDismiss];
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
- (void)viewWillDisappear:(BOOL)animated {
|
|
666
|
+
[super viewWillDisappear:animated];
|
|
667
|
+
[self restoreWindowAccessibilityElements];
|
|
668
|
+
[self setSheetAccessibilityElementsHidden:YES];
|
|
669
|
+
|
|
670
|
+
// Dispatch to allow pan gesture to set _isDragging before checking;
|
|
671
|
+
// the transition tracker emits when the sheet is transitioning to dismiss
|
|
672
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
673
|
+
if (!self->_isDragging) {
|
|
674
|
+
[self emitWillDismissEvents];
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
[self setupTransitionTracker];
|
|
136
679
|
}
|
|
137
680
|
|
|
138
681
|
- (void)viewDidDisappear:(BOOL)animated {
|
|
139
682
|
[super viewDidDisappear:animated];
|
|
140
|
-
|
|
141
|
-
|
|
683
|
+
|
|
684
|
+
// Backstop: if the sheet is torn down mid-gesture (e.g. owning view deallocated),
|
|
685
|
+
// the observer can't reach us through its weak delegate, so end here to invalidate
|
|
686
|
+
// _interactivePositionLink — which otherwise retains this controller indefinitely.
|
|
687
|
+
if (_isInteractiveDismiss) {
|
|
688
|
+
[self endInteractiveDismissState];
|
|
142
689
|
}
|
|
143
690
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
691
|
+
// Dismissing with the keyboard up skips the hide notification — don't let
|
|
692
|
+
// the stale flag block learning on the next present.
|
|
693
|
+
_keyboardSheetGrown = NO;
|
|
694
|
+
|
|
695
|
+
[self emitDidDismissEvents];
|
|
147
696
|
}
|
|
148
697
|
|
|
698
|
+
- (void)viewWillLayoutSubviews {
|
|
699
|
+
[super viewWillLayoutSubviews];
|
|
700
|
+
|
|
701
|
+
// Skip during transitions and nav swipes — their trackers own position then.
|
|
702
|
+
if (_isTransitioning || _isInteractiveDismiss) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Mid-drag the frame tracks the finger — never settle or learn from it;
|
|
707
|
+
// pending changes keep their flags and settle on the first post-drag layout.
|
|
708
|
+
if ((_pendingContentSizeChange || _pendingDetentsChange) && !_isDragging) {
|
|
709
|
+
_pendingContentSizeChange = NO;
|
|
710
|
+
_pendingDetentsChange = NO;
|
|
711
|
+
[self settleAtDetentIndex:self.currentDetentIndex debug:@"layout"];
|
|
712
|
+
} else {
|
|
713
|
+
// A child on top moves this sheet two ways: an animated collapse/restore
|
|
714
|
+
// step (presenting/dismissing) — emit the target non-realtime so JS
|
|
715
|
+
// animates to it — or direct per-frame re-layouts while the child drags,
|
|
716
|
+
// which stay realtime. Mid-drag no child is on top, so drags emit realtime.
|
|
717
|
+
BOOL realtime = self.presentedViewController == nil || !self.isPresentedViewAnimating;
|
|
718
|
+
|
|
719
|
+
// A layout pass at rest can nudge the frame by a sub-pixel amount
|
|
720
|
+
// (pixel-grid snapping after present), leaving the interpolated index
|
|
721
|
+
// slightly off the whole number. Re-learn when the frame is effectively
|
|
722
|
+
// at the current detent so the drift is absorbed instead of emitted.
|
|
723
|
+
// The tight threshold keeps real movement (e.g. scroll-driven expansion
|
|
724
|
+
// or a drag hovering near the detent) from being absorbed.
|
|
725
|
+
if (!_isDragging && self.presentedViewController == nil && !self.isPresentedViewAnimating) {
|
|
726
|
+
NSInteger index = self.currentDetentIndex;
|
|
727
|
+
CGFloat expectedHeight = [_detentCalculator resolvedHeightForIndex:index];
|
|
728
|
+
CGFloat actualHeight = self.screenHeight - self.currentPosition;
|
|
729
|
+
if (expectedHeight > 0 && fabs(actualHeight - expectedHeight) < 2) {
|
|
730
|
+
[self learnOffsetForDetentIndex:index];
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:realtime debug:@"layout"];
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Rotation or an iPad window resize changes the height fractional detents
|
|
739
|
+
// resolve against — re-resolve alongside the transition so the sheet lands on
|
|
740
|
+
// the new height instead of waiting for a content-driven rebuild.
|
|
149
741
|
- (void)viewWillTransitionToSize:(CGSize)size
|
|
150
742
|
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
|
151
743
|
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
|
152
744
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
159
|
-
// After rotation completes
|
|
160
|
-
[self setupSheetDetents];
|
|
161
|
-
|
|
162
|
-
// Notify delegate of size changes
|
|
163
|
-
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
|
|
164
|
-
[self.delegate viewControllerDidChangeSize:self.view.frame.size];
|
|
745
|
+
if (@available(iOS 16.0, *)) {
|
|
746
|
+
[coordinator
|
|
747
|
+
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
748
|
+
[self.sheet invalidateDetents];
|
|
165
749
|
}
|
|
166
|
-
|
|
750
|
+
completion:nil];
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Fires when UIKit resolves the sheet's float/anchored placement (present,
|
|
755
|
+
// detent resize crossing the float threshold, rotation). The footer's
|
|
756
|
+
// absorbed inset follows the observed safe area — repush it right away so
|
|
757
|
+
// the footer pads/unpads in the same layout pass.
|
|
758
|
+
- (void)viewSafeAreaInsetsDidChange {
|
|
759
|
+
[super viewSafeAreaInsetsDidChange];
|
|
760
|
+
|
|
761
|
+
// Mid-dismiss the frame moves off-screen and reports transient insets —
|
|
762
|
+
// don't let them pollute the observed value (matches viewDidLayoutSubviews).
|
|
763
|
+
if (!self.isBeingDismissed) {
|
|
764
|
+
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
765
|
+
}
|
|
167
766
|
}
|
|
168
767
|
|
|
169
768
|
- (void)viewDidLayoutSubviews {
|
|
170
769
|
[super viewDidLayoutSubviews];
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
770
|
+
|
|
771
|
+
// viewSafeAreaInsetsDidChange doesn't fire on re-present when the observed
|
|
772
|
+
// inset matches the previous presentation's — refresh every layout pass
|
|
773
|
+
// instead (deduped downstream, so unchanged values are free).
|
|
774
|
+
if (!self.isBeingDismissed) {
|
|
775
|
+
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Skip size reports while backgrounded behind a stacked child — the push-back
|
|
779
|
+
// scaling changes the frame and would needlessly resize content.
|
|
780
|
+
// _lastReportedSize stays stale so the restore pass re-reports any real change.
|
|
781
|
+
if (!self.isStackedBehindChild) {
|
|
782
|
+
// Report any size change (detent resize, keyboard, rotation) so Yoga
|
|
783
|
+
// relayouts the container synchronously with the sheet.
|
|
784
|
+
CGSize size = self.view.frame.size;
|
|
785
|
+
if (!CGSizeEqualToSize(_lastReportedSize, size)) {
|
|
786
|
+
_lastReportedSize = size;
|
|
787
|
+
[self.delegate viewControllerDidChangeSize:size];
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (_pendingDetentIndex >= 0) {
|
|
792
|
+
NSInteger pendingIndex = _pendingDetentIndex;
|
|
793
|
+
_pendingDetentIndex = -1;
|
|
794
|
+
|
|
795
|
+
// The presentedView frame isn't final until UIKit finishes the resize
|
|
796
|
+
// animation — no completion hook exists for it, hence the delay.
|
|
797
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
798
|
+
CGFloat detent = [self detentValueForIndex:pendingIndex];
|
|
799
|
+
[self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
|
|
800
|
+
[self->_grabberView updateAccessibilityValueWithIndex:pendingIndex detentCount:self->_detents.count];
|
|
801
|
+
[self settleAtDetentIndex:pendingIndex debug:@"pending detent change"];
|
|
185
802
|
});
|
|
186
803
|
}
|
|
187
804
|
}
|
|
188
805
|
|
|
189
|
-
#pragma mark - Gesture Handling
|
|
806
|
+
#pragma mark - Position & Gesture Handling
|
|
190
807
|
|
|
191
|
-
/**
|
|
192
|
-
* Finds the TrueSheetContentView in the hierarchy.
|
|
193
|
-
*
|
|
194
|
-
* @param view The presentedView to start searching from
|
|
195
|
-
* @return The TrueSheetContentView found, or nil
|
|
196
|
-
*/
|
|
197
808
|
- (TrueSheetContentView *)findContentView:(UIView *)view {
|
|
198
|
-
// Check if this view itself is TrueSheetContentView
|
|
199
809
|
if ([view isKindOfClass:[TrueSheetContentView class]]) {
|
|
200
810
|
return (TrueSheetContentView *)view;
|
|
201
811
|
}
|
|
202
812
|
|
|
203
|
-
// Recursively search all subviews
|
|
204
813
|
for (UIView *subview in view.subviews) {
|
|
205
814
|
TrueSheetContentView *found = [self findContentView:subview];
|
|
206
815
|
if (found) {
|
|
@@ -216,44 +825,67 @@
|
|
|
216
825
|
if (!presentedView)
|
|
217
826
|
return;
|
|
218
827
|
|
|
219
|
-
|
|
828
|
+
if (!self.draggable) {
|
|
829
|
+
[GestureUtil setPanGesturesEnabled:NO forView:presentedView];
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
220
833
|
[GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
|
|
221
834
|
|
|
222
|
-
// Find and attach to the first ScrollView's pan gesture in the view hierarchy
|
|
223
|
-
// This handles cases where the sheet content includes a ScrollView
|
|
224
835
|
TrueSheetContentView *contentView = [self findContentView:presentedView];
|
|
225
836
|
if (contentView) {
|
|
226
837
|
RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView];
|
|
227
|
-
if (scrollViewComponent) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
[GestureUtil attachPanGestureHandler:scrollView target:self selector:@selector(handlePanGesture:)];
|
|
232
|
-
}
|
|
838
|
+
if (scrollViewComponent && scrollViewComponent.scrollView) {
|
|
839
|
+
[GestureUtil attachPanGestureHandler:scrollViewComponent.scrollView
|
|
840
|
+
target:self
|
|
841
|
+
selector:@selector(handlePanGesture:)];
|
|
233
842
|
}
|
|
234
843
|
}
|
|
235
844
|
}
|
|
236
845
|
|
|
846
|
+
- (void)setupDraggable {
|
|
847
|
+
UIView *presentedView = self.presentedView;
|
|
848
|
+
if (!presentedView)
|
|
849
|
+
return;
|
|
850
|
+
|
|
851
|
+
[GestureUtil setPanGesturesEnabled:self.draggable forView:presentedView];
|
|
852
|
+
}
|
|
853
|
+
|
|
237
854
|
- (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
|
|
238
|
-
NSInteger index =
|
|
855
|
+
NSInteger index = self.currentDetentIndex;
|
|
856
|
+
CGFloat detent = [self detentValueForIndex:index];
|
|
239
857
|
|
|
240
|
-
|
|
241
|
-
[self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition];
|
|
242
|
-
}
|
|
858
|
+
[self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition detent:detent];
|
|
243
859
|
|
|
244
860
|
switch (gesture.state) {
|
|
245
861
|
case UIGestureRecognizerStateBegan:
|
|
246
862
|
_isDragging = YES;
|
|
247
863
|
break;
|
|
248
864
|
case UIGestureRecognizerStateChanged:
|
|
249
|
-
|
|
250
|
-
|
|
865
|
+
// Layout owns the emit while the sheet resizes with the finger (a
|
|
866
|
+
// layout pass is already queued), and the transition tick owns it
|
|
867
|
+
// during an interactive dismiss. Translation-only movement (e.g.
|
|
868
|
+
// dragging down to the first detent) triggers neither — emit here as
|
|
869
|
+
// the backup.
|
|
870
|
+
if (!_isTransitioning && !self.presentedView.layer.needsLayout && !self.viewIfLoaded.layer.needsLayout) {
|
|
871
|
+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
|
|
872
|
+
}
|
|
251
873
|
break;
|
|
252
874
|
case UIGestureRecognizerStateEnded:
|
|
253
|
-
case UIGestureRecognizerStateCancelled:
|
|
875
|
+
case UIGestureRecognizerStateCancelled: {
|
|
876
|
+
if (!_isTransitioning) {
|
|
877
|
+
// Dispatch so UIKit picks the target detent first; the settle emit is
|
|
878
|
+
// non-realtime and JS animates alongside UIKit's snap spring.
|
|
879
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
880
|
+
NSInteger endIndex = self.currentDetentIndex;
|
|
881
|
+
[self->_grabberView updateAccessibilityValueWithIndex:endIndex detentCount:self->_detents.count];
|
|
882
|
+
[self settleAtDetentIndex:endIndex debug:@"drag end"];
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
254
886
|
_isDragging = NO;
|
|
255
887
|
break;
|
|
256
|
-
|
|
888
|
+
}
|
|
257
889
|
default:
|
|
258
890
|
break;
|
|
259
891
|
}
|
|
@@ -262,165 +894,336 @@
|
|
|
262
894
|
#pragma mark - Position Tracking
|
|
263
895
|
|
|
264
896
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
897
|
+
* Tracks the sheet's live position through a present/dismiss transition. The
|
|
898
|
+
* link's lifetime is scoped to the transition coordinator: started alongside
|
|
899
|
+
* the transition, stopped in its completion — which fires for finished and
|
|
900
|
+
* cancelled (interactive) transitions alike.
|
|
267
901
|
*/
|
|
268
|
-
- (void)
|
|
269
|
-
|
|
270
|
-
|
|
902
|
+
- (void)setupTransitionTracker {
|
|
903
|
+
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
|
|
904
|
+
|
|
905
|
+
// A cancelled dismiss re-enters viewWillAppear with the same coordinator —
|
|
906
|
+
// the tracker from viewWillDisappear still owns it.
|
|
907
|
+
if (!coordinator || _isTransitioning) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
271
910
|
|
|
272
|
-
|
|
273
|
-
NSInteger index = [self currentDetentIndex];
|
|
911
|
+
_isTransitioning = YES;
|
|
274
912
|
|
|
275
|
-
|
|
276
|
-
|
|
913
|
+
// Learn the resolver-vs-actual offset before emitting transition positions so
|
|
914
|
+
// the interpolated index lands exactly on the target detent. The presented
|
|
915
|
+
// frame is already final here (UIKit animates the layer, not the frame).
|
|
916
|
+
if (!self.isBeingDismissed) {
|
|
917
|
+
[self learnOffsetForDetentIndex:self.currentDetentIndex];
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
__weak __typeof(self) weakSelf = self;
|
|
921
|
+
[coordinator
|
|
922
|
+
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
923
|
+
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
924
|
+
if (!strongSelf)
|
|
925
|
+
return;
|
|
926
|
+
|
|
927
|
+
[strongSelf->_transitionLink invalidate];
|
|
928
|
+
strongSelf->_transitionLink = [CADisplayLink displayLinkWithTarget:strongSelf
|
|
929
|
+
selector:@selector(handleTransitionTick)];
|
|
930
|
+
// Track at the display's native refresh rate — the default caps at 60Hz
|
|
931
|
+
// on ProMotion, dropping every other frame of a 120Hz animation.
|
|
932
|
+
strongSelf->_transitionLink.preferredFrameRateRange = CAFrameRateRangeMake(60, 120, 120);
|
|
933
|
+
[strongSelf->_transitionLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
934
|
+
}
|
|
935
|
+
completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
936
|
+
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
937
|
+
if (!strongSelf)
|
|
938
|
+
return;
|
|
939
|
+
|
|
940
|
+
[strongSelf->_transitionLink invalidate];
|
|
941
|
+
strongSelf->_transitionLink = nil;
|
|
942
|
+
strongSelf->_isTransitioning = NO;
|
|
943
|
+
|
|
944
|
+
// Settle after a present, cancelled dismiss, or detent-snap transition.
|
|
945
|
+
// Delayed because the presentedView frame isn't final until UIKit
|
|
946
|
+
// completes its layout pass after the transition animation — learning
|
|
947
|
+
// here absorbs any sub-pixel drift since the earlier learns.
|
|
948
|
+
if (strongSelf->_isPresented && !strongSelf.isBeingDismissed) {
|
|
949
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
950
|
+
[strongSelf settleAtDetentIndex:strongSelf.currentDetentIndex debug:@"transition end"];
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
}];
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
- (void)handleTransitionTick {
|
|
957
|
+
// Also covers the interactive dismiss phase (finger down) — layout passes
|
|
958
|
+
// are skipped while transitioning, so the tick owns position there too.
|
|
959
|
+
CGFloat position = self.livePosition;
|
|
960
|
+
|
|
961
|
+
if (self.isBeingDismissed) {
|
|
962
|
+
// Emit only once the dismiss is committed: on release UIKit moves the
|
|
963
|
+
// model frame off-screen, while a cancelled drag rewinds it back to the
|
|
964
|
+
// detent (isBeingDismissed stays YES during the rewind).
|
|
965
|
+
if (self.currentPosition >= self.screenHeight) {
|
|
966
|
+
[self emitWillDismissEvents];
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Hide blur at the end of dismiss to prevent UIVisualEffectView
|
|
970
|
+
// from causing a flicker/flash at the bottom edge of the sheet.
|
|
971
|
+
if (self.screenHeight - position < 1) {
|
|
972
|
+
_blurView.alpha = 0;
|
|
277
973
|
}
|
|
278
974
|
}
|
|
975
|
+
|
|
976
|
+
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
|
|
279
977
|
}
|
|
280
978
|
|
|
281
979
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* alongside the actual presented view. By observing the presentation layer of this
|
|
286
|
-
* fake view's frame, we can track smooth position changes during the native transition
|
|
287
|
-
* animation without manually animating in JavaScript.
|
|
288
|
-
*
|
|
289
|
-
* The display link fires at screen refresh rate, allowing us to emit position updates
|
|
290
|
-
* that match the native animation curve, providing smooth synchronized updates to JS.
|
|
980
|
+
* The sheet is at rest at a detent: learn the resolver-vs-actual offset and
|
|
981
|
+
* emit the settled position. Only valid when the presentedView frame is final
|
|
982
|
+
* — never mid-drag or mid-animation, where learning would store a bogus offset.
|
|
291
983
|
*/
|
|
292
|
-
- (void)
|
|
293
|
-
|
|
294
|
-
|
|
984
|
+
- (void)settleAtDetentIndex:(NSInteger)index debug:(NSString *)debug {
|
|
985
|
+
// Don't learn while a child sits on top — the deck transform skews the
|
|
986
|
+
// measured frame; the next unobstructed settle learns.
|
|
987
|
+
if (self.presentedViewController == nil) {
|
|
988
|
+
[self learnOffsetForDetentIndex:index];
|
|
989
|
+
}
|
|
295
990
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
UIView *presentedView = self.presentedView;
|
|
991
|
+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:debug];
|
|
992
|
+
}
|
|
299
993
|
|
|
300
|
-
|
|
301
|
-
return;
|
|
994
|
+
#pragma mark - Interactive Navigation Dismiss
|
|
302
995
|
|
|
303
|
-
|
|
996
|
+
- (void)beginInteractiveDismiss {
|
|
997
|
+
if (_isInteractiveDismiss) {
|
|
998
|
+
// A settle animation from a prior gesture is still running. Stop it and reset so
|
|
999
|
+
// this gesture tracks from a clean state; its stale completion is ignored via the
|
|
1000
|
+
// generation check in animateInteractiveContainerToTransform.
|
|
1001
|
+
[_interactiveContainerView.layer removeAllAnimations];
|
|
1002
|
+
[self endInteractiveDismissState];
|
|
1003
|
+
}
|
|
304
1004
|
|
|
305
|
-
|
|
306
|
-
|
|
1005
|
+
_interactiveGeneration += 1;
|
|
1006
|
+
_isInteractiveDismiss = YES;
|
|
1007
|
+
_interactiveStartPosition = self.currentPosition;
|
|
1008
|
+
_interactiveContainerView = self.sheet.containerView;
|
|
307
1009
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
frame.origin.y = presentedView.frame.origin.y;
|
|
315
|
-
}
|
|
1010
|
+
// Emit position from the container's presentation layer for the whole gesture, so
|
|
1011
|
+
// both the direct-set drag and the settle animation report a live, smooth position.
|
|
1012
|
+
_interactivePositionLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(emitInteractivePosition)];
|
|
1013
|
+
_interactivePositionLink.preferredFrameRateRange = CAFrameRateRangeMake(60, 120, 120);
|
|
1014
|
+
[_interactivePositionLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
1015
|
+
}
|
|
316
1016
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
[[context containerView] addSubview:self->_fakeTransitionView];
|
|
323
|
-
|
|
324
|
-
// Animate fake view to the presented view's target position
|
|
325
|
-
// UIKit will animate this with the same timing curve as the sheet
|
|
326
|
-
CGRect finalFrame = presentedView.frame;
|
|
327
|
-
finalFrame.origin.y = presentedView.frame.origin.y;
|
|
328
|
-
self->_fakeTransitionView.frame = finalFrame;
|
|
329
|
-
|
|
330
|
-
// Set our last transition position so we can check if
|
|
331
|
-
// fake view's presentation layer has changed
|
|
332
|
-
// This value will not change if the sheet is being repositioned
|
|
333
|
-
// during a transition after a drag
|
|
334
|
-
self->_lastTransitionPosition = finalFrame.origin.y;
|
|
335
|
-
|
|
336
|
-
// Start display link to track position changes at screen refresh rate
|
|
337
|
-
// This fires at 60-120Hz and reads from the presentation layer
|
|
338
|
-
self->_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(trackTransitionPosition:)];
|
|
339
|
-
[self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
|
340
|
-
};
|
|
1017
|
+
- (void)emitInteractivePosition {
|
|
1018
|
+
CALayer *presentation = _interactiveContainerView.layer.presentationLayer;
|
|
1019
|
+
CGFloat offset = presentation ? presentation.affineTransform.ty : 0;
|
|
1020
|
+
[self emitChangePositionDelegateWithPosition:_interactiveStartPosition + offset realtime:YES debug:@"nav swipe"];
|
|
1021
|
+
}
|
|
341
1022
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
1023
|
+
// Translate the presentation container, not the sheet's presentedView (whose transform
|
|
1024
|
+
// UISheetPresentationController owns for the floating-sheet inset). Only runs for undimmed
|
|
1025
|
+
// sheets — a dimmed sheet's dimming view intercepts the edge-swipe so the pop never starts.
|
|
1026
|
+
- (void)updateInteractiveDismiss:(CGFloat)progress {
|
|
1027
|
+
if (!_isInteractiveDismiss) {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
CGFloat clamped = fmin(1, fmax(0, progress));
|
|
1031
|
+
CGFloat dy = clamped * (self.screenHeight - _interactiveStartPosition);
|
|
1032
|
+
_interactiveContainerView.transform = CGAffineTransformMakeTranslation(0, dy);
|
|
1033
|
+
}
|
|
348
1034
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}];
|
|
1035
|
+
- (void)cancelInteractiveDismissWithDuration:(NSTimeInterval)duration {
|
|
1036
|
+
if (!_isInteractiveDismiss) {
|
|
1037
|
+
return;
|
|
353
1038
|
}
|
|
1039
|
+
[self animateInteractiveContainerToTransform:CGAffineTransformIdentity
|
|
1040
|
+
duration:duration
|
|
1041
|
+
allowUserInteraction:YES
|
|
1042
|
+
completion:nil];
|
|
354
1043
|
}
|
|
355
1044
|
|
|
356
|
-
- (void)
|
|
357
|
-
|
|
1045
|
+
- (void)finishInteractiveDismissWithDuration:(NSTimeInterval)duration completion:(void (^)(void))completion {
|
|
1046
|
+
if (!_isInteractiveDismiss) {
|
|
1047
|
+
if (completion) {
|
|
1048
|
+
completion();
|
|
1049
|
+
}
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
// Leave the sheet translated off-screen; the caller tears it down non-animated
|
|
1053
|
+
// from here so there is no second slide.
|
|
1054
|
+
CGFloat dy = self.screenHeight - _interactiveStartPosition;
|
|
1055
|
+
[self animateInteractiveContainerToTransform:CGAffineTransformMakeTranslation(0, dy)
|
|
1056
|
+
duration:duration
|
|
1057
|
+
allowUserInteraction:NO
|
|
1058
|
+
completion:completion];
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
- (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
|
|
1062
|
+
duration:(NSTimeInterval)duration
|
|
1063
|
+
allowUserInteraction:(BOOL)allowUserInteraction
|
|
1064
|
+
completion:(void (^)(void))completion {
|
|
1065
|
+
UIView *container = _interactiveContainerView;
|
|
1066
|
+
NSUInteger generation = _interactiveGeneration;
|
|
1067
|
+
|
|
1068
|
+
UIViewAnimationOptions options = UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState;
|
|
1069
|
+
if (allowUserInteraction) {
|
|
1070
|
+
options |= UIViewAnimationOptionAllowUserInteraction;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
[UIView animateWithDuration:fmax(duration, 0.2)
|
|
1074
|
+
delay:0
|
|
1075
|
+
options:options
|
|
1076
|
+
animations:^{
|
|
1077
|
+
container.transform = transform;
|
|
1078
|
+
}
|
|
1079
|
+
completion:^(BOOL finished) {
|
|
1080
|
+
// A newer gesture superseded this settle and owns teardown now.
|
|
1081
|
+
if (generation != self->_interactiveGeneration) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
[self endInteractiveDismissState];
|
|
1085
|
+
if (completion) {
|
|
1086
|
+
completion();
|
|
1087
|
+
}
|
|
1088
|
+
}];
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
- (void)endInteractiveDismissState {
|
|
1092
|
+
_isInteractiveDismiss = NO;
|
|
1093
|
+
_interactiveContainerView = nil;
|
|
1094
|
+
_interactiveStartPosition = 0;
|
|
1095
|
+
[_interactivePositionLink invalidate];
|
|
1096
|
+
_interactivePositionLink = nil;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
- (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)realtime debug:(NSString *)debug {
|
|
1100
|
+
UIViewController *presented = self.presentedViewController;
|
|
1101
|
+
if (presented) {
|
|
1102
|
+
UIModalPresentationStyle style = presented.modalPresentationStyle;
|
|
1103
|
+
if (style == UIModalPresentationFullScreen || style == UIModalPresentationOverFullScreen ||
|
|
1104
|
+
style == UIModalPresentationCurrentContext || style == UIModalPresentationOverCurrentContext) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
TrueSheetPositionState state = {
|
|
1110
|
+
.position = position,
|
|
1111
|
+
.detent = [self interpolatedDetentForPosition:position],
|
|
1112
|
+
.index = [self interpolatedIndexForPosition:position],
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
// Settle (non-realtime) emits are authoritative and bypass the dedupe — a
|
|
1116
|
+
// learn right before them can correct the interpolated index by less than
|
|
1117
|
+
// the tolerance, and the corrected value must still reach JS.
|
|
1118
|
+
if (!realtime || !TrueSheetPositionStateEquals(_lastEmittedPositionState, state)) {
|
|
1119
|
+
_lastEmittedPositionState = state;
|
|
1120
|
+
|
|
1121
|
+
[self.delegate viewControllerDidChangePosition:state.index
|
|
1122
|
+
position:state.position
|
|
1123
|
+
detent:state.detent
|
|
1124
|
+
realtime:realtime];
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
358
1127
|
|
|
359
|
-
|
|
1128
|
+
- (void)learnOffsetForDetentIndex:(NSInteger)index {
|
|
1129
|
+
if (_keyboardSheetGrown) {
|
|
360
1130
|
return;
|
|
361
1131
|
}
|
|
1132
|
+
[_detentCalculator learnOffsetForDetentIndex:index];
|
|
1133
|
+
}
|
|
362
1134
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
CALayer *presentationLayer = _fakeTransitionView.layer.presentationLayer;
|
|
1135
|
+
- (void)setKeyboardSheetGrown:(BOOL)keyboardSheetGrown {
|
|
1136
|
+
BOOL wasGrown = _keyboardSheetGrown;
|
|
1137
|
+
_keyboardSheetGrown = keyboardSheetGrown;
|
|
367
1138
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1139
|
+
// Settles are skipped or measure a mid-animation frame while the keyboard
|
|
1140
|
+
// has the sheet grown (e.g. a drag-end during the shrink-back) — re-settle
|
|
1141
|
+
// at the resting detent once the keyboard is fully away.
|
|
1142
|
+
if (wasGrown && !keyboardSheetGrown && self.isPresented && !_isDragging) {
|
|
1143
|
+
[self settleAtDetentIndex:self.currentDetentIndex debug:@"keyboard settled"];
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
371
1146
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// Let's just flag it as transitioning to let JS manually animate
|
|
376
|
-
transitioning = YES;
|
|
1147
|
+
- (BOOL)findSegmentForPosition:(CGFloat)position outIndex:(NSInteger *)outIndex outProgress:(CGFloat *)outProgress {
|
|
1148
|
+
return [_detentCalculator findSegmentForPosition:position outIndex:outIndex outProgress:outProgress];
|
|
1149
|
+
}
|
|
377
1150
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
1151
|
+
- (CGFloat)interpolatedIndexForPosition:(CGFloat)position {
|
|
1152
|
+
return [_detentCalculator interpolatedIndexForPosition:position];
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
- (CGFloat)interpolatedDetentForPosition:(CGFloat)position {
|
|
1156
|
+
return [_detentCalculator interpolatedDetentForPosition:position];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
- (CGFloat)detentValueForIndex:(NSInteger)index {
|
|
1160
|
+
return [_detentCalculator detentValueForIndex:index];
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
#pragma mark - Sheet Configuration
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Applies a content/header/footer/peek size change to the already-built
|
|
1167
|
+
* detents. Auto and peek detents resolve lazily from snapshots, so this only
|
|
1168
|
+
* refreshes the snapshots and invalidates — never reassigns `sheet.detents`,
|
|
1169
|
+
* which would force UIKit to re-layout the whole presentation stack (visibly
|
|
1170
|
+
* perturbing a sheet behind this one).
|
|
1171
|
+
*/
|
|
1172
|
+
- (void)setupSheetDetentsForSizeChange {
|
|
1173
|
+
if (@available(iOS 16.0, *)) {
|
|
1174
|
+
CGFloat autoHeight = [_detentCalculator autoHeight];
|
|
1175
|
+
CGFloat peekHeight = [_detentCalculator peekHeight];
|
|
1176
|
+
|
|
1177
|
+
if (fabs(autoHeight - _autoDetentHeight) < 0.5 && fabs(peekHeight - _peekDetentHeight) < 0.5) {
|
|
1178
|
+
return;
|
|
384
1179
|
}
|
|
385
1180
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
1181
|
+
_autoDetentHeight = autoHeight;
|
|
1182
|
+
_peekDetentHeight = peekHeight;
|
|
1183
|
+
|
|
1184
|
+
[self.sheet animateChanges:^{
|
|
1185
|
+
self->_pendingContentSizeChange = YES;
|
|
1186
|
+
[self.sheet invalidateDetents];
|
|
1187
|
+
}];
|
|
389
1188
|
}
|
|
1189
|
+
// iOS 15: detents are fixed medium/large — size changes can't affect them.
|
|
390
1190
|
}
|
|
391
1191
|
|
|
392
|
-
|
|
1192
|
+
- (void)setupSheetDetentsForDetentsChange {
|
|
1193
|
+
_pendingDetentsChange = YES;
|
|
1194
|
+
[self setupSheetDetents];
|
|
1195
|
+
}
|
|
393
1196
|
|
|
394
1197
|
- (void)setupSheetDetents {
|
|
395
|
-
UISheetPresentationController *sheet = self.
|
|
396
|
-
if (!sheet)
|
|
1198
|
+
UISheetPresentationController *sheet = self.sheet;
|
|
1199
|
+
if (!sheet) {
|
|
1200
|
+
RCTLogError(@"TrueSheet: sheetPresentationController is nil in setupSheetDetents");
|
|
397
1201
|
return;
|
|
1202
|
+
}
|
|
398
1203
|
|
|
399
1204
|
NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
|
|
1205
|
+
[_detentCalculator clearResolvedHeights];
|
|
400
1206
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
CGFloat totalHeight = [self.contentHeight floatValue] - _bottomInset;
|
|
1207
|
+
_autoDetentHeight = [_detentCalculator autoHeight];
|
|
1208
|
+
_peekDetentHeight = [_detentCalculator peekHeight];
|
|
404
1209
|
|
|
405
1210
|
for (NSInteger index = 0; index < self.detents.count; index++) {
|
|
406
1211
|
id detent = self.detents[index];
|
|
407
|
-
UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent
|
|
408
|
-
withHeight:totalHeight
|
|
409
|
-
atIndex:index];
|
|
1212
|
+
UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent atIndex:index];
|
|
410
1213
|
[detents addObject:sheetDetent];
|
|
411
1214
|
}
|
|
412
1215
|
|
|
1216
|
+
[_detentCalculator setDetentCount:self.detents.count];
|
|
413
1217
|
sheet.detents = detents;
|
|
414
1218
|
|
|
415
|
-
|
|
416
|
-
if (self.dimmed && [self.dimmedIndex integerValue] == 0) {
|
|
1219
|
+
if (self.dimmed && [self.dimmedDetentIndex integerValue] == 0) {
|
|
417
1220
|
sheet.largestUndimmedDetentIdentifier = nil;
|
|
418
1221
|
} else {
|
|
419
1222
|
sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
|
|
420
1223
|
|
|
421
1224
|
if (@available(iOS 16.0, *)) {
|
|
422
|
-
if (self.dimmed && self.
|
|
423
|
-
NSInteger dimmedIdx = [self.
|
|
1225
|
+
if (self.dimmed && self.dimmedDetentIndex) {
|
|
1226
|
+
NSInteger dimmedIdx = [self.dimmedDetentIndex integerValue];
|
|
424
1227
|
if (dimmedIdx > 0 && dimmedIdx - 1 < sheet.detents.count) {
|
|
425
1228
|
sheet.largestUndimmedDetentIdentifier = sheet.detents[dimmedIdx - 1].identifier;
|
|
426
1229
|
} else if (sheet.detents.lastObject) {
|
|
@@ -433,62 +1236,109 @@
|
|
|
433
1236
|
}
|
|
434
1237
|
}
|
|
435
1238
|
|
|
436
|
-
- (UISheetPresentationControllerDetent *)detentForValue:(id)detent
|
|
1239
|
+
- (UISheetPresentationControllerDetent *)detentForValue:(id)detent atIndex:(NSInteger)index {
|
|
437
1240
|
if (![detent isKindOfClass:[NSNumber class]]) {
|
|
438
1241
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
439
1242
|
}
|
|
440
1243
|
|
|
441
|
-
CGFloat value = [detent
|
|
1244
|
+
CGFloat value = [detent doubleValue];
|
|
442
1245
|
|
|
443
|
-
//
|
|
1246
|
+
// Auto/peek resolve from height snapshots — refreshed only in
|
|
1247
|
+
// setupSheetDetents and setupSheetDetentsForSizeChange — so size changes are
|
|
1248
|
+
// picked up on invalidation, while UIKit's spontaneous re-resolutions (e.g.
|
|
1249
|
+
// while a child sheet dismisses) read a stable value.
|
|
444
1250
|
if (value == -1) {
|
|
445
1251
|
if (@available(iOS 16.0, *)) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
1252
|
+
return [self customDetentWithIdentifier:@"custom-auto"
|
|
1253
|
+
atIndex:index
|
|
1254
|
+
heightBlock:^CGFloat {
|
|
1255
|
+
return self->_autoDetentHeight - [self autoDetentBottomInset];
|
|
1256
|
+
}];
|
|
1257
|
+
} else {
|
|
1258
|
+
return [UISheetPresentationControllerDetent mediumDetent];
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (value == -2) {
|
|
1263
|
+
if (@available(iOS 16.0, *)) {
|
|
1264
|
+
return [self customDetentWithIdentifier:@"custom-peek"
|
|
1265
|
+
atIndex:index
|
|
1266
|
+
heightBlock:^CGFloat {
|
|
1267
|
+
return self->_peekDetentHeight - [self peekDetentBottomInset];
|
|
1268
|
+
}];
|
|
456
1269
|
} else {
|
|
457
1270
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
458
1271
|
}
|
|
459
1272
|
}
|
|
460
1273
|
|
|
461
|
-
// Handle fraction (0-1)
|
|
462
|
-
// Fraction should only be > 0 and <= 1
|
|
463
1274
|
if (value <= 0 || value > 1) {
|
|
464
1275
|
RCTLogError(@"TrueSheet: detent fraction (%f) must be between 0 and 1", value);
|
|
465
1276
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
466
1277
|
}
|
|
467
1278
|
|
|
468
|
-
NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
|
|
469
|
-
|
|
470
1279
|
if (@available(iOS 16.0, *)) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1280
|
+
NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
|
|
1281
|
+
// Resolve lazily — detents are built before presentation (no window yet), and
|
|
1282
|
+
// UIKit re-resolves on container size changes (rotation, windowed iPad resize).
|
|
1283
|
+
return [self customDetentWithIdentifier:detentId
|
|
1284
|
+
atIndex:index
|
|
1285
|
+
heightBlock:^CGFloat {
|
|
1286
|
+
return value * self.screenHeight;
|
|
1287
|
+
}];
|
|
1288
|
+
} else if (value >= 0.5) {
|
|
1289
|
+
return [UISheetPresentationControllerDetent largeDetent];
|
|
480
1290
|
} else {
|
|
481
1291
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
482
1292
|
}
|
|
483
1293
|
}
|
|
484
1294
|
|
|
1295
|
+
- (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
|
|
1296
|
+
atIndex:(NSInteger)index
|
|
1297
|
+
heightBlock:(CGFloat (^)(void))heightBlock
|
|
1298
|
+
API_AVAILABLE(ios(16.0)) {
|
|
1299
|
+
return [UISheetPresentationControllerDetent
|
|
1300
|
+
customDetentWithIdentifier:identifier
|
|
1301
|
+
resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
|
|
1302
|
+
CGFloat maxDetentValue = context.maximumDetentValue;
|
|
1303
|
+
self->_detentCalculator.maxDetentHeight = maxDetentValue;
|
|
1304
|
+
|
|
1305
|
+
CGFloat height = heightBlock();
|
|
1306
|
+
CGFloat bottomAdjustment = [self detentBottomAdjustmentForHeight:height];
|
|
1307
|
+
CGFloat maxValue = self.maxContentHeight
|
|
1308
|
+
? fmin(maxDetentValue, [self.maxContentHeight floatValue])
|
|
1309
|
+
: maxDetentValue;
|
|
1310
|
+
|
|
1311
|
+
CGFloat adjustedHeight = height - bottomAdjustment;
|
|
1312
|
+
|
|
1313
|
+
// Only the last detent may occupy the full maximum. Two detents
|
|
1314
|
+
// resolving to the same height (e.g. a capped auto and 1) make
|
|
1315
|
+
// UIKit skip the stack's deck animation when presenting a child
|
|
1316
|
+
// sheet and snap the sheet behind into place instead.
|
|
1317
|
+
NSInteger lastIndex = (NSInteger)self.detents.count - 1;
|
|
1318
|
+
if (adjustedHeight >= maxValue && index < lastIndex) {
|
|
1319
|
+
maxValue -= lastIndex - index;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
CGFloat resolved = fmin(adjustedHeight, maxValue);
|
|
1323
|
+
|
|
1324
|
+
NSMutableArray *heights = self->_detentCalculator.resolvedDetentHeights;
|
|
1325
|
+
if (heights && index >= 0 && index < (NSInteger)heights.count) {
|
|
1326
|
+
heights[index] = @(resolved);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return resolved;
|
|
1330
|
+
}];
|
|
1331
|
+
}
|
|
1332
|
+
|
|
485
1333
|
- (UISheetPresentationControllerDetentIdentifier)detentIdentifierForIndex:(NSInteger)index {
|
|
486
|
-
UISheetPresentationController *sheet = self.
|
|
487
|
-
if (!sheet)
|
|
1334
|
+
UISheetPresentationController *sheet = self.sheet;
|
|
1335
|
+
if (!sheet) {
|
|
1336
|
+
RCTLogError(@"TrueSheet: sheetPresentationController is nil in detentIdentifierForIndex");
|
|
488
1337
|
return UISheetPresentationControllerDetentIdentifierMedium;
|
|
1338
|
+
}
|
|
489
1339
|
|
|
490
1340
|
UISheetPresentationControllerDetentIdentifier identifier = UISheetPresentationControllerDetentIdentifierMedium;
|
|
491
|
-
if (index < sheet.detents.count) {
|
|
1341
|
+
if (index >= 0 && index < (NSInteger)sheet.detents.count) {
|
|
492
1342
|
UISheetPresentationControllerDetent *detent = sheet.detents[index];
|
|
493
1343
|
if (@available(iOS 16.0, *)) {
|
|
494
1344
|
identifier = detent.identifier;
|
|
@@ -503,17 +1353,15 @@
|
|
|
503
1353
|
}
|
|
504
1354
|
|
|
505
1355
|
- (void)applyActiveDetent {
|
|
506
|
-
|
|
507
|
-
|
|
1356
|
+
if (!self.sheet) {
|
|
1357
|
+
RCTLogError(@"TrueSheet: sheetPresentationController is nil in applyActiveDetent");
|
|
508
1358
|
return;
|
|
1359
|
+
}
|
|
509
1360
|
|
|
510
|
-
// Validate and clamp activeDetentIndex to detents bounds
|
|
511
1361
|
NSInteger detentCount = _detents.count;
|
|
512
|
-
if (detentCount == 0)
|
|
1362
|
+
if (detentCount == 0)
|
|
513
1363
|
return;
|
|
514
|
-
}
|
|
515
1364
|
|
|
516
|
-
// Clamp index to valid range
|
|
517
1365
|
NSInteger clampedIndex = _activeDetentIndex;
|
|
518
1366
|
if (clampedIndex < 0) {
|
|
519
1367
|
clampedIndex = 0;
|
|
@@ -521,14 +1369,13 @@
|
|
|
521
1369
|
clampedIndex = detentCount - 1;
|
|
522
1370
|
}
|
|
523
1371
|
|
|
524
|
-
// Update the stored index if it was clamped
|
|
525
1372
|
if (clampedIndex != _activeDetentIndex) {
|
|
526
1373
|
_activeDetentIndex = clampedIndex;
|
|
527
1374
|
}
|
|
528
1375
|
|
|
529
1376
|
UISheetPresentationControllerDetentIdentifier identifier = [self detentIdentifierForIndex:clampedIndex];
|
|
530
1377
|
if (identifier) {
|
|
531
|
-
sheet.selectedDetentIdentifier = identifier;
|
|
1378
|
+
self.sheet.selectedDetentIdentifier = identifier;
|
|
532
1379
|
}
|
|
533
1380
|
}
|
|
534
1381
|
|
|
@@ -537,113 +1384,251 @@
|
|
|
537
1384
|
[self applyActiveDetent];
|
|
538
1385
|
}
|
|
539
1386
|
|
|
540
|
-
- (NSInteger)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1387
|
+
- (void)resizeToDetentIndex:(NSInteger)index {
|
|
1388
|
+
if (index == _activeDetentIndex) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
544
1391
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1392
|
+
_pendingDetentIndex = index;
|
|
1393
|
+
_activeDetentIndex = index;
|
|
1394
|
+
[self applyActiveDetent];
|
|
1395
|
+
}
|
|
548
1396
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1397
|
+
- (void)setupBackground {
|
|
1398
|
+
auto effectiveBackgroundBlur = self.backgroundBlur;
|
|
1399
|
+
if (@available(iOS 26.0, *)) {
|
|
1400
|
+
// iOS 26+ has default liquid glass effect
|
|
1401
|
+
} else if (effectiveBackgroundBlur == TrueSheetViewBackgroundBlur::None && !self.backgroundColor) {
|
|
1402
|
+
effectiveBackgroundBlur = TrueSheetViewBackgroundBlur::SystemMaterial;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
BOOL hasBlur = effectiveBackgroundBlur != TrueSheetViewBackgroundBlur::None;
|
|
1406
|
+
|
|
1407
|
+
_blurView.backgroundBlur = hasBlur ? effectiveBackgroundBlur : TrueSheetViewBackgroundBlur::None;
|
|
1408
|
+
_blurView.blurIntensity = self.blurIntensity;
|
|
1409
|
+
_blurView.blurInteraction = self.blurInteraction;
|
|
1410
|
+
[_blurView applyBlurEffect];
|
|
1411
|
+
|
|
1412
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
|
|
1413
|
+
if (@available(iOS 26.1, *)) {
|
|
1414
|
+
if (!self.isDesignCompatibilityMode) {
|
|
1415
|
+
if (self.backgroundColor || hasBlur) {
|
|
1416
|
+
self.sheet.backgroundEffect = [UIColorEffect effectWithColor:[UIColor clearColor]];
|
|
1417
|
+
} else {
|
|
1418
|
+
self.sheet.backgroundEffect = nil;
|
|
563
1419
|
}
|
|
564
1420
|
}
|
|
565
1421
|
}
|
|
1422
|
+
#endif
|
|
566
1423
|
|
|
567
|
-
|
|
1424
|
+
self.view.backgroundColor = self.backgroundColor;
|
|
568
1425
|
}
|
|
569
1426
|
|
|
570
|
-
- (
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1427
|
+
- (void)setupGrabber {
|
|
1428
|
+
BOOL showGrabber = self.grabber && self.draggable;
|
|
1429
|
+
|
|
1430
|
+
if (self.grabberOptions) {
|
|
1431
|
+
self.sheet.prefersGrabberVisible = NO;
|
|
1432
|
+
|
|
1433
|
+
GrabberOptions *options = self.grabberOptions;
|
|
1434
|
+
_grabberView.accessibilityOptions = self.accessibilityOptions;
|
|
1435
|
+
_grabberView.grabberWidth = options.width;
|
|
1436
|
+
_grabberView.grabberHeight = options.height;
|
|
1437
|
+
_grabberView.topMargin = options.topMargin;
|
|
1438
|
+
_grabberView.cornerRadius = options.cornerRadius;
|
|
1439
|
+
_grabberView.color = options.color;
|
|
1440
|
+
_grabberView.adaptive = @(options.adaptive);
|
|
1441
|
+
[_grabberView applyConfiguration];
|
|
1442
|
+
_grabberView.hidden = !showGrabber;
|
|
1443
|
+
|
|
1444
|
+
__weak __typeof(self) weakSelf = self;
|
|
1445
|
+
_grabberView.onTap = ^{
|
|
1446
|
+
[weakSelf handleGrabberTap];
|
|
1447
|
+
};
|
|
1448
|
+
_grabberView.onIncrement = ^{
|
|
1449
|
+
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
1450
|
+
if (!strongSelf)
|
|
1451
|
+
return;
|
|
1452
|
+
NSInteger current = strongSelf.currentDetentIndex;
|
|
1453
|
+
NSInteger count = strongSelf->_detents.count;
|
|
1454
|
+
if (current >= 0 && current < count - 1) {
|
|
1455
|
+
[strongSelf.sheet animateChanges:^{
|
|
1456
|
+
[strongSelf resizeToDetentIndex:current + 1];
|
|
1457
|
+
}];
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
_grabberView.onDecrement = ^{
|
|
1461
|
+
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
1462
|
+
if (!strongSelf)
|
|
1463
|
+
return;
|
|
1464
|
+
NSInteger current = strongSelf.currentDetentIndex;
|
|
1465
|
+
if (current > 0) {
|
|
1466
|
+
[strongSelf.sheet animateChanges:^{
|
|
1467
|
+
[strongSelf resizeToDetentIndex:current - 1];
|
|
1468
|
+
}];
|
|
1469
|
+
} else if (strongSelf.dismissible) {
|
|
1470
|
+
[strongSelf.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
1471
|
+
} else {
|
|
1472
|
+
[strongSelf.delegate viewControllerDidAttemptDismiss];
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
574
1475
|
|
|
575
|
-
|
|
1476
|
+
[self.view bringSubviewToFront:_grabberView];
|
|
1477
|
+
} else {
|
|
1478
|
+
self.sheet.prefersGrabberVisible = showGrabber;
|
|
1479
|
+
_grabberView.hidden = YES;
|
|
1480
|
+
_grabberView.onTap = nil;
|
|
1481
|
+
_grabberView.onIncrement = nil;
|
|
1482
|
+
_grabberView.onDecrement = nil;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Only the topmost sheet may refresh, or a parent's prop update would steal the
|
|
1486
|
+
// window accessibility override from the child presented over it.
|
|
1487
|
+
if (_isPresented && self.isTopmostPresentedController) {
|
|
1488
|
+
[self setupAccessibilityContainer];
|
|
1489
|
+
}
|
|
576
1490
|
}
|
|
577
1491
|
|
|
578
|
-
- (
|
|
579
|
-
|
|
1492
|
+
- (void)handleGrabberTap {
|
|
1493
|
+
NSInteger detentCount = _detents.count;
|
|
1494
|
+
if (detentCount == 0)
|
|
1495
|
+
return;
|
|
1496
|
+
|
|
1497
|
+
NSInteger currentIndex = self.currentDetentIndex;
|
|
1498
|
+
if (currentIndex < 0)
|
|
1499
|
+
return;
|
|
1500
|
+
|
|
1501
|
+
NSInteger nextIndex = (currentIndex + 1) % detentCount;
|
|
1502
|
+
if (nextIndex == 0 && detentCount == 1) {
|
|
1503
|
+
if (self.dismissible) {
|
|
1504
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
1505
|
+
} else {
|
|
1506
|
+
[self.delegate viewControllerDidAttemptDismiss];
|
|
1507
|
+
}
|
|
1508
|
+
} else {
|
|
1509
|
+
[self.sheet animateChanges:^{
|
|
1510
|
+
[self resizeToDetentIndex:nextIndex];
|
|
1511
|
+
}];
|
|
1512
|
+
}
|
|
580
1513
|
}
|
|
581
1514
|
|
|
582
|
-
- (
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
1515
|
+
- (BOOL)isAnchored {
|
|
1516
|
+
return self.anchor == TrueSheetViewAnchor::Left || self.anchor == TrueSheetViewAnchor::Right;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
- (void)setupAnchorViewInView:(UIView *)parentView {
|
|
1520
|
+
if (!parentView)
|
|
1521
|
+
return;
|
|
1522
|
+
|
|
1523
|
+
[_anchorView removeFromSuperview];
|
|
1524
|
+
_anchorView = nil;
|
|
1525
|
+
|
|
1526
|
+
if (!self.isAnchored) {
|
|
1527
|
+
self.sheetPresentationController.sourceView = nil;
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
586
1530
|
|
|
587
|
-
|
|
1531
|
+
_anchorView = [[UIView alloc] init];
|
|
1532
|
+
_anchorView.userInteractionEnabled = NO;
|
|
1533
|
+
_anchorView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
1534
|
+
[parentView addSubview:_anchorView];
|
|
1535
|
+
|
|
1536
|
+
NSLayoutAnchor *horizontalAnchor =
|
|
1537
|
+
self.anchor == TrueSheetViewAnchor::Right ? parentView.trailingAnchor : parentView.leadingAnchor;
|
|
1538
|
+
|
|
1539
|
+
[NSLayoutConstraint activateConstraints:@[
|
|
1540
|
+
[_anchorView.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor],
|
|
1541
|
+
[horizontalAnchor constraintEqualToAnchor:_anchorView.leadingAnchor],
|
|
1542
|
+
]];
|
|
1543
|
+
|
|
1544
|
+
self.sheetPresentationController.sourceView = _anchorView;
|
|
588
1545
|
}
|
|
589
1546
|
|
|
590
|
-
- (void)
|
|
591
|
-
UISheetPresentationController *sheet = self.
|
|
592
|
-
if (!sheet)
|
|
1547
|
+
- (void)setupSheetSizing {
|
|
1548
|
+
UISheetPresentationController *sheet = self.sheet;
|
|
1549
|
+
if (!sheet)
|
|
593
1550
|
return;
|
|
1551
|
+
|
|
1552
|
+
// `presentation` is absolute on the form side: 'form' always renders a
|
|
1553
|
+
// centered form sheet and ignores `maxContentWidth`. For 'page' with a
|
|
1554
|
+
// custom `maxContentWidth`, prefersPageSizing has to flip to NO since
|
|
1555
|
+
// `widthFollowsPreferredContentSizeWhenEdgeAttached` only takes effect
|
|
1556
|
+
// when the sheet is edge-attached (Apple API constraint).
|
|
1557
|
+
BOOL formSheet = self.presentation == facebook::react::TrueSheetViewPresentation::Form;
|
|
1558
|
+
BOOL hasMaxWidth = self.maxContentWidth != nil && !formSheet;
|
|
1559
|
+
|
|
1560
|
+
if (@available(iOS 17.0, *)) {
|
|
1561
|
+
sheet.prefersPageSizing = !formSheet && !hasMaxWidth;
|
|
594
1562
|
}
|
|
595
1563
|
|
|
1564
|
+
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = hasMaxWidth;
|
|
1565
|
+
|
|
1566
|
+
if (hasMaxWidth) {
|
|
1567
|
+
CGFloat height = self.maxContentHeight ? [self.maxContentHeight floatValue] : self.screenHeight;
|
|
1568
|
+
self.preferredContentSize = CGSizeMake([self.maxContentWidth floatValue], height);
|
|
1569
|
+
} else {
|
|
1570
|
+
self.preferredContentSize = CGSizeZero;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
- (void)setupSheetProps {
|
|
1575
|
+
UISheetPresentationController *sheet = self.sheet;
|
|
1576
|
+
if (!sheet)
|
|
1577
|
+
return;
|
|
1578
|
+
|
|
596
1579
|
sheet.delegate = self;
|
|
597
1580
|
sheet.prefersEdgeAttachedInCompactHeight = YES;
|
|
598
|
-
sheet.
|
|
599
|
-
|
|
1581
|
+
sheet.prefersScrollingExpandsWhenScrolledToEdge = self.draggable && self.scrollingExpandsSheet;
|
|
1582
|
+
|
|
600
1583
|
if (self.cornerRadius) {
|
|
601
1584
|
sheet.preferredCornerRadius = [self.cornerRadius floatValue];
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Setup blur effect if blurTint is provided
|
|
605
|
-
if (self.blurTint && self.blurTint.length > 0) {
|
|
606
|
-
UIBlurEffectStyle style = UIBlurEffectStyleLight;
|
|
607
|
-
|
|
608
|
-
if ([self.blurTint isEqualToString:@"dark"]) {
|
|
609
|
-
style = UIBlurEffectStyleDark;
|
|
610
|
-
} else if ([self.blurTint isEqualToString:@"light"]) {
|
|
611
|
-
style = UIBlurEffectStyleLight;
|
|
612
|
-
} else if ([self.blurTint isEqualToString:@"extraLight"]) {
|
|
613
|
-
style = UIBlurEffectStyleExtraLight;
|
|
614
|
-
} else if ([self.blurTint isEqualToString:@"regular"]) {
|
|
615
|
-
style = UIBlurEffectStyleRegular;
|
|
616
|
-
} else if ([self.blurTint isEqualToString:@"prominent"]) {
|
|
617
|
-
style = UIBlurEffectStyleProminent;
|
|
618
|
-
} else if ([self.blurTint isEqualToString:@"systemThinMaterial"]) {
|
|
619
|
-
style = UIBlurEffectStyleSystemThinMaterial;
|
|
620
|
-
} else if ([self.blurTint isEqualToString:@"systemMaterial"]) {
|
|
621
|
-
style = UIBlurEffectStyleSystemMaterial;
|
|
622
|
-
} else if ([self.blurTint isEqualToString:@"systemThickMaterial"]) {
|
|
623
|
-
style = UIBlurEffectStyleSystemThickMaterial;
|
|
624
|
-
} else if ([self.blurTint isEqualToString:@"systemChromeMaterial"]) {
|
|
625
|
-
style = UIBlurEffectStyleSystemChromeMaterial;
|
|
626
|
-
} else if ([self.blurTint isEqualToString:@"systemUltraThinMaterial"]) {
|
|
627
|
-
style = UIBlurEffectStyleSystemUltraThinMaterial;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
_backgroundView.effect = [UIBlurEffect effectWithStyle:style];
|
|
631
|
-
_backgroundView.backgroundColor = nil;
|
|
632
1585
|
} else {
|
|
633
|
-
|
|
634
|
-
_backgroundView.effect = nil;
|
|
635
|
-
_backgroundView.backgroundColor = self.backgroundColor;
|
|
1586
|
+
sheet.preferredCornerRadius = UISheetPresentationControllerAutomaticDimension;
|
|
636
1587
|
}
|
|
1588
|
+
|
|
1589
|
+
[self setupBackground];
|
|
1590
|
+
[self setupGrabber];
|
|
637
1591
|
}
|
|
638
1592
|
|
|
639
1593
|
#pragma mark - UISheetPresentationControllerDelegate
|
|
640
1594
|
|
|
1595
|
+
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController {
|
|
1596
|
+
return self.dismissible;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// UIKit calls this after shouldDismiss returns NO — the sheet has already
|
|
1600
|
+
// bounced back, so JS can confirm and dismiss programmatically (Apple's
|
|
1601
|
+
// unsaved-changes pattern).
|
|
1602
|
+
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController {
|
|
1603
|
+
[self.delegate viewControllerDidAttemptDismiss];
|
|
1604
|
+
}
|
|
1605
|
+
|
|
641
1606
|
- (void)sheetPresentationControllerDidChangeSelectedDetentIdentifier:
|
|
642
1607
|
(UISheetPresentationController *)sheetPresentationController {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1608
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1609
|
+
NSInteger index = self.currentDetentIndex;
|
|
1610
|
+
if (index >= 0) {
|
|
1611
|
+
CGFloat detent = [self detentValueForIndex:index];
|
|
1612
|
+
[self.delegate viewControllerDidChangeDetent:index position:self.currentPosition detent:detent];
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
#pragma mark - RNSDismissibleModalProtocol
|
|
1618
|
+
|
|
1619
|
+
#if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
|
|
1620
|
+
- (BOOL)isDismissible {
|
|
1621
|
+
return NO;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
- (UIViewController *)newPresentingViewController {
|
|
1625
|
+
UIViewController *topmost = self;
|
|
1626
|
+
while (topmost.presentedViewController != nil && !topmost.presentedViewController.isBeingDismissed &&
|
|
1627
|
+
[topmost.presentedViewController isKindOfClass:[TrueSheetViewController class]]) {
|
|
1628
|
+
topmost = topmost.presentedViewController;
|
|
646
1629
|
}
|
|
1630
|
+
return topmost;
|
|
647
1631
|
}
|
|
1632
|
+
#endif
|
|
648
1633
|
|
|
649
1634
|
@end
|