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.
- package/CHANGELOG.md +23 -0
- package/android/build.gradle +2 -2
- package/android/src/main/cpp/fabric/NativeStatePropsGetter.cpp +29 -0
- package/android/src/main/cpp/fabric/NativeStatePropsGetter.h +17 -0
- package/android/src/main/cpp/worklets/WorkletJSCallInvoker.cpp +1 -4
- package/android/src/main/java/expo/modules/kotlin/jni/fabric/NativeStatePropsGetter.kt +23 -0
- package/android/src/main/java/expo/modules/kotlin/views/ShadowNodeProxy.kt +14 -0
- package/build/NativeViewManagerAdapter.native.d.ts.map +1 -1
- package/common/cpp/fabric/ContentOriginRegistry.h +63 -0
- package/common/cpp/fabric/ExpoViewComponentDescriptor.h +41 -0
- package/common/cpp/fabric/ExpoViewShadowNode.h +136 -0
- package/ios/Core/AppContext.swift +7 -5
- package/ios/Core/Conversions.swift +17 -0
- package/ios/Core/Views/SwiftUI/SwiftUIShadowNodeProxy.swift +3 -0
- package/ios/Core/Views/SwiftUI/SwiftUIViewProps.swift +24 -2
- package/ios/Core/Views/SwiftUI/SwiftUIVirtualView.swift +12 -0
- package/ios/Core/Views/SwiftUI/SwiftUIVirtualViewObjC.h +17 -0
- package/ios/Core/Views/SwiftUI/SwiftUIVirtualViewObjCDev.h +13 -0
- package/ios/Core/Views/SwiftUI/SwiftUIVirtualViewSharedImpl+Private.h +18 -0
- package/ios/Fabric/EXContentOriginRegistry.h +17 -0
- package/ios/Fabric/EXContentOriginRegistry.mm +13 -0
- package/ios/Fabric/ExpoFabricView.swift +1 -16
- package/ios/WorkletsAdapter/ExpoWorkletsBridgeProvider.mm +1 -1
- package/package.json +3 -3
- package/prebuilds/output/debug/xcframeworks/ExpoModulesCore.tar.gz +0 -0
- package/prebuilds/output/debug/xcframeworks/ExpoModulesWorklets.tar.gz +0 -0
- package/prebuilds/output/release/xcframeworks/ExpoModulesCore.tar.gz +0 -0
- package/prebuilds/output/release/xcframeworks/ExpoModulesWorklets.tar.gz +0 -0
- 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
|
package/android/build.gradle
CHANGED
|
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
group = 'host.exp.exponent'
|
|
30
|
-
version = '57.0.
|
|
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.
|
|
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,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,
|
|
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
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
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
|
-
|
|
52
|
-
|
|
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->
|
|
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.
|
|
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.
|
|
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": "
|
|
69
|
+
"gitHead": "b76ecf192c5325793bcea31dd9bb8efd91f9ad7f",
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "expo-module build",
|
|
72
72
|
"clean": "expo-module clean",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
}
|