@onekeyfe/react-native-native-overlay 3.0.162-alpha.268

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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/NativeOverlay.podspec +30 -0
  3. package/README.md +35 -0
  4. package/android/build.gradle +82 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAnimation.kt +176 -0
  7. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +415 -0
  8. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEvents.kt +38 -0
  9. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +174 -0
  10. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPackage.kt +16 -0
  11. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +122 -0
  12. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostViewManager.kt +40 -0
  13. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +465 -0
  14. package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayViewManager.kt +156 -0
  15. package/android/src/main/jni/CMakeLists.txt +44 -0
  16. package/android/src/main/jni/RNCNativeOverlay.h +19 -0
  17. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayComponentDescriptor.h +12 -0
  18. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.cpp +12 -0
  19. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.h +41 -0
  20. package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayState.h +40 -0
  21. package/docs/SPEC.md +693 -0
  22. package/ios/NativeOverlayAnimation.swift +160 -0
  23. package/ios/NativeOverlayContainerView.swift +688 -0
  24. package/ios/NativeOverlayKeyboard.swift +88 -0
  25. package/ios/NativeOverlayPageHostView.swift +98 -0
  26. package/ios/NativeOverlaySheetController.swift +391 -0
  27. package/ios/NativeOverlayWindowManager.swift +194 -0
  28. package/ios/RCTNativeOverlayComponentView.h +11 -0
  29. package/ios/RCTNativeOverlayComponentView.mm +193 -0
  30. package/ios/RCTNativeOverlayPageHostComponentView.h +11 -0
  31. package/ios/RCTNativeOverlayPageHostComponentView.mm +64 -0
  32. package/lib/module/NativeOverlayNativeComponent.ts +61 -0
  33. package/lib/module/NativeOverlayPageHostNativeComponent.ts +13 -0
  34. package/lib/module/OverlayErrors.js +6 -0
  35. package/lib/module/OverlayErrors.js.map +1 -0
  36. package/lib/module/OverlayLevelContext.js +27 -0
  37. package/lib/module/OverlayLevelContext.js.map +1 -0
  38. package/lib/module/OverlayLevels.js +33 -0
  39. package/lib/module/OverlayLevels.js.map +1 -0
  40. package/lib/module/OverlayPageHost.js +24 -0
  41. package/lib/module/OverlayPageHost.js.map +1 -0
  42. package/lib/module/OverlayPageHost.web.js +49 -0
  43. package/lib/module/OverlayPageHost.web.js.map +1 -0
  44. package/lib/module/OverlayPageScope.js +41 -0
  45. package/lib/module/OverlayPageScope.js.map +1 -0
  46. package/lib/module/OverlayStore.js +305 -0
  47. package/lib/module/OverlayStore.js.map +1 -0
  48. package/lib/module/OverlayView.js +141 -0
  49. package/lib/module/OverlayView.js.map +1 -0
  50. package/lib/module/OverlayView.web.js +290 -0
  51. package/lib/module/OverlayView.web.js.map +1 -0
  52. package/lib/module/OverlayViewTypes.js +4 -0
  53. package/lib/module/OverlayViewTypes.js.map +1 -0
  54. package/lib/module/animation/motionPresets.js +57 -0
  55. package/lib/module/animation/motionPresets.js.map +1 -0
  56. package/lib/module/animation/platformMotionPresets.js +4 -0
  57. package/lib/module/animation/platformMotionPresets.js.map +1 -0
  58. package/lib/module/animation/platformMotionPresets.web.js +4 -0
  59. package/lib/module/animation/platformMotionPresets.web.js.map +1 -0
  60. package/lib/module/animation/resolveAnimation.js +80 -0
  61. package/lib/module/animation/resolveAnimation.js.map +1 -0
  62. package/lib/module/animation/spring.js +69 -0
  63. package/lib/module/animation/spring.js.map +1 -0
  64. package/lib/module/animation/types.js +2 -0
  65. package/lib/module/animation/types.js.map +1 -0
  66. package/lib/module/index.js +16 -0
  67. package/lib/module/index.js.map +1 -0
  68. package/lib/module/package.json +1 -0
  69. package/lib/module/types.js +2 -0
  70. package/lib/module/types.js.map +1 -0
  71. package/lib/module/useOverlayController.js +127 -0
  72. package/lib/module/useOverlayController.js.map +1 -0
  73. package/lib/module/useSuspendedPageOwners.js +16 -0
  74. package/lib/module/useSuspendedPageOwners.js.map +1 -0
  75. package/lib/module/web/animateTransition.js +115 -0
  76. package/lib/module/web/animateTransition.js.map +1 -0
  77. package/lib/module/web/overlayLayers.js +147 -0
  78. package/lib/module/web/overlayLayers.js.map +1 -0
  79. package/lib/module/web/useKeyboardShift.js +110 -0
  80. package/lib/module/web/useKeyboardShift.js.map +1 -0
  81. package/lib/module/web/useSheetDrag.js +126 -0
  82. package/lib/module/web/useSheetDrag.js.map +1 -0
  83. package/lib/typescript/package.json +1 -0
  84. package/lib/typescript/src/NativeOverlayNativeComponent.d.ts +41 -0
  85. package/lib/typescript/src/NativeOverlayNativeComponent.d.ts.map +1 -0
  86. package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts +9 -0
  87. package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts.map +1 -0
  88. package/lib/typescript/src/OverlayErrors.d.ts +4 -0
  89. package/lib/typescript/src/OverlayErrors.d.ts.map +1 -0
  90. package/lib/typescript/src/OverlayLevelContext.d.ts +12 -0
  91. package/lib/typescript/src/OverlayLevelContext.d.ts.map +1 -0
  92. package/lib/typescript/src/OverlayLevels.d.ts +13 -0
  93. package/lib/typescript/src/OverlayLevels.d.ts.map +1 -0
  94. package/lib/typescript/src/OverlayPageHost.d.ts +9 -0
  95. package/lib/typescript/src/OverlayPageHost.d.ts.map +1 -0
  96. package/lib/typescript/src/OverlayPageHost.web.d.ts +11 -0
  97. package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -0
  98. package/lib/typescript/src/OverlayPageScope.d.ts +17 -0
  99. package/lib/typescript/src/OverlayPageScope.d.ts.map +1 -0
  100. package/lib/typescript/src/OverlayStore.d.ts +72 -0
  101. package/lib/typescript/src/OverlayStore.d.ts.map +1 -0
  102. package/lib/typescript/src/OverlayView.d.ts +3 -0
  103. package/lib/typescript/src/OverlayView.d.ts.map +1 -0
  104. package/lib/typescript/src/OverlayView.web.d.ts +3 -0
  105. package/lib/typescript/src/OverlayView.web.d.ts.map +1 -0
  106. package/lib/typescript/src/OverlayViewTypes.d.ts +68 -0
  107. package/lib/typescript/src/OverlayViewTypes.d.ts.map +1 -0
  108. package/lib/typescript/src/animation/motionPresets.d.ts +13 -0
  109. package/lib/typescript/src/animation/motionPresets.d.ts.map +1 -0
  110. package/lib/typescript/src/animation/platformMotionPresets.d.ts +2 -0
  111. package/lib/typescript/src/animation/platformMotionPresets.d.ts.map +1 -0
  112. package/lib/typescript/src/animation/platformMotionPresets.web.d.ts +2 -0
  113. package/lib/typescript/src/animation/platformMotionPresets.web.d.ts.map +1 -0
  114. package/lib/typescript/src/animation/resolveAnimation.d.ts +23 -0
  115. package/lib/typescript/src/animation/resolveAnimation.d.ts.map +1 -0
  116. package/lib/typescript/src/animation/spring.d.ts +24 -0
  117. package/lib/typescript/src/animation/spring.d.ts.map +1 -0
  118. package/lib/typescript/src/animation/types.d.ts +56 -0
  119. package/lib/typescript/src/animation/types.d.ts.map +1 -0
  120. package/lib/typescript/src/index.d.ts +18 -0
  121. package/lib/typescript/src/index.d.ts.map +1 -0
  122. package/lib/typescript/src/types.d.ts +59 -0
  123. package/lib/typescript/src/types.d.ts.map +1 -0
  124. package/lib/typescript/src/useOverlayController.d.ts +29 -0
  125. package/lib/typescript/src/useOverlayController.d.ts.map +1 -0
  126. package/lib/typescript/src/useSuspendedPageOwners.d.ts +7 -0
  127. package/lib/typescript/src/useSuspendedPageOwners.d.ts.map +1 -0
  128. package/lib/typescript/src/web/animateTransition.d.ts +18 -0
  129. package/lib/typescript/src/web/animateTransition.d.ts.map +1 -0
  130. package/lib/typescript/src/web/overlayLayers.d.ts +10 -0
  131. package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -0
  132. package/lib/typescript/src/web/useKeyboardShift.d.ts +29 -0
  133. package/lib/typescript/src/web/useKeyboardShift.d.ts.map +1 -0
  134. package/lib/typescript/src/web/useSheetDrag.d.ts +13 -0
  135. package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -0
  136. package/package.json +188 -0
  137. package/react-native.config.js +16 -0
  138. package/src/NativeOverlayNativeComponent.ts +61 -0
  139. package/src/NativeOverlayPageHostNativeComponent.ts +13 -0
  140. package/src/OverlayErrors.ts +3 -0
  141. package/src/OverlayLevelContext.tsx +32 -0
  142. package/src/OverlayLevels.ts +40 -0
  143. package/src/OverlayPageHost.tsx +21 -0
  144. package/src/OverlayPageHost.web.tsx +53 -0
  145. package/src/OverlayPageScope.tsx +42 -0
  146. package/src/OverlayStore.ts +401 -0
  147. package/src/OverlayView.tsx +165 -0
  148. package/src/OverlayView.web.tsx +356 -0
  149. package/src/OverlayViewTypes.ts +81 -0
  150. package/src/animation/motionPresets.ts +52 -0
  151. package/src/animation/platformMotionPresets.ts +1 -0
  152. package/src/animation/platformMotionPresets.web.ts +1 -0
  153. package/src/animation/resolveAnimation.ts +103 -0
  154. package/src/animation/spring.ts +80 -0
  155. package/src/animation/types.ts +71 -0
  156. package/src/index.ts +55 -0
  157. package/src/types.ts +79 -0
  158. package/src/useOverlayController.ts +199 -0
  159. package/src/useSuspendedPageOwners.ts +26 -0
  160. package/src/web/animateTransition.ts +137 -0
  161. package/src/web/overlayLayers.ts +182 -0
  162. package/src/web/useKeyboardShift.ts +130 -0
  163. package/src/web/useSheetDrag.ts +143 -0
@@ -0,0 +1,88 @@
1
+ import UIKit
2
+
3
+ /// Tracks the software keyboard frame for every overlay entry, so an entry
4
+ /// presented while the keyboard is already up avoids it too.
5
+ final class NativeOverlayKeyboard {
6
+ static let shared = NativeOverlayKeyboard()
7
+
8
+ static let didChange = Notification.Name("NativeOverlayKeyboardDidChange")
9
+
10
+ /// Screen coordinates; `.zero` while hidden.
11
+ private(set) var frame: CGRect = .zero
12
+ private(set) var duration: TimeInterval = 0.25
13
+ private(set) var curve: UIView.AnimationCurve = .easeInOut
14
+
15
+ private init() {
16
+ let center = NotificationCenter.default
17
+ center.addObserver(
18
+ self,
19
+ selector: #selector(keyboardWillChange(_:)),
20
+ name: UIResponder.keyboardWillChangeFrameNotification,
21
+ object: nil
22
+ )
23
+ center.addObserver(
24
+ self,
25
+ selector: #selector(keyboardWillHide(_:)),
26
+ name: UIResponder.keyboardWillHideNotification,
27
+ object: nil
28
+ )
29
+ }
30
+
31
+ @objc private func keyboardWillChange(_ notification: Notification) {
32
+ readAnimation(notification)
33
+ let end = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero
34
+ frame = end
35
+ NotificationCenter.default.post(name: Self.didChange, object: self)
36
+ }
37
+
38
+ @objc private func keyboardWillHide(_ notification: Notification) {
39
+ readAnimation(notification)
40
+ frame = .zero
41
+ NotificationCenter.default.post(name: Self.didChange, object: self)
42
+ }
43
+
44
+ private func readAnimation(_ notification: Notification) {
45
+ let info = notification.userInfo
46
+ duration = info?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25
47
+ if let raw = info?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int,
48
+ let value = UIView.AnimationCurve(rawValue: raw) {
49
+ curve = value
50
+ }
51
+ }
52
+
53
+ /// Keyboard top in `view`'s coordinates, or nil when it does not cover it.
54
+ func top(in view: UIView) -> CGFloat? {
55
+ guard frame.height > 0, let window = view.window else { return nil }
56
+ let inWindow = window.convert(frame, from: window.screen.coordinateSpace)
57
+ let local = view.convert(inWindow, from: window)
58
+ guard local.minY < view.bounds.maxY else { return nil }
59
+ return local.minY
60
+ }
61
+ }
62
+
63
+ enum NativeOverlayKeyboardShift {
64
+ /// Gap between centered content and the keyboard.
65
+ static let margin: CGFloat = 16
66
+
67
+ /**
68
+ How far to lift an entry's content (points, >= 0).
69
+
70
+ - Sheets rise until their content bottom clears the keyboard.
71
+ - Other content rises until its bottom is `margin` above the keyboard.
72
+ - Nothing rises above the top safe area; full-window content never moves.
73
+ */
74
+ static func compute(
75
+ keyboardTop: CGFloat?,
76
+ contentFrame: CGRect,
77
+ contentBottom: CGFloat,
78
+ isSheet: Bool,
79
+ safeTop: CGFloat
80
+ ) -> CGFloat {
81
+ guard let keyboardTop else { return 0 }
82
+ let needed = isSheet
83
+ ? contentBottom - keyboardTop
84
+ : contentFrame.maxY + margin - keyboardTop
85
+ let room = contentFrame.minY - safeTop
86
+ return max(0, min(needed, room))
87
+ }
88
+ }
@@ -0,0 +1,98 @@
1
+ import UIKit
2
+
3
+ /// Page-scope overlays of one root route render inside this view. It sits
4
+ /// in the React tree (last child of the root-route screen), so the surface's
5
+ /// own touch handler already dispatches touches to the moved content.
6
+ @objc public final class NativeOverlayPageHostView: UIView {
7
+ private struct WeakHost {
8
+ weak var value: NativeOverlayPageHostView?
9
+ }
10
+
11
+ private static var registry: [String: WeakHost] = [:]
12
+
13
+ static func host(for key: String) -> NativeOverlayPageHostView? {
14
+ registry[key]?.value
15
+ }
16
+
17
+ @objc public var hostKey = "" {
18
+ didSet {
19
+ guard hostKey != oldValue else { return }
20
+ if !oldValue.isEmpty, Self.registry[oldValue]?.value === self {
21
+ Self.registry[oldValue] = nil
22
+ }
23
+ if !hostKey.isEmpty {
24
+ Self.registry[hostKey] = WeakHost(value: self)
25
+ }
26
+ }
27
+ }
28
+
29
+ private var suspendedOwners: Set<String> = []
30
+
31
+ @objc public func setSuspendedOwners(_ joined: String) {
32
+ let owners = Set(joined.split(separator: "\n").map(String.init))
33
+ guard owners != suspendedOwners else { return }
34
+ suspendedOwners = owners
35
+ entries().forEach(applySuspension)
36
+ }
37
+
38
+ override public init(frame: CGRect) {
39
+ super.init(frame: frame)
40
+ backgroundColor = .clear
41
+ }
42
+
43
+ @available(*, unavailable)
44
+ required init?(coder: NSCoder) {
45
+ fatalError("init(coder:) has not been implemented")
46
+ }
47
+
48
+ @objc public func invalidate() {
49
+ if !hostKey.isEmpty, Self.registry[hostKey]?.value === self {
50
+ Self.registry[hostKey] = nil
51
+ }
52
+ }
53
+
54
+ override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
55
+ let hit = super.hitTest(point, with: event)
56
+ return hit === self ? nil : hit
57
+ }
58
+
59
+ override public func layoutSubviews() {
60
+ super.layoutSubviews()
61
+ entries().forEach { $0.frame = bounds }
62
+ }
63
+
64
+ /// Orders page entries by level, then by request order.
65
+ func insert(_ entry: NativeOverlayEntryView) {
66
+ if entry.superview !== self {
67
+ entry.removeFromSuperview()
68
+ entry.frame = bounds
69
+ let above = entries().first { Self.sortKey($0) > Self.sortKey(entry) }
70
+ if let above {
71
+ insertSubview(entry, belowSubview: above)
72
+ } else {
73
+ addSubview(entry)
74
+ }
75
+ }
76
+ applySuspension(entry)
77
+ }
78
+
79
+ private func applySuspension(_ entry: NativeOverlayEntryView) {
80
+ let hidden = suspendedOwners.contains(entry.ownerKey)
81
+ guard entry.isHidden != hidden else { return }
82
+ if hidden {
83
+ entry.isHidden = true
84
+ } else {
85
+ entry.alpha = 0
86
+ entry.isHidden = false
87
+ UIView.animate(withDuration: 0.15) { entry.alpha = 1 }
88
+ }
89
+ }
90
+
91
+ private func entries() -> [NativeOverlayEntryView] {
92
+ subviews.compactMap { $0 as? NativeOverlayEntryView }
93
+ }
94
+
95
+ private static func sortKey(_ entry: NativeOverlayEntryView) -> (Int, Int) {
96
+ (entry.levelOrder, entry.stackOrder)
97
+ }
98
+ }
@@ -0,0 +1,391 @@
1
+ import UIKit
2
+
3
+ /// Callbacks from a presented sheet back to its `NativeOverlayContainerView`.
4
+ protocol NativeOverlaySheetHost: AnyObject {
5
+ var dismissOnPanDown: Bool { get }
6
+ func sheetDidLoad(_ controller: NativeOverlaySheetController)
7
+ /// The sheet's frame settled (presentation, detent or keyboard change).
8
+ func sheetDidLayout(_ controller: NativeOverlaySheetController)
9
+ func sheetDidDismissInteractively(_ controller: NativeOverlaySheetController)
10
+ /// UIKit dismissed the sheet without being asked, e.g. because a sheet
11
+ /// below it in the same level window was dismissed.
12
+ func sheetDidDisappearUnexpectedly(_ controller: NativeOverlaySheetController)
13
+ func sheetRequestedDismiss(_ controller: NativeOverlaySheetController, reason: String)
14
+ }
15
+
16
+ struct NativeOverlaySheetStyle {
17
+ var height: CGFloat
18
+ var backgroundColor: UIColor
19
+ var cornerRadius: CGFloat
20
+ var showHandle: Bool
21
+ var backdropColor: UIColor?
22
+ var dismissOnBackdropPress: Bool
23
+ var enterMotion: NativeOverlayMotion
24
+ var exitMotion: NativeOverlayMotion
25
+ }
26
+
27
+ /// Slides the sheet with the overlay's own motion instead of UIKit's curve
28
+ /// and fades the custom dimming view on the same clock.
29
+ private final class NativeOverlaySheetTransitionAnimator: NSObject,
30
+ UIViewControllerAnimatedTransitioning {
31
+ private let presenting: Bool
32
+ private let motion: NativeOverlayMotion
33
+ private var animator: UIViewPropertyAnimator?
34
+
35
+ init(presenting: Bool, motion: NativeOverlayMotion) {
36
+ self.presenting = presenting
37
+ self.motion = motion
38
+ }
39
+
40
+ func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
41
+ motion.makeAnimator().duration
42
+ }
43
+
44
+ func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
45
+ interruptibleAnimator(using: transitionContext).startAnimation()
46
+ }
47
+
48
+ func interruptibleAnimator(
49
+ using transitionContext: UIViewControllerContextTransitioning
50
+ ) -> UIViewImplicitlyAnimating {
51
+ if let animator {
52
+ return animator
53
+ }
54
+ let viewKey: UITransitionContextViewKey = presenting ? .to : .from
55
+ let controllerKey: UITransitionContextViewControllerKey = presenting ? .to : .from
56
+ guard let sheetView = transitionContext.view(forKey: viewKey),
57
+ let sheetController = transitionContext.viewController(forKey: controllerKey)
58
+ as? NativeOverlaySheetController else {
59
+ transitionContext.completeTransition(false)
60
+ return motion.makeAnimator()
61
+ }
62
+ let container = transitionContext.containerView
63
+ let finalFrame = presenting ? transitionContext.finalFrame(for: sheetController) : sheetView.frame
64
+ let travel = max(container.bounds.maxY - finalFrame.minY, sheetView.bounds.height)
65
+ let offscreen = CGAffineTransform(translationX: 0, y: travel)
66
+ let reduceMotion = UIAccessibility.isReduceMotionEnabled
67
+
68
+ if presenting {
69
+ sheetView.frame = finalFrame
70
+ if reduceMotion {
71
+ sheetView.alpha = 0
72
+ } else {
73
+ sheetView.transform = offscreen
74
+ }
75
+ sheetController.setDimmingAlpha(0)
76
+ if sheetView.superview == nil {
77
+ container.addSubview(sheetView)
78
+ }
79
+ }
80
+
81
+ let animator = motion.makeAnimator()
82
+ animator.addAnimations {
83
+ if reduceMotion {
84
+ sheetView.alpha = self.presenting ? 1 : 0
85
+ } else {
86
+ sheetView.transform = self.presenting ? .identity : offscreen
87
+ }
88
+ sheetController.setDimmingAlpha(self.presenting ? 1 : 0)
89
+ }
90
+ animator.addCompletion { position in
91
+ let completed = position == .end && !transitionContext.transitionWasCancelled
92
+ if !completed {
93
+ sheetView.transform = self.presenting ? offscreen : .identity
94
+ sheetController.setDimmingAlpha(self.presenting ? 0 : 1)
95
+ }
96
+ transitionContext.completeTransition(completed)
97
+ }
98
+ self.animator = animator
99
+ return animator
100
+ }
101
+
102
+ func animationEnded(_ transitionCompleted: Bool) {
103
+ animator = nil
104
+ }
105
+ }
106
+
107
+ /// A `.pageSheet` with one custom detent, ported from native-sheet. It is
108
+ /// presented from its level window, so sheets of different levels never
109
+ /// share a presentation chain.
110
+ final class NativeOverlaySheetController: UIViewController,
111
+ UIAdaptivePresentationControllerDelegate,
112
+ UIViewControllerTransitioningDelegate {
113
+ private weak var host: NativeOverlaySheetHost?
114
+ private let detentIdentifier = UISheetPresentationController.Detent.Identifier(
115
+ "onekey.nativeOverlay.sheet"
116
+ )
117
+ private var style: NativeOverlaySheetStyle
118
+ private let backgroundView = UIView()
119
+ private var dimmingView: UIView?
120
+ private var backdropTap: UITapGestureRecognizer?
121
+ private var heightAnimator: UIViewPropertyAnimator?
122
+ private var shadowSuppressionDisplayLink: CADisplayLink?
123
+ /// Set while the host itself asked UIKit to dismiss.
124
+ var dismissRequestedByHost = false
125
+
126
+ init(host: NativeOverlaySheetHost, style: NativeOverlaySheetStyle) {
127
+ self.host = host
128
+ self.style = style
129
+ super.init(nibName: nil, bundle: nil)
130
+ modalPresentationStyle = .pageSheet
131
+ transitioningDelegate = self
132
+ }
133
+
134
+ @available(*, unavailable)
135
+ required init?(coder: NSCoder) {
136
+ fatalError("init(coder:) has not been implemented")
137
+ }
138
+
139
+ override func loadView() {
140
+ let rootView = UIView()
141
+ rootView.backgroundColor = style.backgroundColor
142
+ rootView.clipsToBounds = true
143
+ if #available(iOS 26.0, *) {
144
+ backgroundView.frame = rootView.bounds
145
+ backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
146
+ backgroundView.backgroundColor = style.backgroundColor
147
+ backgroundView.isUserInteractionEnabled = false
148
+ rootView.addSubview(backgroundView)
149
+ }
150
+ view = rootView
151
+ }
152
+
153
+ deinit {
154
+ shadowSuppressionDisplayLink?.invalidate()
155
+ dimmingView?.removeFromSuperview()
156
+ }
157
+
158
+ override func viewDidLoad() {
159
+ super.viewDidLoad()
160
+ host?.sheetDidLoad(self)
161
+ guard let sheet = sheetPresentationController else { return }
162
+ let detent = UISheetPresentationController.Detent.custom(
163
+ identifier: detentIdentifier
164
+ ) { [weak self] context in
165
+ min(max(self?.style.height ?? 1, 1), context.maximumDetentValue)
166
+ }
167
+ if #available(iOS 26.1, *) {
168
+ let effect = UIColorEffect(color: style.backgroundColor)
169
+ sheet.backgroundEffect = effect
170
+ detent.backgroundEffect = effect
171
+ }
172
+ sheet.detents = [detent]
173
+ sheet.selectedDetentIdentifier = detentIdentifier
174
+ sheet.largestUndimmedDetentIdentifier = detentIdentifier
175
+ sheet.prefersGrabberVisible = style.showHandle
176
+ sheet.preferredCornerRadius = style.cornerRadius
177
+ sheet.prefersScrollingExpandsWhenScrolledToEdge = false
178
+ sheet.prefersEdgeAttachedInCompactHeight = true
179
+ sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = false
180
+ presentationController?.delegate = self
181
+ isModalInPresentation = !(host?.dismissOnPanDown ?? true)
182
+ }
183
+
184
+ func update(style next: NativeOverlaySheetStyle, animated: Bool) {
185
+ let previousHeight = style.height
186
+ style.height = max(next.height, 1)
187
+ style.dismissOnBackdropPress = next.dismissOnBackdropPress
188
+ style.backdropColor = next.backdropColor
189
+ style.exitMotion = next.exitMotion
190
+ dimmingView?.backgroundColor = next.backdropColor ?? .clear
191
+ isModalInPresentation = !(host?.dismissOnPanDown ?? true)
192
+ guard abs(style.height - previousHeight) >= 0.5,
193
+ isViewLoaded,
194
+ let sheet = sheetPresentationController else { return }
195
+ let changes = { [weak self, weak sheet] in
196
+ guard let self, let sheet else { return }
197
+ sheet.invalidateDetents()
198
+ sheet.selectedDetentIdentifier = self.detentIdentifier
199
+ sheet.containerView?.layoutIfNeeded()
200
+ }
201
+ guard animated else {
202
+ changes()
203
+ return
204
+ }
205
+ if let heightAnimator {
206
+ heightAnimator.stopAnimation(false)
207
+ heightAnimator.finishAnimation(at: .current)
208
+ }
209
+ sheet.containerView?.layoutIfNeeded()
210
+ let animator = style.enterMotion.makeAnimator()
211
+ heightAnimator = animator
212
+ animator.addAnimations(changes)
213
+ animator.addCompletion { [weak self, weak animator] _ in
214
+ guard let self, self.heightAnimator === animator else { return }
215
+ self.heightAnimator = nil
216
+ }
217
+ animator.startAnimation()
218
+ }
219
+
220
+ // MARK: Transitions
221
+
222
+ func animationController(
223
+ forPresented presented: UIViewController,
224
+ presenting: UIViewController,
225
+ source: UIViewController
226
+ ) -> UIViewControllerAnimatedTransitioning? {
227
+ NativeOverlaySheetTransitionAnimator(presenting: true, motion: style.enterMotion)
228
+ }
229
+
230
+ func animationController(
231
+ forDismissed dismissed: UIViewController
232
+ ) -> UIViewControllerAnimatedTransitioning? {
233
+ NativeOverlaySheetTransitionAnimator(presenting: false, motion: style.exitMotion)
234
+ }
235
+
236
+ override func viewWillAppear(_ animated: Bool) {
237
+ super.viewWillAppear(animated)
238
+ installDimmingView()
239
+ startSuppressingPresentationShadow()
240
+ }
241
+
242
+ override func viewDidAppear(_ animated: Bool) {
243
+ super.viewDidAppear(animated)
244
+ stopSuppressingPresentationShadow()
245
+ }
246
+
247
+ override func viewWillDisappear(_ animated: Bool) {
248
+ super.viewWillDisappear(animated)
249
+ stopSuppressingPresentationShadow()
250
+ }
251
+
252
+ override func viewDidDisappear(_ animated: Bool) {
253
+ super.viewDidDisappear(animated)
254
+ if let backdropTap {
255
+ dimmingView?.removeGestureRecognizer(backdropTap)
256
+ }
257
+ backdropTap = nil
258
+ dimmingView?.removeFromSuperview()
259
+ dimmingView = nil
260
+ if presentingViewController == nil && !dismissRequestedByHost {
261
+ host?.sheetDidDisappearUnexpectedly(self)
262
+ }
263
+ }
264
+
265
+ override func viewDidLayoutSubviews() {
266
+ super.viewDidLayoutSubviews()
267
+ host?.sheetDidLayout(self)
268
+ removePresentationShadow()
269
+ if #available(iOS 26.0, *) {
270
+ DispatchQueue.main.async { [weak self] in
271
+ self?.removePresentationShadow()
272
+ }
273
+ }
274
+ }
275
+
276
+ override func accessibilityPerformEscape() -> Bool {
277
+ host?.sheetRequestedDismiss(self, reason: "back")
278
+ return true
279
+ }
280
+
281
+ func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
282
+ host?.dismissOnPanDown ?? true
283
+ }
284
+
285
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
286
+ dismissRequestedByHost = true
287
+ host?.sheetDidDismissInteractively(self)
288
+ }
289
+
290
+ // MARK: Backdrop
291
+
292
+ func setDimmingAlpha(_ alpha: CGFloat) {
293
+ dimmingView?.alpha = alpha
294
+ }
295
+
296
+ private func installDimmingView() {
297
+ guard dimmingView == nil, let container = presentationController?.containerView else { return }
298
+ let dimmingView = UIView(frame: container.bounds)
299
+ dimmingView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
300
+ dimmingView.backgroundColor = style.backdropColor ?? .clear
301
+ dimmingView.alpha = 0
302
+ // Always swallow backdrop touches; a sheet is blocking even without a tap-to-close.
303
+ dimmingView.isUserInteractionEnabled = true
304
+ container.insertSubview(dimmingView, at: 0)
305
+ let recognizer = UITapGestureRecognizer(target: self, action: #selector(handleBackdropTap(_:)))
306
+ recognizer.cancelsTouchesInView = true
307
+ dimmingView.addGestureRecognizer(recognizer)
308
+ backdropTap = recognizer
309
+ self.dimmingView = dimmingView
310
+ }
311
+
312
+ @objc private func handleBackdropTap(_ recognizer: UITapGestureRecognizer) {
313
+ guard recognizer.state == .ended, style.dismissOnBackdropPress else { return }
314
+ host?.sheetRequestedDismiss(self, reason: "backdrop")
315
+ }
316
+
317
+ // MARK: Shadow suppression (same approach as native-sheet)
318
+
319
+ private func removePresentationShadow() {
320
+ guard let container = presentationController?.containerView else { return }
321
+ var ancestor = view.superview
322
+ while let current = ancestor, current !== container {
323
+ clearShadow(on: current)
324
+ ancestor = current.superview
325
+ }
326
+ removeDropShadowViews(in: container)
327
+ }
328
+
329
+ private func removeDropShadowViews(in candidate: UIView) {
330
+ guard candidate !== view else { return }
331
+ let className = NSStringFromClass(type(of: candidate))
332
+ guard className.hasPrefix("UI") || className.hasPrefix("_UI") else { return }
333
+ if className.contains("DropShadowView") {
334
+ clearShadow(on: candidate)
335
+ }
336
+ candidate.subviews.forEach(removeDropShadowViews(in:))
337
+ }
338
+
339
+ private func clearShadow(on shadowView: UIView) {
340
+ CATransaction.begin()
341
+ CATransaction.setDisableActions(true)
342
+ if #available(iOS 26.0, *),
343
+ NSStringFromClass(type(of: shadowView)).contains("UIDropShadowView") {
344
+ compensateForPresentationScale(in: shadowView)
345
+ }
346
+ shadowView.layer.shadowOpacity = 0
347
+ shadowView.layer.shadowRadius = 0
348
+ shadowView.layer.shadowColor = UIColor.clear.cgColor
349
+ shadowView.layer.shadowPath = nil
350
+ CATransaction.commit()
351
+ }
352
+
353
+ @available(iOS 26.0, *)
354
+ private func compensateForPresentationScale(in shadowView: UIView) {
355
+ guard var surfaceView = view else { return }
356
+ while let parent = surfaceView.superview, parent !== shadowView {
357
+ surfaceView = parent
358
+ }
359
+ guard surfaceView.superview === shadowView else { return }
360
+ let transform = shadowView.transform
361
+ guard abs(transform.a) > 0.001, abs(transform.d) > 0.001 else { return }
362
+ surfaceView.transform = CGAffineTransform(scaleX: 1 / transform.a, y: 1 / transform.d)
363
+ }
364
+
365
+ private func startSuppressingPresentationShadow() {
366
+ shadowSuppressionDisplayLink?.invalidate()
367
+ removePresentationShadow()
368
+ let displayLink = CADisplayLink(target: self, selector: #selector(suppressShadowForFrame))
369
+ displayLink.add(to: .main, forMode: .common)
370
+ shadowSuppressionDisplayLink = displayLink
371
+ if let transitionCoordinator {
372
+ transitionCoordinator.animate(alongsideTransition: nil) { [weak self] _ in
373
+ self?.stopSuppressingPresentationShadow()
374
+ }
375
+ } else {
376
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
377
+ self?.stopSuppressingPresentationShadow()
378
+ }
379
+ }
380
+ }
381
+
382
+ private func stopSuppressingPresentationShadow() {
383
+ shadowSuppressionDisplayLink?.invalidate()
384
+ shadowSuppressionDisplayLink = nil
385
+ removePresentationShadow()
386
+ }
387
+
388
+ @objc private func suppressShadowForFrame() {
389
+ removePresentationShadow()
390
+ }
391
+ }