expo-modules-core 57.0.13 → 57.0.15

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 (29) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/cpp/fabric/NativeStatePropsGetter.cpp +29 -0
  4. package/android/src/main/cpp/fabric/NativeStatePropsGetter.h +17 -0
  5. package/android/src/main/cpp/worklets/WorkletJSCallInvoker.cpp +1 -4
  6. package/android/src/main/java/expo/modules/kotlin/jni/fabric/NativeStatePropsGetter.kt +23 -0
  7. package/android/src/main/java/expo/modules/kotlin/views/ShadowNodeProxy.kt +14 -0
  8. package/build/NativeViewManagerAdapter.native.d.ts.map +1 -1
  9. package/common/cpp/fabric/ContentOriginRegistry.h +63 -0
  10. package/common/cpp/fabric/ExpoViewComponentDescriptor.h +41 -0
  11. package/common/cpp/fabric/ExpoViewShadowNode.h +136 -0
  12. package/ios/Core/AppContext.swift +7 -5
  13. package/ios/Core/Conversions.swift +17 -0
  14. package/ios/Core/Views/SwiftUI/SwiftUIShadowNodeProxy.swift +3 -0
  15. package/ios/Core/Views/SwiftUI/SwiftUIViewProps.swift +24 -2
  16. package/ios/Core/Views/SwiftUI/SwiftUIVirtualView.swift +12 -0
  17. package/ios/Core/Views/SwiftUI/SwiftUIVirtualViewObjC.h +17 -0
  18. package/ios/Core/Views/SwiftUI/SwiftUIVirtualViewObjCDev.h +13 -0
  19. package/ios/Core/Views/SwiftUI/SwiftUIVirtualViewSharedImpl+Private.h +18 -0
  20. package/ios/Fabric/EXContentOriginRegistry.h +17 -0
  21. package/ios/Fabric/EXContentOriginRegistry.mm +13 -0
  22. package/ios/Fabric/ExpoFabricView.swift +1 -16
  23. package/ios/WorkletsAdapter/ExpoWorkletsBridgeProvider.mm +1 -1
  24. package/package.json +3 -3
  25. package/prebuilds/output/debug/xcframeworks/ExpoModulesCore.tar.gz +0 -0
  26. package/prebuilds/output/debug/xcframeworks/ExpoModulesWorklets.tar.gz +0 -0
  27. package/prebuilds/output/release/xcframeworks/ExpoModulesCore.tar.gz +0 -0
  28. package/prebuilds/output/release/xcframeworks/ExpoModulesWorklets.tar.gz +0 -0
  29. package/src/NativeViewManagerAdapter.native.tsx +7 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,29 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.15 — 2026-09-01
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS][Android] Fixed a `matchContents` `RNHostView` and the `matchContents` host around it feeding each other's size back and forth, which grew the layout on every pass. ([#49483](https://github.com/expo/expo/pull/49483) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
18
+ - [iOS] Fixed SwiftUI view props re-decoding every field on every props update. Fabric sends the whole props map rather than a delta, so an unrelated prop change replaced each decoded value with an equal but distinct one, which cost a decode per field and stopped SwiftUI from pruning the view tree that reads it. A field whose raw value is unchanged now keeps the value decoded before, the same way `ExpoFabricView.updateProps` already worked for UIKit views. ([#48426](https://github.com/expo/expo/pull/48426) by [@nishan](https://github.com/intergalacticspacehighway))
19
+
20
+ ### 💡 Others
21
+
22
+ - Migrated from deprecated react-native-worklets WorkletRuntime API `executeSync` to up-to-date `runSync`. `runSync` is available since 0.7.0. ([#48691](https://github.com/expo/expo/pull/48691) by [@tjzel](https://github.com/tjzel))
23
+ - Added internal `ExpoModulesProviderModuleName` lookup key for `ExpoModulesProvider` class. ([#49539](https://github.com/expo/expo/pull/49539) by [@kudo](https://github.com/kudo))
24
+
25
+ ## 57.0.14 — 2026-08-26
26
+
27
+ ### 🎉 New features
28
+
29
+ - Added `getNativeRef()` to views created with `requireNativeViewManager`, which returns the underlying host component. ([#48788](https://github.com/expo/expo/pull/48788) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
30
+
31
+ ### 🐛 Bug fixes
32
+
33
+ - [iOS] Measure hosted React Native views where SwiftUI placed them, instead of at their Yoga box. ([#48969](https://github.com/expo/expo/pull/48969) by [@nishan](https://github.com/intergalacticspacehighway))
34
+ - [Android] Measure hosted React Native views where Jetpack Compose placed them, instead of at their Yoga box. ([#48970](https://github.com/expo/expo/pull/48970) by [@nishan](https://github.com/intergalacticspacehighway))
35
+
13
36
  ## 57.0.13 — 2026-08-24
14
37
 
15
38
  ### 🐛 Bug fixes
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '57.0.13'
30
+ version = '57.0.15'
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.13"
97
+ versionName "57.0.15"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -1,5 +1,6 @@
1
1
  #include "NativeStatePropsGetter.h"
2
2
  #include "AndroidExpoViewState.h"
3
+ #include "ContentOriginRegistry.h"
3
4
  #include <react/fabric/StateWrapperImpl.h>
4
5
 
5
6
  #include <react/renderer/core/ConcreteState.h>
@@ -11,6 +12,9 @@ void NativeStatePropsGetter::registerNatives() {
11
12
  makeNativeMethod("getStateProps", NativeStatePropsGetter::getStateProps),
12
13
  makeNativeMethod("updateStyleSizeImmediateImpl", NativeStatePropsGetter::updateStyleSizeImmediate),
13
14
  makeNativeMethod("updateViewSizeImmediateImpl", NativeStatePropsGetter::updateViewSizeImmediate),
15
+ makeNativeMethod("setContentOriginImpl", NativeStatePropsGetter::setContentOrigin),
16
+ makeNativeMethod("clearContentOriginImpl", NativeStatePropsGetter::clearContentOrigin),
17
+ makeNativeMethod("clearAllContentOriginsImpl", NativeStatePropsGetter::clearAllContentOrigins),
14
18
  });
15
19
  }
16
20
 
@@ -57,6 +61,31 @@ void NativeStatePropsGetter::updateViewSizeImmediate(
57
61
  state->updateState(std::move(newState), react::EventQueue::UpdateMode::unstable_Immediate);
58
62
  }
59
63
 
64
+ void NativeStatePropsGetter::setContentOrigin(
65
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self,
66
+ jint tag,
67
+ jdouble x,
68
+ jdouble y
69
+ ) {
70
+ ContentOriginRegistry::set(
71
+ static_cast<react::Tag>(tag),
72
+ react::Point{.x = static_cast<react::Float>(x), .y = static_cast<react::Float>(y)}
73
+ );
74
+ }
75
+
76
+ void NativeStatePropsGetter::clearContentOrigin(
77
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self,
78
+ jint tag
79
+ ) {
80
+ ContentOriginRegistry::clear(static_cast<react::Tag>(tag));
81
+ }
82
+
83
+ void NativeStatePropsGetter::clearAllContentOrigins(
84
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self
85
+ ) {
86
+ ContentOriginRegistry::clearAll();
87
+ }
88
+
60
89
  jni::local_ref<jni::JMap<jstring, jobject>> NativeStatePropsGetter::getStateProps(
61
90
  jni::alias_ref<NativeStatePropsGetter::javaobject> self,
62
91
  jni::alias_ref<jobject> stateWrapper
@@ -35,6 +35,23 @@ public:
35
35
  jdouble width,
36
36
  jdouble height
37
37
  );
38
+
39
+ // Publishes where Compose actually placed a hosted view inside its `Host`, which Yoga cannot see.
40
+ static void setContentOrigin(
41
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self,
42
+ jint tag,
43
+ jdouble x,
44
+ jdouble y
45
+ );
46
+
47
+ static void clearContentOrigin(
48
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self,
49
+ jint tag
50
+ );
51
+
52
+ static void clearAllContentOrigins(
53
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self
54
+ );
38
55
  };
39
56
 
40
57
  } // namespace expo
@@ -24,10 +24,7 @@ namespace expo {
24
24
  return;
25
25
  }
26
26
 
27
- workletRuntime->executeSync([func = std::move(func)](jsi::Runtime &rt) -> jsi::Value {
28
- func(rt);
29
- return jsi::Value::undefined();
30
- });
27
+ workletRuntime->runSync(func);
31
28
  }
32
29
  } // namespace expo
33
30
 
@@ -24,6 +24,29 @@ class NativeStatePropsGetter {
24
24
  updateViewSizeImmediateImpl(stateWrapper, width, height)
25
25
  }
26
26
 
27
+ /**
28
+ * Publishes where the native layout system placed a hosted view inside its `Host`, keyed by the
29
+ * view's tag. Read while measuring, so `measure()` reports the position the content occupies
30
+ * rather than its Yoga box.
31
+ */
32
+ fun setContentOrigin(tag: Int, x: Double, y: Double) {
33
+ setContentOriginImpl(tag, x, y)
34
+ }
35
+
36
+ fun clearContentOrigin(tag: Int) {
37
+ clearContentOriginImpl(tag)
38
+ }
39
+
40
+ fun clearAllContentOrigins() {
41
+ clearAllContentOriginsImpl()
42
+ }
43
+
44
+ private external fun setContentOriginImpl(tag: Int, x: Double, y: Double)
45
+
46
+ private external fun clearContentOriginImpl(tag: Int)
47
+
48
+ private external fun clearAllContentOriginsImpl()
49
+
27
50
  private external fun updateStyleSizeImmediateImpl(
28
51
  stateWrapper: Any,
29
52
  styleWidth: Double,
@@ -28,6 +28,20 @@ class ShadowNodeProxy(expoView: ExpoView) {
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * Reports where the native layout system drew this view inside its `Host`, in dp relative to the
33
+ * `Host`'s origin.
34
+ */
35
+ fun setContentOrigin(x: Double, y: Double) {
36
+ val view = weakExpoView.get() ?: return
37
+ stateUpdater.setContentOrigin(view.id, x, y)
38
+ }
39
+
40
+ fun clearContentOrigin() {
41
+ val view = weakExpoView.get() ?: return
42
+ stateUpdater.clearContentOrigin(view.id)
43
+ }
44
+
31
45
  private fun scheduleFlush(flush: (stateWrapper: Any) -> Unit) {
32
46
  pendingFlush = flush
33
47
  val view = weakExpoView.get() ?: return
@@ -1 +1 @@
1
- {"version":3,"file":"NativeViewManagerAdapter.native.d.ts","sourceRoot":"","sources":["../src/NativeViewManagerAdapter.native.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAA4B,MAAM,OAAO,CAAC;AA4HrF;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAAC,CAAC,CAAC,CAsClB"}
1
+ {"version":3,"file":"NativeViewManagerAdapter.native.d.ts","sourceRoot":"","sources":["../src/NativeViewManagerAdapter.native.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAA4B,MAAM,OAAO,CAAC;AA4HrF;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAAC,CAAC,CAAC,CA6ClB"}
@@ -0,0 +1,63 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ #pragma once
4
+
5
+ #ifdef __cplusplus
6
+
7
+ #include <mutex>
8
+ #include <optional>
9
+ #include <unordered_map>
10
+
11
+ #include <react/renderer/core/ReactPrimitives.h>
12
+ #include <react/renderer/graphics/Point.h>
13
+
14
+ namespace expo {
15
+
16
+ /**
17
+ Where the native layout system drew a view's contents, keyed by React tag.
18
+
19
+ This sits outside the shadow tree on purpose. `ExpoViewState` would be the idiomatic home, but a
20
+ state update enqueues a commit, and the origin changes on every native layout pass — a commit per
21
+ pass can re-run layout, move the origin, and commit again. Writing here schedules no work.
22
+ */
23
+ class ContentOriginRegistry {
24
+ public:
25
+ static void set(facebook::react::Tag tag, facebook::react::Point origin) {
26
+ std::lock_guard<std::mutex> lock(mutex());
27
+ storage()[tag] = origin;
28
+ }
29
+
30
+ static void clear(facebook::react::Tag tag) {
31
+ std::lock_guard<std::mutex> lock(mutex());
32
+ storage().erase(tag);
33
+ }
34
+
35
+ static void clearAll() {
36
+ std::lock_guard<std::mutex> lock(mutex());
37
+ storage().clear();
38
+ }
39
+
40
+ static std::optional<facebook::react::Point> find(facebook::react::Tag tag) {
41
+ std::lock_guard<std::mutex> lock(mutex());
42
+ auto it = storage().find(tag);
43
+ if (it == storage().end()) {
44
+ return std::nullopt;
45
+ }
46
+ return it->second;
47
+ }
48
+
49
+ private:
50
+ static std::mutex &mutex() {
51
+ static std::mutex instance;
52
+ return instance;
53
+ }
54
+
55
+ static std::unordered_map<facebook::react::Tag, facebook::react::Point> &storage() {
56
+ static std::unordered_map<facebook::react::Tag, facebook::react::Point> instance;
57
+ return instance;
58
+ }
59
+ };
60
+
61
+ } // namespace expo
62
+
63
+ #endif // __cplusplus
@@ -30,6 +30,32 @@ public:
30
30
  return std::static_pointer_cast<std::string const>(this->flavor_)->c_str();
31
31
  }
32
32
 
33
+ static bool isRNHostView(const react::Props::Shared &props) {
34
+ // Currently, only RNHostView can have `sizesToContent` set to true.
35
+ return ShadowNodeType::sizesToContent(props);
36
+ }
37
+
38
+ std::shared_ptr<facebook::react::ShadowNode> createShadowNode(
39
+ const facebook::react::ShadowNodeFragment &fragment,
40
+ const facebook::react::ShadowNodeFamily::Shared &family
41
+ ) const override {
42
+ if (!isRNHostView(fragment.props)) {
43
+ return facebook::react::ConcreteComponentDescriptor<ShadowNodeType>::createShadowNode(
44
+ fragment, family);
45
+ }
46
+
47
+ // Treat RNHostView as a leaf node and measurable node in Yoga, so that it can be measured by its children.
48
+ auto traits = this->getTraits();
49
+ traits.set(facebook::react::ShadowNodeTraits::Trait::LeafYogaNode);
50
+ traits.set(facebook::react::ShadowNodeTraits::Trait::MeasurableYogaNode);
51
+
52
+ auto shadowNode = std::make_shared<ShadowNodeType>(fragment, family, traits);
53
+
54
+ this->adopt(*shadowNode);
55
+
56
+ return shadowNode;
57
+ }
58
+
33
59
  void adopt(facebook::react::ShadowNode &shadowNode) const override {
34
60
  react_native_assert(dynamic_cast<ShadowNodeType *>(&shadowNode));
35
61
 
@@ -81,6 +107,21 @@ public:
81
107
  // Updates yoga style from props and sets the node dirty
82
108
  snode->updateYogaProps();
83
109
  }
110
+
111
+ if (isRNHostView(snode->getProps())) {
112
+ auto const &props = *std::static_pointer_cast<const facebook::react::ViewProps>(
113
+ snode->getProps());
114
+ auto &style = const_cast<facebook::yoga::Style &>(props.yogaStyle);
115
+
116
+ // If RNHostView has align self set to auto or stretch, we should override it to flex-start so that the node can size itself to its content
117
+ auto const alignSelf = style.alignSelf();
118
+
119
+ if (alignSelf == facebook::yoga::Align::Auto || alignSelf == facebook::yoga::Align::Stretch) {
120
+ style.setAlignSelf(facebook::yoga::Align::FlexStart);
121
+ snode->updateYogaProps();
122
+ }
123
+ }
124
+
84
125
  facebook::react::ConcreteComponentDescriptor<ShadowNodeType>::adopt(shadowNode);
85
126
  }
86
127
  };
@@ -5,7 +5,13 @@
5
5
  #ifdef __cplusplus
6
6
 
7
7
  #include <react/renderer/components/view/ConcreteViewShadowNode.h>
8
+ #include <react/renderer/core/LayoutConstraints.h>
9
+ #include <react/renderer/core/LayoutContext.h>
10
+ #include <react/renderer/core/LayoutableShadowNode.h>
8
11
 
12
+ #include <algorithm>
13
+
14
+ #include "ContentOriginRegistry.h"
9
15
  #include "ExpoViewEventEmitter.h"
10
16
  #include "ExpoViewProps.h"
11
17
  #include "ExpoViewState.h"
@@ -51,7 +57,137 @@ public:
51
57
  return traits;
52
58
  }
53
59
 
60
+ /**
61
+ Used by `RNHostView`. Reports where this view's contents were actually drawn, for views laid
62
+ out by SwiftUI or Compose instead of Yoga. Without it `measure()` returns this node's Yoga box,
63
+ which is not where the hosted content ended up.
64
+ */
65
+ facebook::react::Point getContentOriginOffset(bool includeTransform) const override {
66
+ // A `false` caller is asking for a scroll offset, which this view never has. Only the layout
67
+ // path passes `true`, and that is the one this override is for.
68
+ if (!includeTransform) {
69
+ return ConcreteViewShadowNode::getContentOriginOffset(includeTransform);
70
+ }
71
+
72
+ auto contentOrigin = ContentOriginRegistry::find(this->getTag());
73
+ if (!contentOrigin) {
74
+ return ConcreteViewShadowNode::getContentOriginOffset(includeTransform);
75
+ }
76
+
77
+ // `computeRelativeLayoutMetrics` adds this node's own Yoga origin before consulting us,
78
+ // so subtract them as we only want content origin to be considered.
79
+ // Publish only the part Yoga cannot see: where
80
+ // the native layout system placed the content inside its host.
81
+ auto ownOrigin = this->getLayoutMetrics().frame.origin;
82
+ return {.x = contentOrigin->x - ownOrigin.x, .y = contentOrigin->y - ownOrigin.y};
83
+ }
84
+
85
+ // Currently, only `RNHostView` declares `expoInternalSizeFromChildren`
86
+ static bool sizesToContent(const react::Props::Shared &props) {
87
+ auto const *viewProps = dynamic_cast<const ExpoViewProps *>(props.get());
88
+
89
+ if (viewProps == nullptr) {
90
+ return false;
91
+ }
92
+
93
+ auto const it = viewProps->propsMap.find("expoInternalSizeFromChildren");
94
+ return it != viewProps->propsMap.end() && it->second.isBool() && it->second.getBool();
95
+ }
96
+
97
+ // Yoga calls this method for RNHostView when it has matchContents set
98
+ react::Size measureContent(
99
+ const react::LayoutContext &layoutContext,
100
+ const react::LayoutConstraints &layoutConstraints
101
+ ) const override {
102
+ // Return default behavior when RNHostView does not have `sizesToContent` set to true
103
+ if (!sizesToContent(this->getProps())) {
104
+ return ConcreteViewShadowNode::measureContent(layoutContext, layoutConstraints);
105
+ }
106
+
107
+ auto const *content = hostedContent();
108
+
109
+ if (content == nullptr) {
110
+ return {};
111
+ }
112
+
113
+ return content->measure(layoutContext, hostedContentConstraints(*content));
114
+ }
115
+
116
+ // We override this so RNHostView can lay out it's children
117
+ // We marked it as a Leaf node so we need to manually lay out the hosted content
118
+ void layout(react::LayoutContext layoutContext) override {
119
+ ConcreteViewShadowNode::layout(layoutContext);
120
+
121
+ if (!sizesToContent(this->getProps())) {
122
+ return;
123
+ }
124
+
125
+ auto const *content = hostedContent();
126
+
127
+ if (content == nullptr) {
128
+ return;
129
+ }
130
+
131
+ // Use the same constraint that was used to measure the content, so that the layout is consistent with the measurement
132
+ auto const clonedContent = content->clone({});
133
+ static_cast<react::LayoutableShadowNode &>(*clonedContent).layoutTree(
134
+ layoutContext,
135
+ hostedContentConstraints(*content)
136
+ );
137
+
138
+ this->replaceChild(*content, clonedContent, 0);
139
+
140
+ if (layoutContext.affectedNodes != nullptr) {
141
+ layoutContext.affectedNodes->push_back(
142
+ static_cast<const react::LayoutableShadowNode *>(clonedContent.get()));
143
+ }
144
+ }
145
+
54
146
  private:
147
+ const react::LayoutableShadowNode *hostedContent() const {
148
+ auto const &children = this->getChildren();
149
+
150
+ return children.empty()
151
+ ? nullptr
152
+ : dynamic_cast<const react::LayoutableShadowNode *>(children.front().get());
153
+ }
154
+
155
+ react::LayoutDirection resolvedLayoutDirection() const {
156
+ return YGNodeLayoutGetDirection(&this->yogaNode_) == YGDirectionRTL
157
+ ? react::LayoutDirection::RightToLeft
158
+ : react::LayoutDirection::LeftToRight;
159
+ }
160
+
161
+ react::LayoutConstraints hostedContentConstraints(const react::ShadowNode &content) const {
162
+ react::LayoutConstraints constraints{};
163
+ constraints.layoutDirection = resolvedLayoutDirection();
164
+
165
+ auto const *contentProps = dynamic_cast<const react::ViewProps *>(content.getProps().get());
166
+
167
+ if (contentProps == nullptr) {
168
+ return constraints;
169
+ }
170
+
171
+ auto const &style = contentProps->yogaStyle;
172
+
173
+ constrainToPoints(style.minDimension(facebook::yoga::Dimension::Width),
174
+ constraints.minimumSize.width);
175
+ constrainToPoints(style.minDimension(facebook::yoga::Dimension::Height),
176
+ constraints.minimumSize.height);
177
+ constrainToPoints(style.maxDimension(facebook::yoga::Dimension::Width),
178
+ constraints.maximumSize.width);
179
+ constrainToPoints(style.maxDimension(facebook::yoga::Dimension::Height),
180
+ constraints.maximumSize.height);
181
+
182
+ return constraints;
183
+ }
184
+
185
+ static void constrainToPoints(facebook::yoga::StyleSizeLength length, react::Float &constraint) {
186
+ if (length.isPoints() && length.value().isDefined()) {
187
+ constraint = std::max<react::Float>(0, length.value().unwrap());
188
+ }
189
+ }
190
+
55
191
  void initialize() noexcept {
56
192
  auto &viewProps = static_cast<const ExpoViewProps &>(*this->props_);
57
193
 
@@ -724,12 +724,14 @@ public final class AppContext: NSObject, EXAppContextProtocol, @unchecked Sendab
724
724
  */
725
725
  @objc
726
726
  public static func modulesProvider(withName providerName: String = "ExpoModulesProvider") -> ModulesProvider {
727
- // [0] When ExpoModulesCore is built as separated framework/module,
728
- // we should explicitly load main bundle's `ExpoModulesProvider` class.
729
- // CFBundleExecutable is tried first: it is the product name, from which the Swift module
730
- // name is derived. CFBundleName is kept as a fallback for the uncommon case where both
731
- // values are identical valid identifiers.
727
+ // [0] When ExpoModulesCore is built as a separate framework/module,
728
+ // explicitly load the main bundle's `ExpoModulesProvider` class.
729
+ // `ExpoModulesProviderModuleName` is an internal key that allows repack-app to
730
+ // preserve the original Swift module name. Try `CFBundleExecutable` next because
731
+ // it usually matches the Swift module name. Keep `CFBundleName` as a final fallback
732
+ // for cases where it is also a valid module identifier.
732
733
  let mainBundleNames = [
734
+ Bundle.main.infoDictionary?["ExpoModulesProviderModuleName"],
733
735
  Bundle.main.infoDictionary?["CFBundleExecutable"],
734
736
  Bundle.main.infoDictionary?["CFBundleName"]
735
737
  ].compactMap { $0 as? String }
@@ -34,6 +34,23 @@ public struct Conversions {
34
34
  }
35
35
  }
36
36
 
37
+ /**
38
+ Compares two prop values for equality, to tell an actual prop change from a re-delivery of the
39
+ same value.
40
+ */
41
+ static func areValuesEqual(_ lhs: Any?, _ rhs: Any?) -> Bool {
42
+ switch (lhs, rhs) {
43
+ case (nil, nil):
44
+ return true
45
+ case let (lhsValue as AnyHashable, rhsValue as AnyHashable):
46
+ return lhsValue == rhsValue
47
+ case let (lhsValue as NSObjectProtocol, rhsValue as NSObjectProtocol):
48
+ return lhsValue.isEqual(rhsValue)
49
+ default:
50
+ return false
51
+ }
52
+ }
53
+
37
54
  static func fromNSObject(_ object: Any) -> Any {
38
55
  switch object {
39
56
  case let object as NSArray:
@@ -12,5 +12,8 @@ extension ExpoSwiftUI {
12
12
 
13
13
  public var setViewSize: ((CGSize) -> Void)?
14
14
  public var setStyleSize: ((NSNumber?, NSNumber?) -> Void)?
15
+ // Where the native layout system drew this view's contents, relative to its host.
16
+ public var setContentOrigin: ((CGPoint) -> Void)?
17
+ public var clearContentOrigin: (() -> Void)?
15
18
  }
16
19
  }
@@ -47,9 +47,31 @@ extension ExpoSwiftUI {
47
47
  */
48
48
  public let globalEventDispatcher = EventDispatcher(GLOBAL_EVENT_NAME)
49
49
 
50
+ /**
51
+ A dictionary to store previous raw prop values for change detection.
52
+ */
53
+ private var previousRawProps: [String: Any] = [:]
54
+
50
55
  internal func updateRawProps(_ rawProps: [String: Any], appContext: AppContext) throws {
51
- // Update the props just like the records
52
- try update(withDict: rawProps, appContext: appContext)
56
+ try fieldsOf(self).forEach { field in
57
+ guard let key = field.key else {
58
+ return
59
+ }
60
+ guard rawProps.keys.contains(key) else {
61
+ if field.isRequired {
62
+ try field.set(nil, appContext: appContext)
63
+ }
64
+ return
65
+ }
66
+ let newValue = rawProps[key]
67
+ let previousValue = previousRawProps[key]
68
+
69
+ if !Conversions.areValuesEqual(previousValue, newValue) {
70
+ try field.set(newValue, appContext: appContext)
71
+
72
+ previousRawProps[key] = newValue
73
+ }
74
+ }
53
75
 
54
76
  // Notify subscribed views about the change to re-render them.
55
77
  objectWillChange.send()
@@ -51,6 +51,12 @@ extension ExpoSwiftUI {
51
51
  props.shadowNodeProxy.setStyleSize = { [weak self] width, height in
52
52
  self?.setStyleSize(width, height: height)
53
53
  }
54
+ props.shadowNodeProxy.setContentOrigin = { [weak self] origin in
55
+ self?.setContentOrigin(origin)
56
+ }
57
+ props.shadowNodeProxy.clearContentOrigin = { [weak self] in
58
+ self?.clearContentOrigin()
59
+ }
54
60
 
55
61
  installEventDispatchers()
56
62
  }
@@ -213,6 +219,12 @@ extension ExpoSwiftUI {
213
219
  props.shadowNodeProxy.setStyleSize = { [weak self] width, height in
214
220
  self?.setStyleSize(width, height: height)
215
221
  }
222
+ props.shadowNodeProxy.setContentOrigin = { [weak self] origin in
223
+ self?.setContentOrigin(origin)
224
+ }
225
+ props.shadowNodeProxy.clearContentOrigin = { [weak self] in
226
+ self?.clearContentOrigin()
227
+ }
216
228
 
217
229
  installEventDispatchers()
218
230
  }
@@ -14,6 +14,15 @@ NS_ASSUME_NONNULL_BEGIN
14
14
  @property (nonatomic) NSInteger tag;
15
15
  @property (nonatomic, copy, nullable) NSString *componentName;
16
16
 
17
+ /**
18
+ The tag this view last published a content origin under.
19
+
20
+ React Native zeroes `tag` when it recycles a component view, and it does so *before* calling
21
+ `prepareForRecycle` or `invalidate`, so a teardown hook cannot use `tag` to find its own registry
22
+ entry. Remembering it here is what lets the entry be removed rather than leaked.
23
+ */
24
+ @property (nonatomic) NSInteger publishedContentOriginTag;
25
+
17
26
  - (void)dispatchEvent:(nonnull NSString *)eventName payload:(nullable id)payload;
18
27
 
19
28
  - (void)updateProps:(nonnull NSDictionary<NSString *, id> *)props NS_SWIFT_UI_ACTOR;
@@ -24,6 +33,14 @@ NS_ASSUME_NONNULL_BEGIN
24
33
 
25
34
  - (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height;
26
35
 
36
+ /**
37
+ Publishes where SwiftUI drew this view's contents, relative to its host, so `measure()` matches
38
+ what is on screen. Pass `CGPointZero`-equivalent by calling `clearContentOrigin`.
39
+ */
40
+ - (void)setContentOrigin:(CGPoint)contentOrigin;
41
+
42
+ - (void)clearContentOrigin;
43
+
27
44
  - (BOOL)supportsPropWithName:(nonnull NSString *)name;
28
45
 
29
46
  /*
@@ -17,6 +17,15 @@ NS_ASSUME_NONNULL_BEGIN
17
17
  // `tag` is inherited from UIView
18
18
  @property (nonatomic, copy, nullable) NSString *componentName;
19
19
 
20
+ /**
21
+ The tag this view last published a content origin under.
22
+
23
+ React Native zeroes `tag` when it recycles a component view, and it does so *before* calling
24
+ `prepareForRecycle` or `invalidate`, so a teardown hook cannot use `tag` to find its own registry
25
+ entry. Remembering it here is what lets the entry be removed rather than leaked.
26
+ */
27
+ @property (nonatomic) NSInteger publishedContentOriginTag;
28
+
20
29
  - (void)dispatchEvent:(nonnull NSString *)eventName payload:(nullable id)payload;
21
30
 
22
31
  - (void)updateProps:(nonnull NSDictionary<NSString *, id> *)props NS_SWIFT_UI_ACTOR;
@@ -27,6 +36,10 @@ NS_ASSUME_NONNULL_BEGIN
27
36
 
28
37
  - (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height;
29
38
 
39
+ - (void)setContentOrigin:(CGPoint)contentOrigin;
40
+
41
+ - (void)clearContentOrigin;
42
+
30
43
  - (BOOL)supportsPropWithName:(nonnull NSString *)name;
31
44
 
32
45
  /*
@@ -98,6 +98,7 @@ namespace react = facebook::react;
98
98
  - (void)prepareForRecycle
99
99
  {
100
100
  // Default implementation does nothing.
101
+ [self clearContentOrigin];
101
102
  _eventEmitter.reset();
102
103
  }
103
104
 
@@ -186,6 +187,23 @@ namespace react = facebook::react;
186
187
  }
187
188
  }
188
189
 
190
+ - (void)setContentOrigin:(CGPoint)contentOrigin
191
+ {
192
+ self.publishedContentOriginTag = self.tag;
193
+ expo::ContentOriginRegistry::set(
194
+ (react::Tag)self.tag,
195
+ react::Point{.x = (react::Float)contentOrigin.x, .y = (react::Float)contentOrigin.y});
196
+ }
197
+
198
+ - (void)clearContentOrigin
199
+ {
200
+ // Not `self.tag`: by the time a teardown hook runs, React Native has already zeroed it.
201
+ if (self.publishedContentOriginTag != 0) {
202
+ expo::ContentOriginRegistry::clear((react::Tag)self.publishedContentOriginTag);
203
+ self.publishedContentOriginTag = 0;
204
+ }
205
+ }
206
+
189
207
  - (BOOL)supportsPropWithName:(nonnull NSString *)name
190
208
  {
191
209
  // Implemented in `SwiftUIVirtualView.swift`
@@ -0,0 +1,17 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ NS_SWIFT_NAME(ContentOriginRegistry)
8
+ @interface EXContentOriginRegistry : NSObject
9
+
10
+ /**
11
+ Drops every published content origin
12
+ */
13
+ + (void)clearAll;
14
+
15
+ @end
16
+
17
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,13 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ #import <ExpoModulesCore/EXContentOriginRegistry.h>
4
+ #import <ExpoModulesCore/ContentOriginRegistry.h>
5
+
6
+ @implementation EXContentOriginRegistry
7
+
8
+ + (void)clearAll
9
+ {
10
+ expo::ContentOriginRegistry::clearAll();
11
+ }
12
+
13
+ @end
@@ -91,7 +91,7 @@ open class ExpoFabricView: ExpoFabricViewObjC, AnyExpoView {
91
91
  let previousValue = previousProps[key]
92
92
 
93
93
  // only set the prop if the value has changed
94
- if !areValuesEqual(previousValue, convertedNewValue) {
94
+ if !Conversions.areValuesEqual(previousValue, convertedNewValue) {
95
95
  // TODO: @tsapeta: Figure out better way to rethrow errors from here.
96
96
  // Adding `throws` keyword to the function results in different
97
97
  // method signature in Objective-C. Maybe just call `RCTLogError`?
@@ -102,21 +102,6 @@ open class ExpoFabricView: ExpoFabricViewObjC, AnyExpoView {
102
102
  }
103
103
  }
104
104
 
105
- /**
106
- Helper function to compare two values for equality using string representation.
107
- */
108
- private func areValuesEqual(_ lhs: Any?, _ rhs: Any?) -> Bool {
109
- switch (lhs, rhs) {
110
- case (nil, nil):
111
- return true
112
- case let (lhsValue as AnyHashable, rhsValue as AnyHashable):
113
- return lhsValue == rhsValue
114
- case let (lhsValue as NSObjectProtocol, rhsValue as NSObjectProtocol):
115
- return lhsValue.isEqual(rhsValue)
116
- default:
117
- return false
118
- }
119
- }
120
105
  /**
121
106
  Calls lifecycle methods registered by `OnViewDidUpdateProps` definition component.
122
107
  */
@@ -233,7 +233,7 @@ static jsi::Value callWorklet(jsi::Runtime &rt, std::shared_ptr<worklets::Serial
233
233
  return;
234
234
  }
235
235
 
236
- workletRuntime->executeSync([worklet, arguments](jsi::Runtime &rt) -> jsi::Value {
236
+ workletRuntime->runSync([worklet, arguments](jsi::Runtime &rt) -> jsi::Value {
237
237
  return callWorklet(rt, worklet, arguments);
238
238
  });
239
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "57.0.13",
3
+ "version": "57.0.15",
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.0.5",
50
+ "expo-modules-jsi": "~57.0.7",
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": "52fc013ec8c75e89ba8f83c167e4b6b0416e6bf7",
69
+ "gitHead": "b76ecf192c5325793bcea31dd9bb8efd91f9ad7f",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",
@@ -150,6 +150,13 @@ export function requireNativeViewManager<P>(
150
150
  this.nativeTag = findNodeHandle(this.nativeRef.current);
151
151
  }
152
152
 
153
+ /**
154
+ * The underlying host component
155
+ */
156
+ getNativeRef(): (Component & ReactNativeElement) | null {
157
+ return this.nativeRef.current;
158
+ }
159
+
153
160
  render() {
154
161
  return <ReactNativeComponent {...this.props} ref={this.nativeRef} />;
155
162
  }