expo-modules-core 56.0.23 → 56.0.24
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 +5 -0
- package/android/build.gradle +2 -2
- package/ios/Core/Views/SwiftUI/SwiftUIHostingView.swift +8 -0
- package/ios/Core/Views/SwiftUI/SystemMenuTouchGate.swift +127 -0
- package/ios/Utilities/SceneGeometry.swift +85 -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,10 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 56.0.24 — 2026-08-17
|
|
14
|
+
|
|
15
|
+
_This version does not introduce any user-facing changes._
|
|
16
|
+
|
|
13
17
|
## 56.0.23 — 2026-08-06
|
|
14
18
|
|
|
15
19
|
### 🐛 Bug fixes
|
|
16
20
|
|
|
21
|
+
- [iOS] Fixed the tap that closes a SwiftUI menu also pressing the React Native view underneath it. ([#48419](https://github.com/expo/expo/issues/48419) by [@bohdanstefaniuk](https://github.com/bohdanstefaniuk)) ([#48463](https://github.com/expo/expo/pull/48463) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
|
17
22
|
- [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
23
|
|
|
19
24
|
## 56.0.22 — 2026-07-23
|
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.24'
|
|
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.24"
|
|
98
98
|
buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
|
|
99
99
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
|
|
100
100
|
|
|
@@ -214,6 +214,14 @@ extension ExpoSwiftUI {
|
|
|
214
214
|
public override func didMoveToWindow() {
|
|
215
215
|
super.didMoveToWindow()
|
|
216
216
|
|
|
217
|
+
#if os(iOS)
|
|
218
|
+
if let window {
|
|
219
|
+
// SwiftUI content can open a menu, and UIKit passes the tap that closes it through to
|
|
220
|
+
// React Native underneath. The gate stops that tap from reaching the view below.
|
|
221
|
+
SystemMenuTouchGate.install(in: window)
|
|
222
|
+
}
|
|
223
|
+
#endif
|
|
224
|
+
|
|
217
225
|
if window != nil, let parentController = reactViewController() {
|
|
218
226
|
#if !os(macOS)
|
|
219
227
|
if parentController as? UINavigationController == nil && parentController as? UITabBarController == nil {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
#if os(iOS)
|
|
4
|
+
// Also brings in the subclass header, required to set `state` from the touch callbacks
|
|
5
|
+
// and to call `ignore(_:for:)` on React Native's touch handler.
|
|
6
|
+
import UIKit.UIGestureRecognizerSubclass
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
We create a custom gesture recognizer to stop React Root listening to touches when user taps background while Menu is opened.
|
|
10
|
+
Menu when opened attaches a container view `_UIContextMenuContainerView` to the window.
|
|
11
|
+
On tapping it, React Root's `RCTSurfaceTouchHandler` also listens to the touch and causes Pressables to fire onPress.
|
|
12
|
+
*/
|
|
13
|
+
internal final class SystemMenuTouchGate: UIGestureRecognizer {
|
|
14
|
+
init() {
|
|
15
|
+
super.init(target: nil, action: nil)
|
|
16
|
+
// Both default to true. Never hold a touch back, and never let React Native's handler see a
|
|
17
|
+
// reason to cancel: it calls `_cancelTouches` for any outside recognizer that cancels in view.
|
|
18
|
+
delaysTouchesEnded = false
|
|
19
|
+
cancelsTouchesInView = false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// MARK: - Detecting an open menu
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
The one view UIKit adds to the window for a presented context menu.
|
|
26
|
+
*/
|
|
27
|
+
static func isContextMenuContainerClassName(_ className: String) -> Bool {
|
|
28
|
+
return className == "_UI".appending("ContextMenuContainerView")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
Consider Menu to be open when the container is present, still accepts interaction, and is modal.
|
|
33
|
+
UIKit turns `isUserInteractionEnabled` off the moment dismissal commits, so taps made during
|
|
34
|
+
the dismiss animation pass through to the app again. `accessibilityViewIsModal` is how UIKit
|
|
35
|
+
marks the container as blocking the content behind it — a semantic signal beside the class name.
|
|
36
|
+
*/
|
|
37
|
+
static func isOpenContextMenuContainer(
|
|
38
|
+
className: String,
|
|
39
|
+
isUserInteractionEnabled: Bool,
|
|
40
|
+
accessibilityViewIsModal: Bool
|
|
41
|
+
) -> Bool {
|
|
42
|
+
return isUserInteractionEnabled && accessibilityViewIsModal && isContextMenuContainerClassName(className)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static func isContextMenuContainer(_ view: UIView) -> Bool {
|
|
46
|
+
return isOpenContextMenuContainer(
|
|
47
|
+
className: NSStringFromClass(type(of: view)),
|
|
48
|
+
isUserInteractionEnabled: view.isUserInteractionEnabled,
|
|
49
|
+
accessibilityViewIsModal: view.accessibilityViewIsModal
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
UIKit adds the container as a direct subview of the window, above the app's content, so only the
|
|
55
|
+
window's own subviews are worth checking. Anything deeper belongs to the app itself.
|
|
56
|
+
*/
|
|
57
|
+
static func isShowingContextMenu(
|
|
58
|
+
in window: UIWindow,
|
|
59
|
+
isContainer: @MainActor (UIView) -> Bool = SystemMenuTouchGate.isContextMenuContainer
|
|
60
|
+
) -> Bool {
|
|
61
|
+
for subview in window.subviews where isContainer(subview) {
|
|
62
|
+
return true
|
|
63
|
+
}
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// MARK: - Cancelling React Native's touches
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
Matched by name so that this file needs no React Native headers.
|
|
71
|
+
*/
|
|
72
|
+
static func isSurfaceTouchHandlerClassName(_ className: String) -> Bool {
|
|
73
|
+
return className == "RCTSurfaceTouchHandler"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static func isSurfaceTouchHandler(_ recognizer: UIGestureRecognizer) -> Bool {
|
|
77
|
+
return isSurfaceTouchHandlerClassName(NSStringFromClass(type(of: recognizer)))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
React Native's touch handlers along the touched view's superview chain.
|
|
82
|
+
*/
|
|
83
|
+
static func surfaceTouchHandlers(
|
|
84
|
+
above view: UIView?,
|
|
85
|
+
isSurfaceTouchHandler: @MainActor (UIGestureRecognizer) -> Bool = SystemMenuTouchGate.isSurfaceTouchHandler
|
|
86
|
+
) -> [UIGestureRecognizer] {
|
|
87
|
+
var handlers: [UIGestureRecognizer] = []
|
|
88
|
+
var current = view
|
|
89
|
+
while let view = current {
|
|
90
|
+
for recognizer in view.gestureRecognizers ?? [] where isSurfaceTouchHandler(recognizer) {
|
|
91
|
+
handlers.append(recognizer)
|
|
92
|
+
}
|
|
93
|
+
current = view.superview
|
|
94
|
+
}
|
|
95
|
+
return handlers
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - Installing
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
Adds one gate to the window. The window owns it for as long as it lives, and the gate stays
|
|
102
|
+
inert until a menu is actually open, so there is nothing to tear down per host.
|
|
103
|
+
*/
|
|
104
|
+
static func install(in window: UIWindow) {
|
|
105
|
+
let isInstalled = window.gestureRecognizers?.contains { $0 is SystemMenuTouchGate } ?? false
|
|
106
|
+
guard !isInstalled else {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
window.addGestureRecognizer(SystemMenuTouchGate())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// MARK: - UIGestureRecognizer
|
|
113
|
+
|
|
114
|
+
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
115
|
+
super.touchesBegan(touches, with: event)
|
|
116
|
+
|
|
117
|
+
if let window = view as? UIWindow, Self.isShowingContextMenu(in: window) {
|
|
118
|
+
for touch in touches {
|
|
119
|
+
for handler in Self.surfaceTouchHandlers(above: touch.view) {
|
|
120
|
+
handler.ignore(touch, for: event)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
state = .failed
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
#endif
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
#if os(iOS) || os(tvOS)
|
|
4
|
+
|
|
5
|
+
import UIKit
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
Geometry lookups that start from the view asking the question, because iOS 27 makes iPhone apps
|
|
9
|
+
freely resizable. Neither `UIScreen.main` nor the first key window in any connected scene describes
|
|
10
|
+
the space a given view has, since either can belong to a differently sized scene.
|
|
11
|
+
*/
|
|
12
|
+
public enum SceneGeometry {
|
|
13
|
+
public static func windowScene(for view: UIView? = nil) -> UIWindowScene? {
|
|
14
|
+
if let scene = view?.window?.windowScene {
|
|
15
|
+
return scene
|
|
16
|
+
}
|
|
17
|
+
return foregroundScene() ?? windowScenes().first
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Deliberately strict, for callers deciding whether to attach a window to a scene at all.
|
|
21
|
+
public static func foregroundActiveScene() -> UIWindowScene? {
|
|
22
|
+
return windowScenes().first { $0.activationState == .foregroundActive }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Nil when no scene is on screen, so callers don't present UI into a background scene.
|
|
26
|
+
public static func foregroundScene() -> UIWindowScene? {
|
|
27
|
+
let scenes = windowScenes()
|
|
28
|
+
return scenes.first { $0.activationState == .foregroundActive }
|
|
29
|
+
?? scenes.first { $0.activationState == .foregroundInactive }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private static func windowScenes() -> [UIWindowScene] {
|
|
33
|
+
return UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public static func keyWindow(for view: UIView? = nil) -> UIWindow? {
|
|
37
|
+
guard let scene = windowScene(for: view) else {
|
|
38
|
+
return nil
|
|
39
|
+
}
|
|
40
|
+
return scene.windows.first { $0.isKeyWindow } ?? scene.windows.first
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static func bounds(for view: UIView? = nil) -> CGRect {
|
|
44
|
+
return (view?.window ?? keyWindow(for: view))?.bounds ?? .zero
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static func safeAreaSize(for view: UIView? = nil) -> CGSize {
|
|
48
|
+
guard let window = view?.window ?? keyWindow(for: view) else {
|
|
49
|
+
return .zero
|
|
50
|
+
}
|
|
51
|
+
let safeArea = window.safeAreaLayoutGuide.layoutFrame.size
|
|
52
|
+
if safeArea.width > 0 && safeArea.height > 0 {
|
|
53
|
+
return safeArea
|
|
54
|
+
}
|
|
55
|
+
return window.bounds.size
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Never cache the result. Scale can differ per scene.
|
|
59
|
+
public static func displayScale(for view: UIView? = nil) -> CGFloat {
|
|
60
|
+
return resolveDisplayScale(candidates: [
|
|
61
|
+
view?.traitCollection.displayScale,
|
|
62
|
+
windowScene(for: view)?.traitCollection.displayScale,
|
|
63
|
+
UITraitCollection.current.displayScale
|
|
64
|
+
])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Returns the first positive candidate. `UITraitCollection` reports an unspecified scale as 0.
|
|
68
|
+
internal static func resolveDisplayScale(candidates: [CGFloat?]) -> CGFloat {
|
|
69
|
+
for case let candidate? in candidates where candidate > 0 {
|
|
70
|
+
return candidate
|
|
71
|
+
}
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#if os(iOS)
|
|
77
|
+
public extension SceneGeometry {
|
|
78
|
+
/// Reads `effectiveGeometry` because `UIWindowScene.interfaceOrientation` is deprecated as of iOS 26.
|
|
79
|
+
static func interfaceOrientation(for view: UIView? = nil) -> UIInterfaceOrientation {
|
|
80
|
+
return windowScene(for: view)?.effectiveGeometry.interfaceOrientation ?? .unknown
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
#endif
|
|
84
|
+
|
|
85
|
+
#endif
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-core",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.24",
|
|
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": "39f9dc47fb5ddd099a2533dd0ff24be7126a1ed0",
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "expo-module build",
|
|
72
72
|
"clean": "expo-module clean",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|