expo-modules-core 56.0.21 → 56.0.23
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 +17 -0
- package/android/build.gradle +2 -2
- package/android/src/compose/expo/modules/kotlin/views/ExpoComposeView.kt +19 -0
- package/android/src/main/java/expo/modules/kotlin/views/ShadowNodeProxy.kt +31 -18
- package/ios/Core/Views/SwiftUI/SwiftUIHostingView.swift +19 -46
- package/ios/Core/Views/SwiftUI/SwiftUIViewHost.swift +31 -9
- package/ios/Core/Views/SwiftUI/SwiftUIViewProps.swift +1 -0
- package/package.json +2 -2
- 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/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,23 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 56.0.23 — 2026-08-06
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [Android] Fixed hosted Compose views missing layout after reattachment or in-place configuration changes. ([#48370](https://github.com/expo/expo/issues/48370) by [@lujjjh](https://github.com/lujjjh))
|
|
18
|
+
|
|
19
|
+
## 56.0.22 — 2026-07-23
|
|
20
|
+
|
|
21
|
+
### 🎉 New features
|
|
22
|
+
|
|
23
|
+
- [iOS] `ExpoSwiftUI.IgnoreSafeArea` that ignores the device and container safe area regions. ([#47619](https://github.com/expo/expo/pull/47619) by [@nishan](https://github.com/intergalacticspacehighway))
|
|
24
|
+
|
|
25
|
+
### 🐛 Bug fixes
|
|
26
|
+
|
|
27
|
+
- [iOS] Fix SwiftUI-hosted React views corrupting Fabric rendering after unmount: `UIViewHost` now hands SwiftUI a disposable isolation container instead of the React-managed view, so leaked `autoresizingMask`/frame/visibility mutations (including deferred ones from `Menu`/`ContextMenu` teardown) can no longer zero-size or hide unrelated components. ([#47707](https://github.com/expo/expo/pull/47707) by [@cvburgess](https://github.com/cvburgess))
|
|
28
|
+
- [Android] Fix Compose-hosted React Native content (e.g. the `@expo/ui` community `BottomSheet`) keeping a stale size after a rapid resize such as dismissing the software keyboard, leaving bottom-anchored content stranded mid-view. The final shadow node size update could be dropped when its one-shot pre-draw listener fired without a following draw pass; the pending update is now also posted to the view so the latest size always flushes. ([#47778](https://github.com/expo/expo/issues/47778) by [@zayyartun-cgm](https://github.com/zayyartun-cgm)) ([#47810](https://github.com/expo/expo/pull/47810) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
|
29
|
+
|
|
13
30
|
## 56.0.21 — 2026-07-15
|
|
14
31
|
|
|
15
32
|
### 🐛 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 = '56.0.
|
|
30
|
+
version = '56.0.23'
|
|
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 "56.0.
|
|
97
|
+
versionName "56.0.23"
|
|
98
98
|
buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
|
|
99
99
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
|
|
100
100
|
|
|
@@ -2,6 +2,7 @@ package expo.modules.kotlin.views
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
+
import android.content.res.Configuration
|
|
5
6
|
import android.util.Log
|
|
6
7
|
import android.view.View
|
|
7
8
|
import android.view.ViewGroup
|
|
@@ -113,6 +114,15 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
override fun dispatchConfigurationChanged(newConfig: Configuration) {
|
|
118
|
+
super.dispatchConfigurationChanged(newConfig)
|
|
119
|
+
// React Native owns this view's bounds and may not schedule another Android layout pass
|
|
120
|
+
// when a configuration change leaves its Yoga layout unchanged.
|
|
121
|
+
if (withHostingView && isAttachedToWindow && isLaidOut) {
|
|
122
|
+
requestLayout()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
/**
|
|
117
127
|
* Validates that this non-hosting Compose view has a valid Compose parent.
|
|
118
128
|
*
|
|
@@ -193,6 +203,15 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
193
203
|
if (withHostingView) {
|
|
194
204
|
clipChildren = false
|
|
195
205
|
clipToPadding = false
|
|
206
|
+
addOnAttachStateChangeListener(
|
|
207
|
+
OnAttachAfterDetachmentListener(
|
|
208
|
+
onAttachAfterDetachment = {
|
|
209
|
+
// Restore the Android layout pass after a real detach. The listener deliberately
|
|
210
|
+
// ignores the first attach and React Native's same-loop reparenting.
|
|
211
|
+
requestLayout()
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
)
|
|
196
215
|
addComposeView()
|
|
197
216
|
} else {
|
|
198
217
|
this.visibility = GONE
|
|
@@ -10,6 +10,7 @@ class ShadowNodeProxy(expoView: ExpoView) {
|
|
|
10
10
|
|
|
11
11
|
private var pendingFlush: ((stateWrapper: Any) -> Unit)? = null
|
|
12
12
|
private var preDrawListener: ViewTreeObserver.OnPreDrawListener? = null
|
|
13
|
+
private val flushRunnable = Runnable { drainPendingFlush() }
|
|
13
14
|
|
|
14
15
|
// Schedule in predraw listener to avoid early return in re-entrancy
|
|
15
16
|
// We have a proper fix [here](https://github.com/facebook/react-native/pull/56311)
|
|
@@ -29,25 +30,37 @@ class ShadowNodeProxy(expoView: ExpoView) {
|
|
|
29
30
|
|
|
30
31
|
private fun scheduleFlush(flush: (stateWrapper: Any) -> Unit) {
|
|
31
32
|
pendingFlush = flush
|
|
32
|
-
val
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
val view = weakExpoView.get() ?: return
|
|
34
|
+
val observer = view.viewTreeObserver?.takeIf { it.isAlive }
|
|
35
|
+
|
|
36
|
+
if (observer != null) {
|
|
37
|
+
// Remove the previous attached listener
|
|
38
|
+
preDrawListener?.let(observer::removeOnPreDrawListener)
|
|
39
|
+
|
|
40
|
+
val listener = object : ViewTreeObserver.OnPreDrawListener {
|
|
41
|
+
override fun onPreDraw(): Boolean {
|
|
42
|
+
preDrawListener = null
|
|
43
|
+
// The view is attached while drawing, so this re-fetch returns the same
|
|
44
|
+
// observer that is dispatching us. removeOnPreDrawListener throws on a dead
|
|
45
|
+
// observer, hence the isAlive guard.
|
|
46
|
+
weakExpoView.get()?.viewTreeObserver?.takeIf { it.isAlive }?.removeOnPreDrawListener(this)
|
|
47
|
+
drainPendingFlush()
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
48
50
|
}
|
|
51
|
+
preDrawListener = listener
|
|
52
|
+
observer.addOnPreDrawListener(listener)
|
|
49
53
|
}
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
|
|
55
|
+
// Predraw listener do not get called for each keyboard transition event so we add a fallback flush to be called here
|
|
56
|
+
// https://github.com/expo/expo/issues/47778
|
|
57
|
+
view.removeCallbacks(flushRunnable)
|
|
58
|
+
view.post(flushRunnable)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun drainPendingFlush() {
|
|
62
|
+
val flushNow = pendingFlush ?: return
|
|
63
|
+
pendingFlush = null
|
|
64
|
+
weakExpoView.get()?.stateWrapper?.let { flushNow.invoke(it) }
|
|
52
65
|
}
|
|
53
66
|
}
|
|
@@ -60,11 +60,6 @@ extension ExpoSwiftUI {
|
|
|
60
60
|
*/
|
|
61
61
|
private let hostingController: UIHostingController<AnyView>
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
Tracks whether safe area has been configured (can only be set once on mount)
|
|
65
|
-
*/
|
|
66
|
-
private var hasSafeAreaBeenConfigured = false
|
|
67
|
-
|
|
68
63
|
/**
|
|
69
64
|
Initializes a SwiftUI hosting view with the given SwiftUI view type.
|
|
70
65
|
*/
|
|
@@ -121,11 +116,8 @@ extension ExpoSwiftUI {
|
|
|
121
116
|
log.error("Updating props for \(ContentView.self) has failed: \(error.localizedDescription)")
|
|
122
117
|
}
|
|
123
118
|
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
let ignoreSafeArea = safeAreaProps.ignoreSafeArea {
|
|
127
|
-
hostingController.disableSafeArea(ignoreSafeArea)
|
|
128
|
-
hasSafeAreaBeenConfigured = true
|
|
119
|
+
if let safeAreaProps = props as? SafeAreaControllable {
|
|
120
|
+
hostingController.setSafeAreaRegions(ignoring: safeAreaProps.ignoreSafeArea)
|
|
129
121
|
}
|
|
130
122
|
}
|
|
131
123
|
|
|
@@ -260,45 +252,26 @@ extension ExpoSwiftUI {
|
|
|
260
252
|
}
|
|
261
253
|
|
|
262
254
|
extension UIHostingController {
|
|
263
|
-
|
|
264
|
-
|
|
255
|
+
/// Applies the `ignoreSafeArea` mode reactively, restoring the default safe area when `nil` so
|
|
256
|
+
/// clearing the prop re-enables the safe area without an app reload.
|
|
257
|
+
func setSafeAreaRegions(ignoring mode: ExpoSwiftUI.IgnoreSafeArea?) {
|
|
258
|
+
// `safeAreaRegions` needs iOS 16.4+; the precompiled xcframework targets 16.0, so no-op below it.
|
|
259
|
+
guard #available(iOS 16.4, tvOS 16.4, macOS 13.3, *) else {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
var regions: SafeAreaRegions = .all
|
|
263
|
+
if let mode {
|
|
265
264
|
switch mode {
|
|
266
265
|
case .all:
|
|
267
|
-
|
|
266
|
+
regions = []
|
|
267
|
+
case .container:
|
|
268
|
+
regions.remove(.container)
|
|
268
269
|
case .keyboard:
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
// For older versions
|
|
273
|
-
// https://gist.github.com/steipete/da72299613dcc91e8d729e48b4bb582c
|
|
274
|
-
// https://developer.apple.com/forums/thread/658432
|
|
275
|
-
guard let viewClass = object_getClass(view) else { return }
|
|
276
|
-
|
|
277
|
-
let suffix = mode == .all ? "_IgnoresSafeArea" : "_IgnoresKeyboard"
|
|
278
|
-
let viewSubclassName = String(cString: class_getName(viewClass)).appending(suffix)
|
|
279
|
-
if let viewSubclass = NSClassFromString(viewSubclassName) {
|
|
280
|
-
object_setClass(view, viewSubclass)
|
|
281
|
-
} else {
|
|
282
|
-
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
|
|
283
|
-
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
|
|
284
|
-
|
|
285
|
-
if mode == .all,
|
|
286
|
-
let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
|
|
287
|
-
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
|
|
288
|
-
return .zero
|
|
289
|
-
}
|
|
290
|
-
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets),
|
|
291
|
-
imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if let method = class_getInstanceMethod(viewClass, NSSelectorFromString("keyboardWillShowWithNotification:")) {
|
|
295
|
-
let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in }
|
|
296
|
-
class_addMethod(viewSubclass, NSSelectorFromString("keyboardWillShowWithNotification:"),
|
|
297
|
-
imp_implementationWithBlock(keyboardWillShow), method_getTypeEncoding(method))
|
|
298
|
-
}
|
|
299
|
-
objc_registerClassPair(viewSubclass)
|
|
300
|
-
object_setClass(view, viewSubclass)
|
|
301
|
-
}
|
|
270
|
+
regions.remove(.keyboard)
|
|
302
271
|
}
|
|
303
272
|
}
|
|
273
|
+
if safeAreaRegions != regions {
|
|
274
|
+
safeAreaRegions = regions
|
|
275
|
+
}
|
|
276
|
+
}
|
|
304
277
|
}
|
|
@@ -13,7 +13,6 @@ extension ExpoSwiftUI {
|
|
|
13
13
|
|
|
14
14
|
#if os(macOS)
|
|
15
15
|
func makeNSView(context: Context) -> NSView {
|
|
16
|
-
context.coordinator.originalAutoresizingMask = view.autoresizingMask
|
|
17
16
|
return view
|
|
18
17
|
}
|
|
19
18
|
|
|
@@ -23,8 +22,17 @@ extension ExpoSwiftUI {
|
|
|
23
22
|
#endif
|
|
24
23
|
|
|
25
24
|
func makeUIView(context: Context) -> UIView {
|
|
26
|
-
|
|
25
|
+
#if os(macOS)
|
|
27
26
|
return view
|
|
27
|
+
#else
|
|
28
|
+
// SwiftUI mutates the view it hosts (autoresizingMask, frame, visibility), sometimes
|
|
29
|
+
// asynchronously after teardown, corrupting unmounted or recycled React views.
|
|
30
|
+
// Hand it a disposable container instead. Fixes expo/expo#47706; supersedes the #40604 mitigation.
|
|
31
|
+
let container = ReactViewIsolationContainer()
|
|
32
|
+
container.hostedView = view
|
|
33
|
+
container.addSubview(view)
|
|
34
|
+
return container
|
|
35
|
+
#endif
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
func updateUIView(_ uiView: UIView, context: Context) {
|
|
@@ -32,11 +40,7 @@ extension ExpoSwiftUI {
|
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
static func dismantleUIView(_ uiView: UIView, coordinator: Coordinator) {
|
|
35
|
-
//
|
|
36
|
-
// UIViewRepresentable attaches autoresizingMask w+h to the hosted UIView
|
|
37
|
-
// This causes issues for RN views when they are recycled.
|
|
38
|
-
// So we restore the original autoresizingMask to avoid issues.
|
|
39
|
-
uiView.autoresizingMask = coordinator.originalAutoresizingMask
|
|
43
|
+
// Nothing to restore — SwiftUI only ever touched the container.
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
func makeCoordinator() -> Coordinator {
|
|
@@ -44,7 +48,6 @@ extension ExpoSwiftUI {
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
class Coordinator {
|
|
47
|
-
var originalAutoresizingMask: UIView.AutoresizingMask = []
|
|
48
51
|
init() {}
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -63,5 +66,24 @@ extension ExpoSwiftUI {
|
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
#if !os(macOS)
|
|
70
|
+
/**
|
|
71
|
+
Disposable UIView handed to SwiftUI in place of the React-managed view —
|
|
72
|
+
see `UIViewHost.makeUIView`.
|
|
73
|
+
*/
|
|
74
|
+
private final class ReactViewIsolationContainer: UIView {
|
|
75
|
+
weak var hostedView: UIView?
|
|
76
|
+
|
|
77
|
+
override func layoutSubviews() {
|
|
78
|
+
super.layoutSubviews()
|
|
79
|
+
// Ignore transient zero bounds (initial layout, teardown) — never propagate them.
|
|
80
|
+
guard let hostedView, hostedView.superview === self, !bounds.isEmpty else {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if hostedView.frame != bounds {
|
|
84
|
+
hostedView.frame = bounds
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
#endif
|
|
66
89
|
}
|
|
67
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-core",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.23",
|
|
4
4
|
"description": "The core of Expo Modules architecture",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -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": "eebecc21d8d868d97e72bd61bb8e668fb2feb704",
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "expo-module build",
|
|
72
72
|
"clean": "expo-module clean",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|