expo-modules-core 57.0.18 → 57.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.19 — 2026-09-24
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Fixed SwiftUI view mounting blocking the main thread: `ViewProps` and `ShadowNodeProxy` now store their `objectWillChange` publisher instead of resolving it through Combine's global observable-object table on every access. ([#49528](https://github.com/expo/expo/pull/49528) by [@wielski](https://github.com/wielski))
18
+ - Fixed a crash while laying out an `RNHostView` with `matchContents` whose content is laid out more than once, for example a nested host or a Reanimated node. The hosted shadow node could be released before `replaceChild` finished reading it. ([#50456](https://github.com/expo/expo/pull/50456) by [@expo-bot](https://github.com/expo-bot))
19
+ - [Android] Fixed worklet callbacks crashing with `NoSuchFieldError` in minified release builds. ([#50502](https://github.com/expo/expo/issues/50502) by [@dhalenok](https://github.com/dhalenok), [#50510](https://github.com/expo/expo/pull/50510) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
20
+ - [Android] Replaced the hosting `ComposeView`'s view id with a tag for its `rememberSaveable` namespace, fixing a crash on `react-native-screens` tab switch and touches inside a `Host` reaching the wrong React Native view. ([#49964](https://github.com/expo/expo/issues/49964) by [@b2x-software](https://github.com/b2x-software), [#50299](https://github.com/expo/expo/pull/50299) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
21
+ - [iOS] Fixed the tap that closes a SwiftUI menu still reaching the React Native view underneath it in apps with no accessibility client attached. The menu gate no longer requires `accessibilityViewIsModal`, which UIKit only implements for the menu container while its accessibility bundle is loaded. ([#48765](https://github.com/expo/expo/issues/48765) by [@ayyoub-coder](https://github.com/ayyoub-coder), [#49916](https://github.com/expo/expo/pull/49916) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
22
+
13
23
  ## 57.0.18 — 2026-09-11
14
24
 
15
25
  ### 🐛 Bug fixes
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '57.0.18'
30
+ version = '57.0.19'
31
31
 
32
32
  def isExpoModulesCoreTests = {
33
33
  Gradle gradle = getGradle()
@@ -94,7 +94,7 @@ android {
94
94
  defaultConfig {
95
95
  consumerProguardFiles 'proguard-rules.pro'
96
96
  versionCode 1
97
- versionName "57.0.18"
97
+ versionName "57.0.19"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -221,10 +221,10 @@ abstract class ExpoComposeView<T : ComposeProps>(
221
221
 
222
222
  private fun addComposeView() {
223
223
  val composeView = ComposeView(context).also {
224
- // Give each Host a unique id so its rememberSaveable state gets its own key.
225
- // All Hosts share the Activity's SavedStateRegistry (set below), so without an id
226
- // they'd collide on one key and only the first could save/restore state.
227
- it.id = generateViewId()
224
+ // Give each Host a unique rememberSaveable namespace. All Hosts share the Activity's
225
+ // SavedStateRegistry (set below), so without one they'd collide on a single key and only
226
+ // the first could save/restore state.
227
+ HostingViewSaveableState.assignNamespace(it)
228
228
  it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
229
229
  // Pin the composition to the Activity lifecycle so it survives
230
230
  // react-native-screens detaching inactive screens on every switch.
@@ -0,0 +1,23 @@
1
+ package expo.modules.kotlin.views
2
+
3
+ import android.view.View
4
+ import androidx.compose.ui.R as ComposeUiR
5
+ import java.util.concurrent.atomic.AtomicInteger
6
+
7
+ /**
8
+ * Gives each Compose hosting view a unique `rememberSaveable` namespace.
9
+ *
10
+ * Previous solutions tried to give view an id but it caused some other issues
11
+ * See https://github.com/expo/expo/issues/49964 and
12
+ * https://github.com/expo/expo/issues/49964#issuecomment-5714493768.
13
+ *
14
+ * So using a tag instead of an id is a better solution.
15
+ * Compose reads this tag before falling back to `View.id`, so we don't call `generateViewId()`.
16
+ */
17
+ internal object HostingViewSaveableState {
18
+ private val nextNamespace = AtomicInteger()
19
+
20
+ fun assignNamespace(view: View) {
21
+ view.setTag(ComposeUiR.id.compose_view_saveable_id_tag, "Host:${nextNamespace.getAndIncrement()}")
22
+ }
23
+ }
@@ -1,10 +1,12 @@
1
1
  package expo.modules.kotlin.jni.worklets
2
2
 
3
3
  import com.facebook.jni.HybridData
4
+ import expo.modules.core.interfaces.DoNotStrip
4
5
 
5
6
  class WorkletNativeRuntime(
6
7
  jsRuntimePointer: Long
7
8
  ) {
9
+ @DoNotStrip
8
10
  private val mHybridData = initHybrid(jsRuntimePointer)
9
11
 
10
12
  external fun initHybrid(jsRuntimePointer: Long): HybridData
@@ -10,6 +10,7 @@
10
10
  #include <react/renderer/core/LayoutableShadowNode.h>
11
11
 
12
12
  #include <algorithm>
13
+ #include <memory>
13
14
 
14
15
  #include "ContentOriginRegistry.h"
15
16
  #include "ExpoViewEventEmitter.h"
@@ -104,7 +105,7 @@ public:
104
105
  return ConcreteViewShadowNode::measureContent(layoutContext, layoutConstraints);
105
106
  }
106
107
 
107
- auto const *content = hostedContent();
108
+ auto const content = hostedContent();
108
109
 
109
110
  if (content == nullptr) {
110
111
  return {};
@@ -122,7 +123,7 @@ public:
122
123
  return;
123
124
  }
124
125
 
125
- auto const *content = hostedContent();
126
+ auto const content = hostedContent();
126
127
 
127
128
  if (content == nullptr) {
128
129
  return;
@@ -144,12 +145,15 @@ public:
144
145
  }
145
146
 
146
147
  private:
147
- const react::LayoutableShadowNode *hostedContent() const {
148
+ // Owning on purpose: `layout()` calls `replaceChild()`, which overwrites this node's only strong
149
+ // reference to the content — `RNHostView` is a leaf Yoga node, so Yoga keeps no second one, and
150
+ // `YogaLayoutableShadowNode::replaceChild()` reads `oldChild` right after.
151
+ std::shared_ptr<const react::LayoutableShadowNode> hostedContent() const {
148
152
  auto const &children = this->getChildren();
149
153
 
150
154
  return children.empty()
151
155
  ? nullptr
152
- : dynamic_cast<const react::LayoutableShadowNode *>(children.front().get());
156
+ : std::dynamic_pointer_cast<const react::LayoutableShadowNode>(children.front());
153
157
  }
154
158
 
155
159
  react::LayoutDirection resolvedLayoutDirection() const {
@@ -1,7 +1,12 @@
1
1
  // Copyright 2025-present 650 Industries. All rights reserved.
2
2
 
3
+ import Combine
4
+
3
5
  extension ExpoSwiftUI {
4
6
  open class ShadowNodeProxy: ObservableObject, Record, @unchecked Sendable {
7
+ // Stored so `objectWillChange` is a plain property read instead of Combine's O(live objects) side-table lookup.
8
+ public let objectWillChange = ObservableObjectPublisher()
9
+
5
10
  public required init() {}
6
11
 
7
12
  // We use Double.nan to mark a dimension as unset. This value is passed to https://github.com/facebook/yoga/blob/49ee855f99fb67079c24d507a4ea1b6d80fa2ebf/yoga/style/StyleLength.h#L33
@@ -23,6 +23,9 @@ extension ExpoSwiftUI {
23
23
  It's a record that can be observed by SwiftUI to re-render on its changes.
24
24
  */
25
25
  open class ViewProps: ObservableObject, Record {
26
+ // Stored so `objectWillChange` is a plain property read instead of Combine's O(live objects) side-table lookup.
27
+ public let objectWillChange = ObservableObjectPublisher()
28
+
26
29
  public required init() {}
27
30
 
28
31
  public required init(rawProps: [String: Any], context: AppContext) throws {
@@ -31,24 +31,18 @@ internal final class SystemMenuTouchGate: UIGestureRecognizer, UIGestureRecogniz
31
31
  }
32
32
 
33
33
  /**
34
- Consider Menu to be open when the container is present, still accepts interaction, and is modal.
34
+ Consider Menu to be open while the container is present and still accepts interaction.
35
35
  UIKit turns `isUserInteractionEnabled` off the moment dismissal commits, so taps made during
36
- the dismiss animation pass through to the app again. `accessibilityViewIsModal` is how UIKit
37
- marks the container as blocking the content behind it — a semantic signal beside the class name.
36
+ the dismiss animation pass through to the app again.
38
37
  */
39
- static func isOpenContextMenuContainer(
40
- className: String,
41
- isUserInteractionEnabled: Bool,
42
- accessibilityViewIsModal: Bool
43
- ) -> Bool {
44
- return isUserInteractionEnabled && accessibilityViewIsModal && isContextMenuContainerClassName(className)
38
+ static func isOpenContextMenuContainer(className: String, isUserInteractionEnabled: Bool) -> Bool {
39
+ return isUserInteractionEnabled && isContextMenuContainerClassName(className)
45
40
  }
46
41
 
47
42
  static func isContextMenuContainer(_ view: UIView) -> Bool {
48
43
  return isOpenContextMenuContainer(
49
44
  className: NSStringFromClass(type(of: view)),
50
- isUserInteractionEnabled: view.isUserInteractionEnabled,
51
- accessibilityViewIsModal: view.accessibilityViewIsModal
45
+ isUserInteractionEnabled: view.isUserInteractionEnabled
52
46
  )
53
47
  }
54
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "57.0.18",
3
+ "version": "57.0.19",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@expo/expo-modules-macros-plugin": "0.6.1",
50
- "expo-modules-jsi": "~57.1.0",
50
+ "expo-modules-jsi": "~57.1.1",
51
51
  "invariant": "^2.2.4"
52
52
  },
53
53
  "peerDependencies": {
@@ -66,7 +66,7 @@
66
66
  "@types/invariant": "^2.2.33",
67
67
  "expo-module-scripts": "56.0.3"
68
68
  },
69
- "gitHead": "9e5319c0f821a27b7924841903abae50e2b41790",
69
+ "gitHead": "9349d1457178dd9cccb8bdd3aed47473c49134e1",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",