@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,688 @@
1
+ import React
2
+ import UIKit
3
+
4
+ /// One overlay inside a level window: backdrop plus the reparented content.
5
+ final class NativeOverlayEntryView: UIView {
6
+ let backdropView = UIView()
7
+ /// Carries the keyboard avoidance lift, so it composes with the enter /
8
+ /// exit transforms animated on `contentView`.
9
+ let keyboardLayer = UIView()
10
+ let contentView = UIView()
11
+ var stackOrder = 0
12
+ var levelOrder = 0
13
+ /// Page scope: the owning page, used by the page host to hide the entry.
14
+ var ownerKey = ""
15
+ var blocking = true
16
+ var dismissOnBackPress = true
17
+ var onEscape: (() -> Void)?
18
+ var onBackdropTap: (() -> Void)?
19
+ /// Page sheet: the drag closed it past the threshold.
20
+ var onPanDismiss: (() -> Void)?
21
+
22
+ /// Page scope cannot use UIKit sheets (they always cover the window), so a
23
+ /// page sheet is this entry's content view pinned to the bottom.
24
+ private(set) var pageSheetHeight: CGFloat?
25
+ private var pageSheetPan: UIPanGestureRecognizer?
26
+
27
+ override init(frame: CGRect) {
28
+ super.init(frame: frame)
29
+ backgroundColor = .clear
30
+ backdropView.frame = bounds
31
+ backdropView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
32
+ backdropView.alpha = 0
33
+ keyboardLayer.frame = bounds
34
+ keyboardLayer.autoresizingMask = [.flexibleWidth, .flexibleHeight]
35
+ keyboardLayer.backgroundColor = .clear
36
+ contentView.frame = bounds
37
+ contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
38
+ contentView.backgroundColor = .clear
39
+ addSubview(backdropView)
40
+ addSubview(keyboardLayer)
41
+ keyboardLayer.addSubview(contentView)
42
+ let tap = UITapGestureRecognizer(target: self, action: #selector(handleBackdropTap))
43
+ tap.cancelsTouchesInView = false
44
+ backdropView.addGestureRecognizer(tap)
45
+ NotificationCenter.default.addObserver(
46
+ self,
47
+ selector: #selector(keyboardDidChange),
48
+ name: NativeOverlayKeyboard.didChange,
49
+ object: nil
50
+ )
51
+ }
52
+
53
+ @available(*, unavailable)
54
+ required init?(coder: NSCoder) {
55
+ fatalError("init(coder:) has not been implemented")
56
+ }
57
+
58
+ /// Touches that miss the React content either hit the backdrop (blocking)
59
+ /// or fall through to the windows below (toast, debug).
60
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
61
+ guard let hit = super.hitTest(point, with: event) else { return nil }
62
+ // The React root is `pointerEvents="box-none"`, so Fabric returns nil for
63
+ // it and the plain content view reports itself when nothing was hit.
64
+ if hit === self || hit === keyboardLayer || hit === contentView || hit === backdropView {
65
+ return blocking ? backdropView : nil
66
+ }
67
+ return hit
68
+ }
69
+
70
+ override func accessibilityPerformEscape() -> Bool {
71
+ guard blocking else { return false }
72
+ if dismissOnBackPress {
73
+ onEscape?()
74
+ }
75
+ return true
76
+ }
77
+
78
+ @objc private func handleBackdropTap() {
79
+ onBackdropTap?()
80
+ }
81
+
82
+ func configurePageSheet(
83
+ height: CGFloat,
84
+ backgroundColor: UIColor,
85
+ cornerRadius: CGFloat,
86
+ draggable: Bool
87
+ ) {
88
+ pageSheetHeight = height
89
+ contentView.backgroundColor = backgroundColor
90
+ contentView.layer.cornerRadius = cornerRadius
91
+ contentView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
92
+ contentView.layer.cornerCurve = .continuous
93
+ contentView.clipsToBounds = true
94
+ contentView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
95
+ layoutPageSheet()
96
+ if draggable, pageSheetPan == nil {
97
+ let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePageSheetPan(_:)))
98
+ contentView.addGestureRecognizer(pan)
99
+ pageSheetPan = pan
100
+ }
101
+ pageSheetPan?.isEnabled = draggable
102
+ }
103
+
104
+ /// Layout changed (rotation, page sheet height); the owner re-reports
105
+ /// where its content is.
106
+ var onLayout: (() -> Void)?
107
+
108
+ override func layoutSubviews() {
109
+ super.layoutSubviews()
110
+ layoutPageSheet()
111
+ updateKeyboardShift(animated: false)
112
+ onLayout?()
113
+ }
114
+
115
+ override func didMoveToWindow() {
116
+ super.didMoveToWindow()
117
+ updateKeyboardShift(animated: false)
118
+ }
119
+
120
+ // MARK: - Keyboard
121
+
122
+ private var keyboardShift: CGFloat = 0
123
+
124
+ @objc private func keyboardDidChange() {
125
+ updateKeyboardShift(animated: true)
126
+ }
127
+
128
+ /// Lifts the content above the software keyboard (see
129
+ /// `NativeOverlayKeyboardShift`). A full-window content never moves.
130
+ func updateKeyboardShift(animated: Bool) {
131
+ let keyboard = NativeOverlayKeyboard.shared
132
+ let isSheet = pageSheetHeight != nil
133
+ // The resting frame, independent of the running enter / exit transform.
134
+ let contentFrame: CGRect
135
+ if isSheet {
136
+ let height = min(pageSheetHeight ?? 0, bounds.height)
137
+ contentFrame = CGRect(x: 0, y: bounds.height - height, width: bounds.width, height: height)
138
+ } else {
139
+ contentFrame = contentExtent()
140
+ }
141
+ let shift = NativeOverlayKeyboardShift.compute(
142
+ keyboardTop: window == nil ? nil : keyboard.top(in: self),
143
+ contentFrame: contentFrame,
144
+ contentBottom: contentFrame.maxY,
145
+ isSheet: isSheet,
146
+ safeTop: safeAreaInsets.top
147
+ )
148
+ guard abs(shift - keyboardShift) >= 0.5 else { return }
149
+ keyboardShift = shift
150
+ let apply = { self.keyboardLayer.transform = CGAffineTransform(translationX: 0, y: -shift) }
151
+ guard animated else {
152
+ apply()
153
+ onLayout?()
154
+ return
155
+ }
156
+ let animator = UIViewPropertyAnimator(duration: keyboard.duration, curve: keyboard.curve, animations: apply)
157
+ animator.addCompletion { [weak self] _ in self?.onLayout?() }
158
+ animator.startAnimation()
159
+ }
160
+
161
+ private func layoutPageSheet() {
162
+ guard let height = pageSheetHeight else { return }
163
+ let sheetHeight = min(height, bounds.height)
164
+ // Keep the transform while animating; only the resting frame changes.
165
+ let transform = contentView.transform
166
+ contentView.transform = .identity
167
+ contentView.frame = CGRect(x: 0, y: bounds.height - sheetHeight, width: bounds.width, height: sheetHeight)
168
+ contentView.transform = transform
169
+ }
170
+
171
+ @objc private func handlePageSheetPan(_ pan: UIPanGestureRecognizer) {
172
+ let travel = max(pan.translation(in: self).y, 0)
173
+ let height = max(contentView.bounds.height, 1)
174
+ switch pan.state {
175
+ case .changed:
176
+ contentView.transform = CGAffineTransform(translationX: 0, y: travel)
177
+ backdropView.alpha = 1 - min(travel / height, 1)
178
+ case .ended, .cancelled:
179
+ let velocity = pan.velocity(in: self).y
180
+ if pan.state == .ended, travel > height * 0.4 || velocity > 1000 {
181
+ UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut]) {
182
+ self.contentView.transform = CGAffineTransform(translationX: 0, y: height)
183
+ self.backdropView.alpha = 0
184
+ } completion: { _ in
185
+ self.onPanDismiss?()
186
+ }
187
+ } else {
188
+ UIView.animate(withDuration: 0.25, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0) {
189
+ self.contentView.transform = .identity
190
+ self.backdropView.alpha = 1
191
+ }
192
+ }
193
+ default:
194
+ break
195
+ }
196
+ }
197
+
198
+ /// Union of the rendered React children; the root React view fills the window.
199
+ func contentExtent() -> CGRect {
200
+ let roots = contentView.subviews
201
+ let rects = roots.flatMap { root in
202
+ root.subviews.map { root.convert($0.frame, to: contentView) }
203
+ }
204
+ guard var union = rects.first else { return bounds }
205
+ rects.dropFirst().forEach { union = union.union($0) }
206
+ return union
207
+ }
208
+ }
209
+
210
+ @objc public final class NativeOverlayContainerView: UIView {
211
+ @objc public var visible = false
212
+ @objc public var level = "modal"
213
+ @objc public var scope = "global"
214
+ @objc public var hostKey = ""
215
+ @objc public var ownerKey = ""
216
+ @objc public var presentation = "center"
217
+ @objc public var stackOrder = 0
218
+ @objc public var blocking = true
219
+ @objc public var dismissOnBackPress = true
220
+ @objc public var dismissOnBackdropPress = false
221
+ @objc public var backdropColor: UIColor?
222
+ @objc public var sheetHeight: CGFloat = 0
223
+ @objc public var sheetCornerRadius: CGFloat = 24
224
+ @objc public var showHandle = false
225
+ @objc public var sheetBackgroundColor: UIColor?
226
+ @objc public var dismissOnPanDown = true
227
+ @objc public var animationConfig = ""
228
+ @objc public var touchHandler: UIGestureRecognizer?
229
+ @objc public var onPresented: RCTDirectEventBlock?
230
+ @objc public var onDismissed: RCTDirectEventBlock?
231
+ @objc public var onRequestDismiss: RCTDirectEventBlock?
232
+ /// Window origin (points) of the view the content lives in; the shadow
233
+ /// node offsets `measure` / `measureInWindow` by it.
234
+ @objc public var onContentOffsetChanged: ((CGPoint) -> Void)?
235
+ private var reportedContentOffset: CGPoint?
236
+
237
+ private enum Phase {
238
+ case hidden
239
+ case entering
240
+ case shown
241
+ case exiting
242
+ }
243
+
244
+ private var phase: Phase = .hidden
245
+ private weak var contentChild: UIView?
246
+ private var entryView: NativeOverlayEntryView?
247
+ private var sheetController: NativeOverlaySheetController?
248
+ private weak var hostWindow: UIWindow?
249
+ private var isSheet: Bool { presentation == "sheet" }
250
+ private var isPageScope: Bool { scope == "page" }
251
+ /// UIKit sheets only for global scope; page sheets are drawn by the entry.
252
+ private var usesSystemSheet: Bool { isSheet && !isPageScope }
253
+ private var animator: UIViewPropertyAnimator?
254
+ private var backdropAnimator: UIViewPropertyAnimator?
255
+ private var parsedConfigSource: String?
256
+ private var config = NativeOverlayAnimationConfig()
257
+ /// True between a visible=true commit and the matching onDismissed.
258
+ private var cycleOpen = false
259
+
260
+ override public func didMoveToWindow() {
261
+ super.didMoveToWindow()
262
+ if window != nil {
263
+ commitConfiguration()
264
+ } else if phase != .hidden && !isPageScope {
265
+ // A page overlay's owner screen leaves the window when another screen
266
+ // is pushed over it; the page host hides the entry, it is not closed.
267
+ finishDismiss(reason: "system")
268
+ }
269
+ }
270
+
271
+ override public init(frame: CGRect) {
272
+ super.init(frame: frame)
273
+ // The staged child lives in this zero-size view while hidden.
274
+ clipsToBounds = true
275
+ // Start tracking the keyboard with the first overlay host (the toaster
276
+ // host mounts at launch), so later entries know a keyboard already up.
277
+ _ = NativeOverlayKeyboard.shared
278
+ }
279
+
280
+ @available(*, unavailable)
281
+ required init?(coder: NSCoder) {
282
+ fatalError("init(coder:) has not been implemented")
283
+ }
284
+
285
+ @objc public func insertChild(_ child: UIView, atIndex index: Int) {
286
+ contentChild = child
287
+ child.removeFromSuperview()
288
+ if let sheetController, sheetController.isViewLoaded {
289
+ sheetController.view.addSubview(child)
290
+ } else if let entryView {
291
+ entryView.contentView.addSubview(child)
292
+ } else {
293
+ stageContent()
294
+ }
295
+ reportContentOffset()
296
+ }
297
+
298
+ /// Keeps the React child attached (inside this clipped, zero-size view)
299
+ /// while hidden, so Fabric keeps reporting its layout; fitted sheets size
300
+ /// themselves from that measurement before presenting.
301
+ private func stageContent() {
302
+ guard let child = contentChild, child.superview !== self else { return }
303
+ child.removeFromSuperview()
304
+ addSubview(child)
305
+ }
306
+
307
+ @objc public func removeChild(_ child: UIView) {
308
+ guard contentChild === child else { return }
309
+ child.removeFromSuperview()
310
+ contentChild = nil
311
+ }
312
+
313
+ @objc public func commitConfiguration() {
314
+ if parsedConfigSource != animationConfig {
315
+ parsedConfigSource = animationConfig
316
+ config = NativeOverlayAnimationConfig(jsonString: animationConfig)
317
+ }
318
+ if let entryView {
319
+ applyEntryConfiguration(entryView)
320
+ }
321
+ if let sheetController, phase == .entering || phase == .shown {
322
+ sheetController.update(style: sheetStyle(), animated: phase == .shown)
323
+ }
324
+ if visible {
325
+ cycleOpen = true
326
+ // A sheet that is still animating out is re-presented after it finishes.
327
+ if phase == .hidden || (phase == .exiting && !usesSystemSheet) {
328
+ present()
329
+ }
330
+ } else if cycleOpen {
331
+ if phase == .entering || phase == .shown {
332
+ dismiss()
333
+ } else if phase == .hidden {
334
+ cycleOpen = false
335
+ onDismissed?(["reason": "programmatic"])
336
+ }
337
+ }
338
+ }
339
+
340
+ @objc public func invalidate() {
341
+ if phase != .hidden {
342
+ finishDismiss(reason: nil)
343
+ }
344
+ onPresented = nil
345
+ onDismissed = nil
346
+ onRequestDismiss = nil
347
+ }
348
+
349
+ // MARK: - Presentation
350
+
351
+ private func present() {
352
+ guard let overlayLevel = NativeOverlayLevel(rawValue: level) else { return }
353
+ let entry: NativeOverlayEntryView
354
+ if isPageScope {
355
+ guard let pageHost = NativeOverlayPageHostView.host(for: hostKey) else { return }
356
+ entry = entryView ?? makeEntryView(in: pageHost)
357
+ entry.levelOrder = overlayLevel.order
358
+ entry.ownerKey = ownerKey
359
+ applyEntryConfiguration(entry)
360
+ pageHost.insert(entry)
361
+ } else {
362
+ guard let appWindow = window,
363
+ let host = NativeOverlayWindowManager.shared.window(for: overlayLevel, appWindow: appWindow)
364
+ else { return }
365
+ hostWindow = host
366
+ if isSheet {
367
+ presentSheet(in: host)
368
+ return
369
+ }
370
+ entry = entryView ?? makeEntryView(in: host)
371
+ applyEntryConfiguration(entry)
372
+ insert(entry, into: host)
373
+ }
374
+ if let child = contentChild, child.superview !== entry.contentView {
375
+ entry.contentView.addSubview(child)
376
+ }
377
+ entry.layoutIfNeeded()
378
+
379
+ let transition = config.enter.accessibilityAdjusted
380
+ if phase == .hidden {
381
+ entry.contentView.alpha = transition.hidesOpacity ? 0 : 1
382
+ entry.contentView.transform = transition.hiddenTransform(
383
+ contentExtent: entry.contentExtent(),
384
+ bounds: entry.bounds
385
+ )
386
+ entry.backdropView.alpha = 0
387
+ }
388
+ stopAnimators()
389
+ phase = .entering
390
+ if blocking {
391
+ UIAccessibility.post(notification: .screenChanged, argument: entry)
392
+ }
393
+
394
+ if transition.kind == .none {
395
+ entry.contentView.alpha = 1
396
+ entry.contentView.transform = .identity
397
+ entry.backdropView.alpha = 1
398
+ phase = .shown
399
+ didPresent()
400
+ return
401
+ }
402
+ let contentAnimator = transition.motion.makeAnimator()
403
+ contentAnimator.addAnimations {
404
+ entry.contentView.alpha = 1
405
+ entry.contentView.transform = .identity
406
+ }
407
+ contentAnimator.addCompletion { [weak self] position in
408
+ guard let self, position == .end, self.phase == .entering else { return }
409
+ self.phase = .shown
410
+ self.didPresent()
411
+ }
412
+ let backdrop = config.backdropMotion.makeAnimator()
413
+ backdrop.addAnimations { entry.backdropView.alpha = 1 }
414
+ animator = contentAnimator
415
+ backdropAnimator = backdrop
416
+ contentAnimator.startAnimation()
417
+ backdrop.startAnimation()
418
+ }
419
+
420
+ private func dismiss() {
421
+ if usesSystemSheet || sheetController != nil {
422
+ dismissSheet()
423
+ return
424
+ }
425
+ guard let entry = entryView else {
426
+ finishDismiss(reason: "programmatic")
427
+ return
428
+ }
429
+ stopAnimators()
430
+ phase = .exiting
431
+ let transition = config.exit.accessibilityAdjusted
432
+ guard transition.kind != .none else {
433
+ finishDismiss(reason: "programmatic")
434
+ return
435
+ }
436
+ let hiddenTransform = transition.hiddenTransform(
437
+ contentExtent: entry.contentExtent(),
438
+ bounds: entry.bounds
439
+ )
440
+ let contentAnimator = transition.motion.makeAnimator()
441
+ contentAnimator.addAnimations {
442
+ if transition.hidesOpacity {
443
+ entry.contentView.alpha = 0
444
+ }
445
+ entry.contentView.transform = hiddenTransform
446
+ }
447
+ contentAnimator.addCompletion { [weak self] position in
448
+ guard let self, position == .end, self.phase == .exiting else { return }
449
+ self.finishDismiss(reason: "programmatic")
450
+ }
451
+ let backdrop = config.backdropMotion.makeAnimator()
452
+ backdrop.addAnimations { entry.backdropView.alpha = 0 }
453
+ animator = contentAnimator
454
+ backdropAnimator = backdrop
455
+ contentAnimator.startAnimation()
456
+ backdrop.startAnimation()
457
+ }
458
+
459
+ /// `reason == nil` tears down without notifying JS (view invalidated).
460
+ private func finishDismiss(reason: String?) {
461
+ stopAnimators()
462
+ phase = .hidden
463
+ teardownSheet()
464
+ if let entry = entryView {
465
+ if let touchHandler, touchHandler.view === entry {
466
+ touchHandler.perform(NSSelectorFromString("detachFromView:"), with: entry)
467
+ }
468
+ stageContent()
469
+ entry.removeFromSuperview()
470
+ }
471
+ entryView = nil
472
+ stageContent()
473
+ reportContentOffset()
474
+ NativeOverlayWindowManager.shared.entryRemoved(from: hostWindow)
475
+ guard let reason, cycleOpen else { return }
476
+ cycleOpen = visible
477
+ onDismissed?(["reason": reason])
478
+ // Reopened while the exit animation ran; UIKit-initiated dismissals
479
+ // ("system") are left for JS to settle instead.
480
+ if reason == "programmatic", visible, window != nil {
481
+ present()
482
+ }
483
+ }
484
+
485
+ private func didPresent() {
486
+ reportContentOffset()
487
+ onPresented?(["stackOrder": stackOrder])
488
+ }
489
+
490
+ /// Reports the window origin of the content's current superview (the
491
+ /// level window, sheet, page host, or this host while staged). Only at
492
+ /// rest: the enter / exit transforms would skew it, while UIKit's own
493
+ /// sheet container transform must be included.
494
+ func reportContentOffset() {
495
+ guard phase == .shown || phase == .hidden,
496
+ let container = contentChild?.superview,
497
+ let containerWindow = container.window
498
+ else { return }
499
+ var offset = container.convert(CGPoint.zero, to: nil)
500
+ // Overlay level windows and the app window normally share the screen origin.
501
+ if let referenceWindow = window, referenceWindow !== containerWindow {
502
+ offset.x += containerWindow.frame.origin.x - referenceWindow.frame.origin.x
503
+ offset.y += containerWindow.frame.origin.y - referenceWindow.frame.origin.y
504
+ }
505
+ if let reported = reportedContentOffset,
506
+ abs(reported.x - offset.x) < 0.5, abs(reported.y - offset.y) < 0.5 {
507
+ return
508
+ }
509
+ reportedContentOffset = offset
510
+ onContentOffsetChanged?(offset)
511
+ }
512
+
513
+ private func stopAnimators() {
514
+ // Freeze at the current presentation values so the next run retargets
515
+ // from where the previous one left off.
516
+ [animator, backdropAnimator].forEach { running in
517
+ guard let running, running.state == .active else { return }
518
+ running.stopAnimation(false)
519
+ running.finishAnimation(at: .current)
520
+ }
521
+ animator = nil
522
+ backdropAnimator = nil
523
+ }
524
+
525
+ // MARK: - Sheet
526
+
527
+ private func sheetStyle() -> NativeOverlaySheetStyle {
528
+ NativeOverlaySheetStyle(
529
+ height: max(sheetHeight, 1),
530
+ backgroundColor: sheetBackgroundColor ?? .systemBackground,
531
+ cornerRadius: max(sheetCornerRadius, 0),
532
+ showHandle: showHandle,
533
+ backdropColor: backdropColor,
534
+ dismissOnBackdropPress: dismissOnBackdropPress,
535
+ enterMotion: config.enter.motion,
536
+ exitMotion: config.exit.motion
537
+ )
538
+ }
539
+
540
+ private func presentSheet(in host: UIWindow) {
541
+ guard sheetController == nil,
542
+ let presenter = NativeOverlayWindowManager.shared.presenter(in: host) else { return }
543
+ // UIKit refuses to present while another transition runs in this window.
544
+ if presenter.isBeingPresented || presenter.transitionCoordinator != nil {
545
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
546
+ guard let self, self.visible, self.phase == .hidden else { return }
547
+ self.present()
548
+ }
549
+ return
550
+ }
551
+ let controller = NativeOverlaySheetController(host: self, style: sheetStyle())
552
+ sheetController = controller
553
+ phase = .entering
554
+ presenter.present(controller, animated: true) { [weak self, weak controller] in
555
+ guard let self, let controller, self.sheetController === controller else { return }
556
+ if self.phase == .entering {
557
+ self.phase = .shown
558
+ self.didPresent()
559
+ }
560
+ if !self.visible {
561
+ self.dismissSheet()
562
+ }
563
+ }
564
+ }
565
+
566
+ private func dismissSheet() {
567
+ guard let controller = sheetController else {
568
+ finishDismiss(reason: "programmatic")
569
+ return
570
+ }
571
+ // Closing mid-presentation is handled by the present completion.
572
+ guard !controller.isBeingPresented, phase != .exiting else { return }
573
+ phase = .exiting
574
+ controller.dismissRequestedByHost = true
575
+ controller.dismiss(animated: true) { [weak self, weak controller] in
576
+ guard let self, self.sheetController === controller else { return }
577
+ self.finishDismiss(reason: "programmatic")
578
+ }
579
+ }
580
+
581
+ private func teardownSheet() {
582
+ guard let controller = sheetController else { return }
583
+ if let touchHandler, controller.isViewLoaded, touchHandler.view === controller.view {
584
+ touchHandler.perform(NSSelectorFromString("detachFromView:"), with: controller.view)
585
+ }
586
+ stageContent()
587
+ if controller.presentingViewController != nil, !controller.isBeingDismissed {
588
+ controller.dismissRequestedByHost = true
589
+ controller.dismiss(animated: false)
590
+ }
591
+ sheetController = nil
592
+ }
593
+
594
+ // MARK: - Entry view
595
+
596
+ private func makeEntryView(in host: UIView) -> NativeOverlayEntryView {
597
+ let entry = NativeOverlayEntryView(frame: host.bounds)
598
+ entry.autoresizingMask = [.flexibleWidth, .flexibleHeight]
599
+ entry.onEscape = { [weak self] in
600
+ self?.onRequestDismiss?(["reason": "back"])
601
+ }
602
+ entry.onBackdropTap = { [weak self] in
603
+ guard let self, self.dismissOnBackdropPress, self.phase != .exiting else { return }
604
+ self.onRequestDismiss?(["reason": "backdrop"])
605
+ }
606
+ entry.onLayout = { [weak self] in
607
+ self?.reportContentOffset()
608
+ }
609
+ entry.onPanDismiss = { [weak self] in
610
+ guard let self, self.phase == .shown || self.phase == .entering else { return }
611
+ self.finishDismiss(reason: nil)
612
+ self.onRequestDismiss?(["reason": "pan"])
613
+ }
614
+ // Page hosts live inside the React surface, whose touch handler already
615
+ // reaches the moved content; a second handler would double-dispatch.
616
+ if !isPageScope, let touchHandler, touchHandler.view == nil {
617
+ touchHandler.perform(NSSelectorFromString("attachToView:"), with: entry)
618
+ }
619
+ entryView = entry
620
+ return entry
621
+ }
622
+
623
+ private func applyEntryConfiguration(_ entry: NativeOverlayEntryView) {
624
+ entry.stackOrder = stackOrder
625
+ entry.blocking = blocking
626
+ entry.dismissOnBackPress = dismissOnBackPress
627
+ entry.accessibilityViewIsModal = blocking
628
+ entry.backdropView.backgroundColor = backdropColor ?? .clear
629
+ if isPageScope && isSheet {
630
+ entry.configurePageSheet(
631
+ height: max(sheetHeight, 1),
632
+ backgroundColor: sheetBackgroundColor ?? .systemBackground,
633
+ cornerRadius: max(sheetCornerRadius, 0),
634
+ draggable: dismissOnPanDown
635
+ )
636
+ }
637
+ }
638
+
639
+ /// Keeps entries of one level ordered by `stackOrder`.
640
+ private func insert(_ entry: NativeOverlayEntryView, into host: UIView) {
641
+ if entry.superview === host { return }
642
+ entry.frame = host.bounds
643
+ let above = host.subviews
644
+ .compactMap { $0 as? NativeOverlayEntryView }
645
+ .first { $0.stackOrder > entry.stackOrder }
646
+ if let above {
647
+ host.insertSubview(entry, belowSubview: above)
648
+ } else {
649
+ host.addSubview(entry)
650
+ }
651
+ }
652
+ }
653
+
654
+ extension NativeOverlayContainerView: NativeOverlaySheetHost {
655
+ func sheetDidLayout(_ controller: NativeOverlaySheetController) {
656
+ guard sheetController === controller else { return }
657
+ reportContentOffset()
658
+ }
659
+
660
+ func sheetDidLoad(_ controller: NativeOverlaySheetController) {
661
+ if let touchHandler, touchHandler.view == nil {
662
+ touchHandler.perform(NSSelectorFromString("attachToView:"), with: controller.view)
663
+ }
664
+ if let child = contentChild {
665
+ child.removeFromSuperview()
666
+ controller.view.addSubview(child)
667
+ }
668
+ }
669
+
670
+ func sheetDidDismissInteractively(_ controller: NativeOverlaySheetController) {
671
+ guard sheetController === controller else { return }
672
+ // Already off screen: tear down, then let JS close the entry; the next
673
+ // visible=false commit reports onDismissed.
674
+ finishDismiss(reason: nil)
675
+ onRequestDismiss?(["reason": "pan"])
676
+ }
677
+
678
+ func sheetDidDisappearUnexpectedly(_ controller: NativeOverlaySheetController) {
679
+ guard sheetController === controller else { return }
680
+ finishDismiss(reason: "system")
681
+ }
682
+
683
+ func sheetRequestedDismiss(_ controller: NativeOverlaySheetController, reason: String) {
684
+ guard sheetController === controller, phase != .exiting else { return }
685
+ if reason == "back", !dismissOnBackPress { return }
686
+ onRequestDismiss?(["reason": reason])
687
+ }
688
+ }