react-native-screens 4.10.0-beta.3 → 4.11.0-beta.0

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 (62) hide show
  1. package/RNScreens.podspec +2 -2
  2. package/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt +9 -6
  3. package/android/src/fabric/java/com/swmansion/rnscreens/NativeProxy.kt +2 -1
  4. package/android/src/main/java/com/swmansion/rnscreens/Screen.kt +47 -16
  5. package/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt +3 -0
  6. package/android/src/main/java/com/swmansion/rnscreens/ScreenFragmentWrapper.kt +9 -0
  7. package/android/src/main/java/com/swmansion/rnscreens/ScreenModalFragment.kt +2 -0
  8. package/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +16 -19
  9. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +18 -2
  10. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +6 -1
  11. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.kt +19 -2
  12. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt +1 -0
  13. package/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt +1 -1
  14. package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt +9 -4
  15. package/android/src/versioned/pointerevents/77/com/swmansion/rnscreens/{ScreensCoordinatorLayoutPointerEventsImpl.kt → PointerEventsBoxNoneImpl.kt} +1 -1
  16. package/android/src/versioned/pointerevents/latest/com/swmansion/rnscreens/{ScreensCoordinatorLayoutPointerEventsImpl.kt → PointerEventsBoxNoneImpl.kt} +1 -1
  17. package/common/cpp/react/renderer/components/rnscreens/RNSModalScreenShadowNode.cpp +1 -3
  18. package/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h +7 -3
  19. package/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h +1 -1
  20. package/cpp/RNSScreenRemovalListener.cpp +3 -1
  21. package/ios/RNSConvert.mm +2 -0
  22. package/ios/RNSEnums.h +2 -1
  23. package/ios/RNSScreen.mm +22 -0
  24. package/ios/RNSScreenStack.mm +3 -1
  25. package/ios/RNSScreenStackHeaderConfig.h +1 -1
  26. package/ios/RNSScreenStackHeaderConfig.mm +31 -31
  27. package/lib/commonjs/components/Screen.js +9 -2
  28. package/lib/commonjs/components/Screen.js.map +1 -1
  29. package/lib/commonjs/fabric/ModalScreenNativeComponent.js.map +1 -1
  30. package/lib/commonjs/fabric/ScreenNativeComponent.js.map +1 -1
  31. package/lib/commonjs/gesture-handler/ScreenGestureDetector.js +3 -1
  32. package/lib/commonjs/gesture-handler/ScreenGestureDetector.js.map +1 -1
  33. package/lib/commonjs/native-stack/utils/getDefaultHeaderHeight.js +2 -2
  34. package/lib/commonjs/native-stack/utils/getDefaultHeaderHeight.js.map +1 -1
  35. package/lib/module/components/Screen.js +9 -2
  36. package/lib/module/components/Screen.js.map +1 -1
  37. package/lib/module/fabric/ModalScreenNativeComponent.js.map +1 -1
  38. package/lib/module/fabric/ScreenNativeComponent.js.map +1 -1
  39. package/lib/module/gesture-handler/ScreenGestureDetector.js +3 -1
  40. package/lib/module/gesture-handler/ScreenGestureDetector.js.map +1 -1
  41. package/lib/module/native-stack/utils/getDefaultHeaderHeight.js +2 -2
  42. package/lib/module/native-stack/utils/getDefaultHeaderHeight.js.map +1 -1
  43. package/lib/typescript/components/Screen.d.ts.map +1 -1
  44. package/lib/typescript/fabric/ModalScreenNativeComponent.d.ts +1 -1
  45. package/lib/typescript/fabric/ModalScreenNativeComponent.d.ts.map +1 -1
  46. package/lib/typescript/fabric/ScreenNativeComponent.d.ts +1 -1
  47. package/lib/typescript/fabric/ScreenNativeComponent.d.ts.map +1 -1
  48. package/lib/typescript/gesture-handler/ScreenGestureDetector.d.ts.map +1 -1
  49. package/lib/typescript/native-stack/types.d.ts +12 -5
  50. package/lib/typescript/native-stack/types.d.ts.map +1 -1
  51. package/lib/typescript/native-stack/utils/getDefaultHeaderHeight.d.ts.map +1 -1
  52. package/lib/typescript/types.d.ts +13 -6
  53. package/lib/typescript/types.d.ts.map +1 -1
  54. package/native-stack/README.md +7 -2
  55. package/package.json +1 -1
  56. package/src/components/Screen.tsx +17 -9
  57. package/src/fabric/ModalScreenNativeComponent.ts +1 -0
  58. package/src/fabric/ScreenNativeComponent.ts +1 -0
  59. package/src/gesture-handler/ScreenGestureDetector.tsx +3 -1
  60. package/src/native-stack/types.tsx +12 -5
  61. package/src/native-stack/utils/getDefaultHeaderHeight.tsx +4 -2
  62. package/src/types.tsx +14 -6
package/RNScreens.podspec CHANGED
@@ -3,7 +3,7 @@ require "json"
3
3
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
4
 
5
5
  new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
6
- platform = new_arch_enabled ? "11.0" : "9.0"
6
+ min_supported_ios_version = new_arch_enabled ? "15.1" : "15.1"
7
7
  source_files = new_arch_enabled ? 'ios/**/*.{h,m,mm,cpp}' : ["ios/**/*.{h,m,mm}", "cpp/RNScreensTurboModule.cpp", "cpp/RNScreensTurboModule.h"]
8
8
 
9
9
  Pod::Spec.new do |s|
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
16
16
  s.homepage = "https://github.com/software-mansion/react-native-screens"
17
17
  s.license = "MIT"
18
18
  s.author = { "author" => "author@domain.cn" }
19
- s.platforms = { :ios => platform, :tvos => "11.0", :visionos => "1.0" }
19
+ s.platforms = { :ios => min_supported_ios_version, :tvos => "11.0", :visionos => "1.0" }
20
20
  s.source = { :git => "https://github.com/software-mansion/react-native-screens.git", :tag => "#{s.version}" }
21
21
  s.source_files = source_files
22
22
  s.project_header_files = "cpp/**/*.h" # Don't expose C++ headers publicly to allow importing framework into Swift files
@@ -14,8 +14,9 @@ abstract class FabricEnabledViewGroup(
14
14
  ) : ViewGroup(context) {
15
15
  private var mStateWrapper: StateWrapper? = null
16
16
 
17
- private var lastSetWidth = 0f
18
- private var lastSetHeight = 0f
17
+ private var lastWidth = 0f
18
+ private var lastHeight = 0f
19
+ private var lastHeaderHeight = 0f
19
20
 
20
21
  fun setStateWrapper(wrapper: StateWrapper?) {
21
22
  mStateWrapper = wrapper
@@ -42,14 +43,16 @@ abstract class FabricEnabledViewGroup(
42
43
  // Check incoming state values. If they're already the correct value, return early to prevent
43
44
  // infinite UpdateState/SetState loop.
44
45
  val delta = 0.9f
45
- if (abs(lastSetWidth - realWidth) < delta &&
46
- abs(lastSetHeight - realHeight) < delta
46
+ if (abs(lastWidth - realWidth) < delta &&
47
+ abs(lastHeight - realHeight) < delta &&
48
+ abs(lastHeaderHeight - realHeaderHeight) < delta
47
49
  ) {
48
50
  return
49
51
  }
50
52
 
51
- lastSetWidth = realWidth
52
- lastSetHeight = realHeight
53
+ lastWidth = realWidth
54
+ lastHeight = realHeight
55
+ lastHeaderHeight = realHeaderHeight
53
56
  val map: WritableMap =
54
57
  WritableNativeMap().apply {
55
58
  putDouble("frameWidth", realWidth.toDouble())
@@ -41,7 +41,8 @@ class NativeProxy {
41
41
  }
42
42
  }
43
43
 
44
- // Called from native
44
+ // Called from native. Currently this method is called from MountingCoordinator thread,
45
+ // which usually is not UI thread.
45
46
  @DoNotStrip
46
47
  public fun notifyScreenRemoved(screenTag: Int) {
47
48
  // Since RN 0.78 the screenTag we receive as argument here might not belong to a screen
@@ -27,6 +27,7 @@ import com.google.android.material.shape.CornerFamily
27
27
  import com.google.android.material.shape.MaterialShapeDrawable
28
28
  import com.google.android.material.shape.ShapeAppearanceModel
29
29
  import com.swmansion.rnscreens.bottomsheet.isSheetFitToContents
30
+ import com.swmansion.rnscreens.bottomsheet.useSingleDetent
30
31
  import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation
31
32
  import com.swmansion.rnscreens.events.HeaderHeightChangeEvent
32
33
  import com.swmansion.rnscreens.events.SheetDetentChangedEvent
@@ -103,6 +104,9 @@ class Screen(
103
104
  field = value
104
105
  }
105
106
 
107
+ private val isNativeStackScreen: Boolean
108
+ get() = container is ScreenStack
109
+
106
110
  init {
107
111
  // we set layout params as WindowManager.LayoutParams to workaround the issue with TextInputs
108
112
  // not displaying modal menus (e.g., copy/paste or selection). The missing menus are due to the
@@ -132,11 +136,7 @@ class Screen(
132
136
  val height = bottom - top
133
137
 
134
138
  if (isSheetFitToContents()) {
135
- sheetBehavior?.let {
136
- if (it.maxHeight != height) {
137
- it.maxHeight = height
138
- }
139
- }
139
+ sheetBehavior?.useSingleDetent(height)
140
140
 
141
141
  if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
142
142
  // On old architecture we delay enter transition in order to wait for initial frame.
@@ -173,23 +173,31 @@ class Screen(
173
173
  r: Int,
174
174
  b: Int,
175
175
  ) {
176
- if (container is ScreenStack && changed) {
176
+ // In case of form sheet we get layout notification a bit later, in `onBottomSheetBehaviorDidLayout`
177
+ // after the attached behaviour laid out this view.
178
+ if (changed && isNativeStackScreen && !usesFormSheetPresentation()) {
177
179
  val width = r - l
178
180
  val height = b - t
179
181
 
180
- if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
181
- updateScreenSizeFabric(width, height, t)
182
- } else {
183
- updateScreenSizePaper(width, height)
184
- }
182
+ dispatchShadowStateUpdate(width, height, t)
185
183
 
186
- footer?.onParentLayout(changed, l, t, r, b, container!!.height)
184
+ // FormSheet has no header in current model.
187
185
  notifyHeaderHeightChange(t)
186
+ }
187
+ }
188
188
 
189
- if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
190
- maybeTriggerPostponedTransition()
191
- }
189
+ internal fun onBottomSheetBehaviorDidLayout(coordinatorLayoutDidChange: Boolean) {
190
+ if (!usesFormSheetPresentation()) {
191
+ return
192
192
  }
193
+ if (coordinatorLayoutDidChange && isNativeStackScreen) {
194
+ dispatchShadowStateUpdate(width, height, top)
195
+ }
196
+ if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
197
+ maybeTriggerPostponedTransition()
198
+ }
199
+
200
+ footer?.onParentLayout(coordinatorLayoutDidChange, left, top, right, bottom, container!!.height)
193
201
  }
194
202
 
195
203
  private fun maybeTriggerPostponedTransition() {
@@ -214,6 +222,21 @@ class Screen(
214
222
  )
215
223
  }
216
224
 
225
+ /**
226
+ * @param offsetY ignored on old architecture
227
+ */
228
+ private fun dispatchShadowStateUpdate(
229
+ width: Int,
230
+ height: Int,
231
+ offsetY: Int,
232
+ ) {
233
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
234
+ updateScreenSizeFabric(width, height, offsetY)
235
+ } else {
236
+ updateScreenSizePaper(width, height)
237
+ }
238
+ }
239
+
217
240
  val headerConfig: ScreenStackHeaderConfig?
218
241
  get() = children.find { it is ScreenStackHeaderConfig } as? ScreenStackHeaderConfig
219
242
 
@@ -237,7 +260,10 @@ class Screen(
237
260
  )
238
261
  }
239
262
 
240
- fun isTransparent(): Boolean =
263
+ /**
264
+ * Whether this screen allows to see the content underneath it.
265
+ */
266
+ fun isTranslucent(): Boolean =
241
267
  when (stackPresentation) {
242
268
  StackPresentation.TRANSPARENT_MODAL,
243
269
  StackPresentation.FORM_SHEET,
@@ -487,6 +513,11 @@ class Screen(
487
513
  isStable: Boolean,
488
514
  ) {
489
515
  dispatchSheetDetentChanged(detentIndex, isStable)
516
+ // There is no need to update shadow state for transient sheet states -
517
+ // we are unsure of the exact sheet position anyway.
518
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED && isStable) {
519
+ updateScreenSizeFabric(width, height, top)
520
+ }
490
521
  }
491
522
 
492
523
  private fun dispatchSheetDetentChanged(
@@ -128,6 +128,9 @@ open class ScreenFragment :
128
128
  ScreenWindowTraits.trySetWindowTraits(screen, activity, tryGetContext())
129
129
  }
130
130
 
131
+ // Plain ScreenFragments can not be translucent
132
+ override fun isTranslucent() = false
133
+
131
134
  override fun tryGetActivity(): Activity? {
132
135
  activity?.let { return it }
133
136
  val context = screen.context
@@ -26,6 +26,15 @@ interface ScreenFragmentWrapper :
26
26
 
27
27
  fun onViewAnimationEnd()
28
28
 
29
+ // Fragment information
30
+
31
+ /**
32
+ * Whether this screen fragment makes it possible to see content underneath it
33
+ * (not fully opaque or does not fill full screen).
34
+ */
35
+ fun isTranslucent(): Boolean
36
+
37
+
29
38
  // Helpers
30
39
  fun tryGetActivity(): Activity?
31
40
 
@@ -92,6 +92,8 @@ class ScreenModalFragment :
92
92
  savedInstanceState: Bundle?,
93
93
  ): View? = null
94
94
 
95
+ override fun isTranslucent(): Boolean = true
96
+
95
97
  override fun dismissFromContainer() {
96
98
  check(container is ScreenStack)
97
99
  val container = container as ScreenStack
@@ -175,21 +175,18 @@ class ScreenStack(
175
175
  childDrawingOrderStrategy = null
176
176
 
177
177
  // Determine new first & last visible screens.
178
- // Scope function to limit the scope of locals.
179
- run {
180
- val notDismissedWrappers =
181
- screenWrappers
182
- .asReversed()
183
- .asSequence()
184
- .filter { !dismissedWrappers.contains(it) && it.screen.activityState !== Screen.ActivityState.INACTIVE }
185
-
186
- newTop = notDismissedWrappers.firstOrNull()
187
- visibleBottom =
188
- notDismissedWrappers
189
- .dropWhile { it.screen.isTransparent() }
190
- .firstOrNull()
191
- ?.takeUnless { it === newTop }
192
- }
178
+ val notDismissedWrappers =
179
+ screenWrappers
180
+ .asReversed()
181
+ .asSequence()
182
+ .filter { !dismissedWrappers.contains(it) && it.screen.activityState !== Screen.ActivityState.INACTIVE }
183
+
184
+ newTop = notDismissedWrappers.firstOrNull()
185
+ visibleBottom =
186
+ notDismissedWrappers
187
+ .dropWhile { it.isTranslucent() }
188
+ .firstOrNull()
189
+ ?.takeUnless { it === newTop }
193
190
 
194
191
  var shouldUseOpenAnimation = true
195
192
  var stackAnimation: StackAnimation? = null
@@ -241,8 +238,8 @@ class ScreenStack(
241
238
  childDrawingOrderStrategy = SwapLastTwo()
242
239
  } else if (newTop != null &&
243
240
  newTopAlreadyInStack &&
244
- topScreenWrapper?.screen?.isTransparent() == true &&
245
- newTop.screen.isTransparent() == false
241
+ topScreenWrapper?.isTranslucent() == true &&
242
+ newTop.isTranslucent() == false
246
243
  ) {
247
244
  // In case where we dismiss multiple transparent views we want to ensure
248
245
  // that they are drawn in correct order - Android swaps them by default,
@@ -253,7 +250,7 @@ class ScreenStack(
253
250
  .asSequence()
254
251
  .takeWhile {
255
252
  it !== newTop &&
256
- it.screen.isTransparent()
253
+ it.isTranslucent()
257
254
  }.count()
258
255
  if (dismissedTransparentScreenApproxCount > 1) {
259
256
  childDrawingOrderStrategy =
@@ -314,7 +311,7 @@ class ScreenStack(
314
311
  private fun turnOffA11yUnderTransparentScreen(visibleBottom: ScreenFragmentWrapper?) {
315
312
  if (screenWrappers.size > 1 && visibleBottom != null) {
316
313
  topScreenWrapper?.let {
317
- if (it.screen.isTransparent()) {
314
+ if (it.isTranslucent()) {
318
315
  val screenFragmentsBeneathTop = screenWrappers.slice(0 until screenWrappers.size - 1).asReversed()
319
316
  // go from the top of the stack excluding the top screen
320
317
  for (fragmentWrapper in screenFragmentsBeneathTop) {
@@ -86,6 +86,8 @@ class ScreenStackFragment :
86
86
  )
87
87
  }
88
88
 
89
+ override fun isTranslucent(): Boolean = screen.isTranslucent()
90
+
89
91
  override fun removeToolbar() {
90
92
  appBarLayout?.let {
91
93
  toolbar?.let { toolbar ->
@@ -380,7 +382,7 @@ class ScreenStackFragment :
380
382
  // If the screen is a transparent modal with hidden header we don't want to update the toolbar
381
383
  // menu because it may erase the menu of the previous screen (which is still visible in these
382
384
  // circumstances). See here: https://github.com/software-mansion/react-native-screens/issues/2271
383
- if (!screen.isTransparent() || screen.headerConfig?.isHeaderHidden == false) {
385
+ if (!screen.isTranslucent() || screen.headerConfig?.isHeaderHidden == false) {
384
386
  updateToolbarMenu(menu)
385
387
  }
386
388
  return super.onPrepareOptionsMenu(menu)
@@ -480,7 +482,7 @@ class ScreenStackFragment :
480
482
  constructor(context: Context, fragment: ScreenStackFragment) : this(
481
483
  context,
482
484
  fragment,
483
- ScreensCoordinatorLayoutPointerEventsImpl(),
485
+ PointerEventsBoxNoneImpl(),
484
486
  )
485
487
 
486
488
  override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets = super.onApplyWindowInsets(insets)
@@ -543,6 +545,20 @@ class ScreenStackFragment :
543
545
  }
544
546
  }
545
547
 
548
+ override fun onLayout(
549
+ changed: Boolean,
550
+ l: Int,
551
+ t: Int,
552
+ r: Int,
553
+ b: Int,
554
+ ) {
555
+ super.onLayout(changed, l, t, r, b)
556
+
557
+ if (fragment.screen.usesFormSheetPresentation()) {
558
+ fragment.screen.onBottomSheetBehaviorDidLayout(changed)
559
+ }
560
+ }
561
+
546
562
  // override fun reactTagForTouch(touchX: Float, touchY: Float): Int {
547
563
  // throw IllegalStateException("Screen wrapper should never be asked for the view tag")
548
564
  // }
@@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment
15
15
  import com.facebook.react.ReactApplication
16
16
  import com.facebook.react.bridge.JSApplicationIllegalArgumentException
17
17
  import com.facebook.react.bridge.ReactContext
18
+ import com.facebook.react.uimanager.ReactPointerEventsView
18
19
  import com.facebook.react.uimanager.UIManagerHelper
19
20
  import com.facebook.react.views.text.ReactTypefaceUtils
20
21
  import com.swmansion.rnscreens.events.HeaderAttachedEvent
@@ -23,7 +24,11 @@ import kotlin.math.max
23
24
 
24
25
  class ScreenStackHeaderConfig(
25
26
  context: Context,
26
- ) : FabricEnabledHeaderConfigViewGroup(context) {
27
+ private val pointerEventsImpl: ReactPointerEventsView
28
+ ) : FabricEnabledHeaderConfigViewGroup(context), ReactPointerEventsView by pointerEventsImpl {
29
+
30
+ constructor(context: Context): this(context, pointerEventsImpl = PointerEventsBoxNoneImpl())
31
+
27
32
  private val configSubviews = ArrayList<ScreenStackHeaderSubview>(3)
28
33
  val toolbar: CustomToolbar
29
34
  var isHeaderHidden = false // named this way to avoid conflict with platform's isHidden
@@ -10,6 +10,14 @@ class ScreenStackHeaderSubview(
10
10
  ) : FabricEnabledHeaderSubviewViewGroup(context) {
11
11
  private var reactWidth = 0
12
12
  private var reactHeight = 0
13
+
14
+ /**
15
+ * Semantics: true iff we **believe** that SurfaceMountingManager has measured this view during mount item
16
+ * execution. We recognize this case by checking measure mode in `onMeasure`. If Androidx
17
+ * happens to use `EXACTLY` for both dimensions this property might convey invalid information.
18
+ */
19
+ private var isReactSizeSet = false
20
+
13
21
  var type = Type.RIGHT
14
22
 
15
23
  val config: ScreenStackHeaderConfig?
@@ -22,9 +30,10 @@ class ScreenStackHeaderSubview(
22
30
  if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY &&
23
31
  MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY
24
32
  ) {
25
- // dimensions provided by react
33
+ // dimensions provided by react (with high probability)
26
34
  reactWidth = MeasureSpec.getSize(widthMeasureSpec)
27
35
  reactHeight = MeasureSpec.getSize(heightMeasureSpec)
36
+ isReactSizeSet = true
28
37
  val parent = parent
29
38
  if (parent != null) {
30
39
  forceLayout()
@@ -44,7 +53,15 @@ class ScreenStackHeaderSubview(
44
53
  if (changed && BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
45
54
  val width = r - l
46
55
  val height = b - t
47
- updateSubviewFrameState(width, height, l, t)
56
+
57
+ // When setting subviews via `setOptions` from `useEffect` hook in a component, the first
58
+ // frame received might be computed by native layout & completely invalid (zero height).
59
+ // RN layout is the source of subview **size** (not origin) & we need to avoid sending
60
+ // this native size to ST. Doing otherwise might lead to problems.
61
+ // See: https://github.com/software-mansion/react-native-screens/pull/2812
62
+ if (isReactSizeSet) {
63
+ updateSubviewFrameState(width, height, l, t)
64
+ }
48
65
  }
49
66
  }
50
67
 
@@ -61,6 +61,7 @@ class ScreenStackViewManager :
61
61
  index: Int,
62
62
  ): View = parent.getScreenAt(index)
63
63
 
64
+ // Old architecture only.
64
65
  override fun createShadowNodeInstance(context: ReactApplicationContext): LayoutShadowNode = ScreensShadowNode(context)
65
66
 
66
67
  override fun needsCustomLayoutForChildren() = true
@@ -126,7 +126,7 @@ open class ScreenViewManager :
126
126
  when (presentation) {
127
127
  "push" -> Screen.StackPresentation.PUSH
128
128
  "formSheet" -> Screen.StackPresentation.FORM_SHEET
129
- "modal", "containedModal", "fullScreenModal" ->
129
+ "modal", "containedModal", "fullScreenModal", "pageSheet" ->
130
130
  Screen.StackPresentation.MODAL
131
131
  "transparentModal", "containedTransparentModal" ->
132
132
  Screen.StackPresentation.TRANSPARENT_MODAL
@@ -130,10 +130,15 @@ class SheetDelegate(
130
130
  behavior.apply {
131
131
  val height =
132
132
  if (screen.isSheetFitToContents()) {
133
- screen.contentWrapper
134
- .get()
135
- ?.height
136
- .takeIf { screen.contentWrapper.get()?.isLaidOut == true }
133
+ screen.contentWrapper.get()?.let { contentWrapper ->
134
+ contentWrapper.height.takeIf {
135
+ // subtree might not be laid out, e.g. after fragment reattachment
136
+ // and view recreation, however since it is retained by
137
+ // react-native it has its height cached. We want to use it.
138
+ // Otherwise we would have to trigger RN layout manually.
139
+ contentWrapper.isLaidOut || contentWrapper.height > 0
140
+ }
141
+ }
137
142
  } else {
138
143
  (screen.sheetDetents.first() * containerHeight).toInt()
139
144
  }
@@ -3,7 +3,7 @@ package com.swmansion.rnscreens
3
3
  import com.facebook.react.uimanager.PointerEvents
4
4
  import com.facebook.react.uimanager.ReactPointerEventsView
5
5
 
6
- internal class ScreensCoordinatorLayoutPointerEventsImpl : ReactPointerEventsView {
6
+ internal class PointerEventsBoxNoneImpl() : ReactPointerEventsView {
7
7
  // We set pointer events to BOX_NONE, because we don't want the ScreensCoordinatorLayout
8
8
  // to be target of react gestures and effectively prevent interaction with screens
9
9
  // underneath the current screen (useful in `modal` & `formSheet` presentation).
@@ -3,7 +3,7 @@ package com.swmansion.rnscreens
3
3
  import com.facebook.react.uimanager.PointerEvents
4
4
  import com.facebook.react.uimanager.ReactPointerEventsView
5
5
 
6
- internal class ScreensCoordinatorLayoutPointerEventsImpl() : ReactPointerEventsView {
6
+ internal class PointerEventsBoxNoneImpl() : ReactPointerEventsView {
7
7
  // We set pointer events to BOX_NONE, because we don't want the ScreensCoordinatorLayout
8
8
  // to be target of react gestures and effectively prevent interaction with screens
9
9
  // underneath the current screen (useful in `modal` & `formSheet` presentation).
@@ -7,9 +7,7 @@ extern const char RNSModalScreenComponentName[] = "RNSModalScreen";
7
7
 
8
8
  Point RNSModalScreenShadowNode::getContentOriginOffset(
9
9
  bool /*includeTransform*/) const {
10
- auto stateData = getStateData();
11
- auto contentOffset = stateData.contentOffset;
12
- return {contentOffset.x, contentOffset.y};
10
+ return getStateData().contentOffset;
13
11
  }
14
12
 
15
13
  } // namespace react
@@ -34,17 +34,21 @@ class RNSScreenStackHeaderConfigComponentDescriptor final
34
34
  shadowNode.getState());
35
35
  auto stateData = state->getData();
36
36
 
37
- if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
38
- layoutableShadowNode.setSize(stateData.frameSize);
39
37
  #ifdef ANDROID
38
+ if (stateData.frameSize.width != 0) {
39
+ layoutableShadowNode.setSize({stateData.frameSize.width, YGUndefined});
40
40
  layoutableShadowNode.setPadding({
41
41
  stateData.paddingStart,
42
42
  0,
43
43
  stateData.paddingEnd,
44
44
  0,
45
45
  });
46
- #endif // ANDROID
47
46
  }
47
+ #else
48
+ if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
49
+ layoutableShadowNode.setSize(stateData.frameSize);
50
+ }
51
+ #endif // ANDROID
48
52
 
49
53
  ConcreteComponentDescriptor::adopt(shadowNode);
50
54
  #if !defined(ANDROID) && !defined(NDEBUG)
@@ -32,7 +32,7 @@ class RNSScreenStackHeaderSubviewComponentDescriptor final
32
32
 
33
33
  auto state = std::static_pointer_cast<
34
34
  const RNSScreenStackHeaderSubviewShadowNode::ConcreteState>(
35
- shadowNode.getMostRecentState());
35
+ shadowNode.getState());
36
36
  auto stateData = state->getData();
37
37
 
38
38
  if (!isSizeEmpty(stateData.frameSize)) {
@@ -8,9 +8,11 @@ std::optional<MountingTransaction> RNSScreenRemovalListener::pullTransaction(
8
8
  const TransactionTelemetry &telemetry,
9
9
  ShadowViewMutationList mutations) const {
10
10
  for (const ShadowViewMutation &mutation : mutations) {
11
+ // When using RNSModalScreen on Android it should be added here.
11
12
  if (mutation.type == ShadowViewMutation::Type::Remove &&
12
13
  mutation.oldChildShadowView.componentName != nullptr &&
13
- strcmp(mutation.oldChildShadowView.componentName, "RNSScreen") == 0) {
14
+ std::strcmp(mutation.oldChildShadowView.componentName, "RNSScreen") ==
15
+ 0) {
14
16
  // We call the listener function even if this screen has not been owned
15
17
  // by RNSScreenStack as since RN 0.78 we do not have enough information
16
18
  // here. This final filter is applied later in NativeProxy.
package/ios/RNSConvert.mm CHANGED
@@ -49,6 +49,8 @@
49
49
  return RNSScreenStackPresentationFullScreenModal;
50
50
  case FormSheet:
51
51
  return RNSScreenStackPresentationFormSheet;
52
+ case PageSheet:
53
+ return RNSScreenStackPresentationPageSheet;
52
54
  case ContainedModal:
53
55
  return RNSScreenStackPresentationContainedModal;
54
56
  case TransparentModal:
package/ios/RNSEnums.h CHANGED
@@ -5,7 +5,8 @@ typedef NS_ENUM(NSInteger, RNSScreenStackPresentation) {
5
5
  RNSScreenStackPresentationContainedModal,
6
6
  RNSScreenStackPresentationContainedTransparentModal,
7
7
  RNSScreenStackPresentationFullScreenModal,
8
- RNSScreenStackPresentationFormSheet
8
+ RNSScreenStackPresentationFormSheet,
9
+ RNSScreenStackPresentationPageSheet,
9
10
  };
10
11
 
11
12
  typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
package/ios/RNSScreen.mm CHANGED
@@ -246,8 +246,29 @@ RNS_IGNORE_SUPER_CALL_END
246
246
  }
247
247
  #else
248
248
  _controller.modalPresentationStyle = UIModalPresentationFullScreen;
249
+ #endif
250
+ #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_17_0) && \
251
+ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_17_0 && !TARGET_OS_TV
252
+ if (@available(iOS 18.0, *)) {
253
+ UISheetPresentationController *sheetController = _controller.sheetPresentationController;
254
+ if (sheetController != nil) {
255
+ sheetController.prefersPageSizing = true;
256
+ } else {
257
+ RCTLogError(
258
+ @"[RNScreens] sheetPresentationController is null when attempting to set prefersPageSizing for modal");
259
+ }
260
+ }
261
+ #endif
262
+ break;
263
+
264
+ case RNSScreenStackPresentationPageSheet:
265
+ #if !TARGET_OS_TV
266
+ _controller.modalPresentationStyle = UIModalPresentationPageSheet;
267
+ #else
268
+ _controller.modalPresentationStyle = UIModalPresentationFullScreen;
249
269
  #endif
250
270
  break;
271
+
251
272
  case RNSScreenStackPresentationFullScreenModal:
252
273
  _controller.modalPresentationStyle = UIModalPresentationFullScreen;
253
274
  break;
@@ -2024,6 +2045,7 @@ RCT_ENUM_CONVERTER(
2024
2045
  @"modal" : @(RNSScreenStackPresentationModal),
2025
2046
  @"fullScreenModal" : @(RNSScreenStackPresentationFullScreenModal),
2026
2047
  @"formSheet" : @(RNSScreenStackPresentationFormSheet),
2048
+ @"pageSheet" : @(RNSScreenStackPresentationPageSheet),
2027
2049
  @"containedModal" : @(RNSScreenStackPresentationContainedModal),
2028
2050
  @"transparentModal" : @(RNSScreenStackPresentationTransparentModal),
2029
2051
  @"containedTransparentModal" : @(RNSScreenStackPresentationContainedTransparentModal)
@@ -78,11 +78,13 @@ namespace react = facebook::react;
78
78
  BOOL isNotDismissingModal = screenController.presentedViewController == nil ||
79
79
  (screenController.presentedViewController != nil &&
80
80
  ![screenController.presentedViewController isBeingDismissed]);
81
+ BOOL isPresentingSearchController =
82
+ [screenController.presentedViewController isKindOfClass:UISearchController.class];
81
83
 
82
84
  // Calculate header height during simple transition from one screen to another.
83
85
  // If RNSScreen includes a navigation controller of type RNSNavigationController, it should not calculate
84
86
  // header height, as it could have nested stack.
85
- if (![screenController hasNestedStack] && isNotDismissingModal) {
87
+ if (![screenController hasNestedStack] && (isPresentingSearchController || isNotDismissingModal)) {
86
88
  [screenController calculateAndNotifyHeaderHeightChangeIsModal:NO];
87
89
  }
88
90
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  @interface NSString (RNSStringUtil)
14
14
 
15
- + (BOOL)RNSisBlank:(nullable NSString *)string;
15
+ + (BOOL)rnscreens_isBlankOrNull:(nullable NSString *)string;
16
16
 
17
17
  @end
18
18