expo-dev-menu 55.0.3 → 55.0.4
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 +10 -0
- package/android/build.gradle +2 -2
- package/android/src/debug/java/expo/modules/devmenu/DevMenuFragment.kt +13 -7
- package/android/src/debug/java/expo/modules/devmenu/detectors/{TouchInterceptingWindowCallback.kt → InterceptingWindowCallback.kt} +23 -6
- package/android/src/debug/java/expo/modules/devmenu/fab/FloatingActionButtonContent.kt +2 -2
- package/android/src/debug/java/expo/modules/devmenu/fab/MovableFloatingActionButton.kt +0 -1
- package/expo-dev-menu.podspec +6 -0
- package/ios/Assets.xcassets/Contents.json +6 -0
- package/ios/Assets.xcassets/dev-tools.imageset/Contents.json +21 -0
- package/ios/Assets.xcassets/dev-tools.imageset/dev-tools.png +0 -0
- package/ios/DevMenuManager.swift +47 -14
- package/ios/DevMenuViewController.swift +5 -0
- package/ios/DevMenuWindow-default.swift +3 -2
- package/ios/FAB/DevMenuFABView.swift +108 -21
- package/ios/ReactDelegateHandler/ExpoDevMenuReactDelegateHandler.swift +1 -6
- package/ios/SwiftUI/DevMenuBundle.swift +11 -0
- package/ios/SwiftUI/DevMenuDeveloperTools.swift +26 -24
- package/ios/SwiftUI/DevMenuMainView.swift +1 -1
- package/ios/SwiftUI/DevMenuOnboardingView.swift +43 -22
- package/ios/SwiftUI/HeaderView.swift +9 -5
- package/ios/SwiftUI/HermesDebuggerTip.swift +0 -4
- package/ios/SwiftUI/SourceMapExplorerView.swift +308 -85
- package/ios/SwiftUI/SourceMapExplorerViewModel.swift +0 -10
- package/ios/SwiftUI/SourceMapService.swift +12 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 55.0.4 — 2026-02-03
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [Android] Fix `null cannot be cast to non-null type expo.modules.devmenu.DevMenuFragment`. ([#42660](https://github.com/expo/expo/pull/42660) by [@lukmccall](https://github.com/lukmccall))
|
|
18
|
+
- [iOS] Fix null current bridge in standalone mode ([#42666](https://github.com/expo/expo/pull/42666) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
|
19
|
+
- [Android] Fix `cmd + m` not opening the dev menu. ([#42701](https://github.com/expo/expo/pull/42701) by [@lukmccall](https://github.com/lukmccall))
|
|
20
|
+
- [iOS] Restore dev menu on tvOS. ([#42737](https://github.com/expo/expo/pull/42737) by [@douglowder](https://github.com/douglowder))
|
|
21
|
+
|
|
13
22
|
## 55.0.3 — 2026-01-27
|
|
14
23
|
|
|
15
24
|
### 🎉 New features
|
|
@@ -21,6 +30,7 @@
|
|
|
21
30
|
|
|
22
31
|
- [iOS] Fix tvOS compilation errors in Source Map Explorer by excluding tvOS from unavailable SwiftUI APIs (`navigationBarTitleDisplayMode`, `listStyle(.insetGrouped)`, `Menu`) and adding proper availability checks. Refactored platform-specific modifiers to use `@ViewBuilder` instead of `AnyView` for better type preservation and performance. ([#42574](https://github.com/expo/expo/pull/42574) by [@OtavioStasiak](https://github.com/OtavioStasiak))
|
|
23
32
|
- [Android] Fix dev menu not opening when pressing m key in CLI. ([#42566](https://github.com/expo/expo/pull/42566) by [@lukmccall](https://github.com/lukmccall))
|
|
33
|
+
- [Quest] Fix Floating action button not responding to presses. ([#42563](https://github.com/expo/expo/pull/42563) by [@behenate](https://github.com/behenate))
|
|
24
34
|
|
|
25
35
|
### 💡 Others
|
|
26
36
|
|
package/android/build.gradle
CHANGED
|
@@ -12,7 +12,7 @@ apply plugin: 'expo-module-gradle-plugin'
|
|
|
12
12
|
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
|
13
13
|
|
|
14
14
|
group = 'host.exp.exponent'
|
|
15
|
-
version = '55.0.
|
|
15
|
+
version = '55.0.4'
|
|
16
16
|
|
|
17
17
|
def hasDevLauncher = findProject(":expo-dev-launcher") != null
|
|
18
18
|
def configureInRelease = findProperty("expo.devmenu.configureInRelease") == "true"
|
|
@@ -29,7 +29,7 @@ android {
|
|
|
29
29
|
|
|
30
30
|
defaultConfig {
|
|
31
31
|
versionCode 10
|
|
32
|
-
versionName '55.0.
|
|
32
|
+
versionName '55.0.4'
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
buildTypes {
|
|
@@ -28,9 +28,9 @@ import expo.modules.devmenu.compose.DevMenuState
|
|
|
28
28
|
import expo.modules.devmenu.compose.DevMenuViewModel
|
|
29
29
|
import expo.modules.devmenu.compose.newtheme.AppTheme
|
|
30
30
|
import expo.modules.devmenu.compose.ui.DevMenuBottomSheet
|
|
31
|
+
import expo.modules.devmenu.detectors.InterceptingWindowCallback
|
|
31
32
|
import expo.modules.devmenu.detectors.ShakeDetector
|
|
32
33
|
import expo.modules.devmenu.detectors.ThreeFingerLongPressDetector
|
|
33
|
-
import expo.modules.devmenu.detectors.TouchInterceptingWindowCallback
|
|
34
34
|
import expo.modules.devmenu.devtools.DevMenuDevToolsDelegate
|
|
35
35
|
import expo.modules.devmenu.fab.MovableFloatingActionButton
|
|
36
36
|
import expo.modules.devmenu.helpers.isAcceptingText
|
|
@@ -118,23 +118,29 @@ class DevMenuFragment(
|
|
|
118
118
|
|
|
119
119
|
// Wrap window callback to intercept touch events at the window level
|
|
120
120
|
activity?.window?.let { window ->
|
|
121
|
-
val
|
|
121
|
+
val fingerLongPressDetector = ThreeFingerLongPressDetector(
|
|
122
122
|
lifecycleScope,
|
|
123
123
|
::onThreeFingerLongPressDetected
|
|
124
124
|
)
|
|
125
|
+
val weakSelf = this.weak()
|
|
126
|
+
val keyEventDispatcher = { event: KeyEvent ->
|
|
127
|
+
weakSelf.get()?.onKeyUp(event.keyCode, event) ?: false
|
|
128
|
+
}
|
|
125
129
|
|
|
126
130
|
val currentCallback = window.callback
|
|
127
131
|
// Avoid wrapping multiple times
|
|
128
|
-
if (currentCallback !is
|
|
132
|
+
if (currentCallback !is InterceptingWindowCallback) {
|
|
129
133
|
originalWindowCallback = currentCallback
|
|
130
134
|
window.callback =
|
|
131
|
-
|
|
135
|
+
InterceptingWindowCallback(
|
|
132
136
|
currentCallback,
|
|
133
|
-
|
|
137
|
+
fingerLongPressDetector,
|
|
138
|
+
keyEventDispatcher
|
|
134
139
|
)
|
|
135
140
|
} else {
|
|
136
141
|
// When user reloads the app, the fragment is restarted but the window callback remains the same
|
|
137
|
-
currentCallback.updateDetector(
|
|
142
|
+
currentCallback.updateDetector(fingerLongPressDetector)
|
|
143
|
+
currentCallback.updateKeyEventDispatcher(keyEventDispatcher)
|
|
138
144
|
}
|
|
139
145
|
}
|
|
140
146
|
}
|
|
@@ -312,7 +318,7 @@ class DevMenuFragment(
|
|
|
312
318
|
|
|
313
319
|
internal fun findIn(activity: Activity?): DevMenuFragment? {
|
|
314
320
|
val activity = activity ?: return null
|
|
315
|
-
return (activity as FragmentActivity).supportFragmentManager.findFragmentByTag(TAG) as DevMenuFragment
|
|
321
|
+
return (activity as FragmentActivity).supportFragmentManager.findFragmentByTag(TAG) as? DevMenuFragment
|
|
316
322
|
}
|
|
317
323
|
|
|
318
324
|
private data class KeyCommand(val code: Int, val withShift: Boolean = false)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package expo.modules.devmenu.detectors
|
|
2
2
|
|
|
3
3
|
import android.os.Build
|
|
4
|
+
import android.view.KeyEvent
|
|
4
5
|
import android.view.KeyboardShortcutGroup
|
|
5
6
|
import android.view.Menu
|
|
6
7
|
import android.view.MotionEvent
|
|
@@ -8,23 +9,39 @@ import android.view.Window
|
|
|
8
9
|
import androidx.annotation.RequiresApi
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* A [Window.Callback] that intercepts touch events to detect three-finger long press gestures
|
|
12
|
+
* A [Window.Callback] that intercepts touch events to detect three-finger long press gestures and
|
|
13
|
+
* dispatchKeyEvent to detect cmd + m.
|
|
12
14
|
* Delegates all other calls to the [wrapped] callback.
|
|
13
15
|
*/
|
|
14
|
-
internal class
|
|
16
|
+
internal class InterceptingWindowCallback(
|
|
15
17
|
private val wrapped: Window.Callback,
|
|
16
|
-
private var
|
|
18
|
+
private var threeFingerLongPressDetector: ThreeFingerLongPressDetector,
|
|
19
|
+
private var keyEventDispatcher: (KeyEvent) -> Boolean
|
|
17
20
|
) : Window.Callback by wrapped {
|
|
18
21
|
fun updateDetector(newDetector: ThreeFingerLongPressDetector) {
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
threeFingerLongPressDetector.cancelDetection()
|
|
23
|
+
threeFingerLongPressDetector = newDetector
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fun updateKeyEventDispatcher(newDispatcher: (KeyEvent) -> Boolean) {
|
|
27
|
+
keyEventDispatcher = newDispatcher
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
|
|
24
|
-
|
|
31
|
+
threeFingerLongPressDetector.onTouchEvent(event)
|
|
25
32
|
return wrapped.dispatchTouchEvent(event)
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
|
36
|
+
event?.let { event ->
|
|
37
|
+
val isMenuKey = event.keyCode == KeyEvent.KEYCODE_MENU && event.action == KeyEvent.ACTION_UP
|
|
38
|
+
if (isMenuKey) {
|
|
39
|
+
return keyEventDispatcher(event)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return wrapped.dispatchKeyEvent(event)
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
@RequiresApi(Build.VERSION_CODES.O)
|
|
29
46
|
override fun onPointerCaptureChanged(hasCapture: Boolean) {
|
|
30
47
|
wrapped.onPointerCaptureChanged(hasCapture)
|
|
@@ -86,16 +86,16 @@ fun FloatingActionButtonContent(
|
|
|
86
86
|
interactionSource = interactionSource,
|
|
87
87
|
indication = null,
|
|
88
88
|
onClick = {
|
|
89
|
-
onRefreshPress()
|
|
90
89
|
scope.launch {
|
|
91
90
|
animatedRotation.animateTo(
|
|
92
91
|
targetValue = 360f,
|
|
93
92
|
animationSpec = spring(
|
|
94
93
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
|
95
94
|
stiffness = Spring.StiffnessVeryLow,
|
|
96
|
-
visibilityThreshold =
|
|
95
|
+
visibilityThreshold = 5f
|
|
97
96
|
)
|
|
98
97
|
)
|
|
98
|
+
onRefreshPress()
|
|
99
99
|
animatedRotation.snapTo(0f)
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -123,7 +123,6 @@ fun MovableFloatingActionButton(
|
|
|
123
123
|
.coerceIn(maxX = bounds.x, maxY = bounds.y)
|
|
124
124
|
dragDistance += change.positionChange().getDistance()
|
|
125
125
|
velocityTracker.registerPosition(dragOffset.x, dragOffset.y)
|
|
126
|
-
change.consume()
|
|
127
126
|
|
|
128
127
|
if (dragDistance > ClickDragTolerance) {
|
|
129
128
|
launch {
|
package/expo-dev-menu.podspec
CHANGED
|
@@ -35,6 +35,12 @@ Pod::Spec.new do |s|
|
|
|
35
35
|
s.requires_arc = true
|
|
36
36
|
s.header_dir = 'EXDevMenu'
|
|
37
37
|
|
|
38
|
+
s.resource_bundles = {
|
|
39
|
+
'EXDevMenu' => [
|
|
40
|
+
'ios/Assets.xcassets',
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => 'EX_DEV_MENU_ENABLED=1', 'OTHER_SWIFT_FLAGS' => '-DEX_DEV_MENU_ENABLED' }
|
|
39
45
|
|
|
40
46
|
s.user_target_xcconfig = {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"images" : [
|
|
3
|
+
{
|
|
4
|
+
"filename" : "dev-tools.png",
|
|
5
|
+
"idiom" : "universal",
|
|
6
|
+
"scale" : "1x"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"idiom" : "universal",
|
|
10
|
+
"scale" : "2x"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idiom" : "universal",
|
|
14
|
+
"scale" : "3x"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"info" : {
|
|
18
|
+
"author" : "xcode",
|
|
19
|
+
"version" : 1
|
|
20
|
+
}
|
|
21
|
+
}
|
|
Binary file
|
package/ios/DevMenuManager.swift
CHANGED
|
@@ -76,6 +76,8 @@ open class DevMenuManager: NSObject {
|
|
|
76
76
|
|
|
77
77
|
static public var wasInitilized = false
|
|
78
78
|
|
|
79
|
+
private var contentDidAppearObserver: NSObjectProtocol?
|
|
80
|
+
|
|
79
81
|
/**
|
|
80
82
|
Shared singleton instance.
|
|
81
83
|
*/
|
|
@@ -99,6 +101,8 @@ open class DevMenuManager: NSObject {
|
|
|
99
101
|
|
|
100
102
|
var currentScreen: String?
|
|
101
103
|
|
|
104
|
+
private var isNavigatingHome = false
|
|
105
|
+
|
|
102
106
|
weak var hostDelegate: DevMenuHostDelegate?
|
|
103
107
|
|
|
104
108
|
@objc
|
|
@@ -109,14 +113,32 @@ open class DevMenuManager: NSObject {
|
|
|
109
113
|
if let currentBridge {
|
|
110
114
|
DispatchQueue.main.async {
|
|
111
115
|
self.disableRNDevMenuHoykeys(for: currentBridge)
|
|
112
|
-
self.updateFABVisibility()
|
|
113
116
|
}
|
|
117
|
+
observeContentDidAppear()
|
|
114
118
|
} else {
|
|
115
119
|
updateFABVisibility()
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
private func observeContentDidAppear() {
|
|
125
|
+
if let observer = contentDidAppearObserver {
|
|
126
|
+
NotificationCenter.default.removeObserver(observer)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
contentDidAppearObserver = NotificationCenter.default.addObserver(
|
|
130
|
+
forName: NSNotification.Name.RCTContentDidAppear,
|
|
131
|
+
object: nil,
|
|
132
|
+
queue: .main
|
|
133
|
+
) { [weak self] _ in
|
|
134
|
+
self?.updateFABVisibility()
|
|
135
|
+
if let observer = self?.contentDidAppearObserver {
|
|
136
|
+
NotificationCenter.default.removeObserver(observer)
|
|
137
|
+
self?.contentDidAppearObserver = nil
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
private let manifestSubject = PassthroughSubject<Void, Never>()
|
|
121
143
|
public var manifestPublisher: AnyPublisher<Void, Never> {
|
|
122
144
|
manifestSubject.eraseToAnyPublisher()
|
|
@@ -166,9 +188,17 @@ open class DevMenuManager: NSObject {
|
|
|
166
188
|
return DevMenuPreferences.touchGestureEnabled
|
|
167
189
|
}
|
|
168
190
|
|
|
191
|
+
@objc
|
|
192
|
+
public func setShowFloatingActionButton(_ enabled: Bool) {
|
|
193
|
+
DevMenuPreferences.showFloatingActionButton = enabled
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
@objc
|
|
170
197
|
public func updateCurrentBridge(_ bridge: RCTBridge?) {
|
|
171
198
|
currentBridge = bridge
|
|
199
|
+
if bridge != nil {
|
|
200
|
+
isNavigatingHome = false
|
|
201
|
+
}
|
|
172
202
|
}
|
|
173
203
|
|
|
174
204
|
@objc
|
|
@@ -283,11 +313,7 @@ open class DevMenuManager: NSObject {
|
|
|
283
313
|
@objc
|
|
284
314
|
@discardableResult
|
|
285
315
|
public func hideMenu() -> Bool {
|
|
286
|
-
|
|
287
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
288
|
-
self?.updateFABVisibility()
|
|
289
|
-
}
|
|
290
|
-
return result
|
|
316
|
+
return setVisibility(false)
|
|
291
317
|
}
|
|
292
318
|
|
|
293
319
|
/**
|
|
@@ -323,6 +349,12 @@ open class DevMenuManager: NSObject {
|
|
|
323
349
|
return
|
|
324
350
|
}
|
|
325
351
|
|
|
352
|
+
isNavigatingHome = true
|
|
353
|
+
|
|
354
|
+
#if !os(macOS) && !os(tvOS)
|
|
355
|
+
fabWindow?.setVisible(false, animated: false)
|
|
356
|
+
#endif
|
|
357
|
+
|
|
326
358
|
let action: () -> Void = {
|
|
327
359
|
delegate.devMenuNavigateHome?()
|
|
328
360
|
}
|
|
@@ -414,6 +446,8 @@ open class DevMenuManager: NSObject {
|
|
|
414
446
|
DispatchQueue.main.async {
|
|
415
447
|
#if os(macOS)
|
|
416
448
|
self.window?.makeKeyAndOrderFront(nil)
|
|
449
|
+
#elseif os(tvOS)
|
|
450
|
+
self.window?.makeKeyAndVisible()
|
|
417
451
|
#else
|
|
418
452
|
self.updateFABVisibility()
|
|
419
453
|
|
|
@@ -428,9 +462,6 @@ open class DevMenuManager: NSObject {
|
|
|
428
462
|
} else {
|
|
429
463
|
DispatchQueue.main.async {
|
|
430
464
|
self.window?.closeBottomSheet(nil)
|
|
431
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
432
|
-
self.updateFABVisibility()
|
|
433
|
-
}
|
|
434
465
|
}
|
|
435
466
|
}
|
|
436
467
|
return true
|
|
@@ -516,8 +547,7 @@ open class DevMenuManager: NSObject {
|
|
|
516
547
|
fabWindow = DevMenuFABWindow(manager: self, windowScene: windowScene)
|
|
517
548
|
}
|
|
518
549
|
|
|
519
|
-
|
|
520
|
-
public func updateFABVisibility() {
|
|
550
|
+
public func updateFABVisibility(menuDismissing: Bool = false) {
|
|
521
551
|
DispatchQueue.main.async { [weak self] in
|
|
522
552
|
guard let self = self else { return }
|
|
523
553
|
|
|
@@ -528,13 +558,16 @@ open class DevMenuManager: NSObject {
|
|
|
528
558
|
}
|
|
529
559
|
}
|
|
530
560
|
|
|
531
|
-
let shouldShow = DevMenuPreferences.showFloatingActionButton
|
|
561
|
+
let shouldShow = DevMenuPreferences.showFloatingActionButton
|
|
562
|
+
&& (menuDismissing || !self.isVisible)
|
|
563
|
+
&& self.currentBridge != nil
|
|
564
|
+
&& !self.isNavigatingHome
|
|
565
|
+
&& DevMenuPreferences.isOnboardingFinished
|
|
532
566
|
self.fabWindow?.setVisible(shouldShow, animated: true)
|
|
533
567
|
}
|
|
534
568
|
}
|
|
535
569
|
#else
|
|
536
|
-
|
|
537
|
-
public func updateFABVisibility() {
|
|
570
|
+
public func updateFABVisibility(menuDismissing: Bool = false) {
|
|
538
571
|
// FAB not available on macOS/tvOS
|
|
539
572
|
}
|
|
540
573
|
#endif
|
|
@@ -46,7 +46,12 @@ class DevMenuViewController: UIViewController {
|
|
|
46
46
|
let rootView = DevMenuRootView()
|
|
47
47
|
let hostingController = UIHostingController(rootView: rootView)
|
|
48
48
|
|
|
49
|
+
#if os(tvOS)
|
|
50
|
+
hostingController.view.backgroundColor =
|
|
51
|
+
preferredUserInterfaceStyle == .dark ? UIColor.systemGray.withAlphaComponent(0.8) : UIColor.white.withAlphaComponent(0.8)
|
|
52
|
+
#else
|
|
49
53
|
hostingController.view.backgroundColor = UIColor.clear
|
|
54
|
+
#endif
|
|
50
55
|
|
|
51
56
|
addChild(hostingController)
|
|
52
57
|
view.addSubview(hostingController.view)
|
|
@@ -26,7 +26,7 @@ class DevMenuWindow: UIWindow, PresentationControllerDelegate {
|
|
|
26
26
|
self.rootViewController = UIViewController()
|
|
27
27
|
self.backgroundColor = UIColor(white: 0, alpha: 0.4)
|
|
28
28
|
#if os(tvOS)
|
|
29
|
-
self.windowLevel = .normal
|
|
29
|
+
self.windowLevel = .normal + 1
|
|
30
30
|
#else
|
|
31
31
|
self.windowLevel = .statusBar
|
|
32
32
|
#endif
|
|
@@ -103,11 +103,12 @@ class DevMenuWindow: UIWindow, PresentationControllerDelegate {
|
|
|
103
103
|
self.backgroundColor = .clear
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
DevMenuManager.shared.updateFABVisibility(menuDismissing: true)
|
|
107
|
+
|
|
106
108
|
devMenuViewController.dismiss(animated: true) {
|
|
107
109
|
self.isDismissing = false
|
|
108
110
|
self.isHidden = true
|
|
109
111
|
self.backgroundColor = UIColor(white: 0, alpha: 0.4)
|
|
110
|
-
DevMenuManager.shared.updateFABVisibility()
|
|
111
112
|
completion?()
|
|
112
113
|
}
|
|
113
114
|
}
|
|
@@ -7,9 +7,12 @@ import SwiftUI
|
|
|
7
7
|
enum FABConstants {
|
|
8
8
|
static let iconSize: CGFloat = 44
|
|
9
9
|
static let margin: CGFloat = 16
|
|
10
|
-
static let
|
|
10
|
+
static let verticalPadding: CGFloat = 20
|
|
11
|
+
static let dragThreshold: CGFloat = 10
|
|
11
12
|
static let momentumFactor: CGFloat = 0.35
|
|
12
13
|
static let labelDismissDelay: TimeInterval = 10
|
|
14
|
+
static let idleTimeout: UInt64 = 5_000_000_000
|
|
15
|
+
static let imageSize: CGFloat = 26
|
|
13
16
|
|
|
14
17
|
static let snapAnimation: Animation = .spring(
|
|
15
18
|
response: 0.6,
|
|
@@ -20,23 +23,33 @@ enum FABConstants {
|
|
|
20
23
|
|
|
21
24
|
struct FabPill: View {
|
|
22
25
|
@Binding var isPressed: Bool
|
|
26
|
+
@Binding var isDragging: Bool
|
|
23
27
|
@State private var showLabel = true
|
|
28
|
+
@State private var isIdle = false
|
|
29
|
+
@State private var idleTask: Task<Void, Never>?
|
|
30
|
+
|
|
31
|
+
private var isInteracting: Bool {
|
|
32
|
+
isPressed || isDragging
|
|
33
|
+
}
|
|
24
34
|
|
|
25
35
|
var body: some View {
|
|
26
|
-
VStack(spacing:
|
|
27
|
-
|
|
28
|
-
.font(.system(size: 20))
|
|
29
|
-
.foregroundStyle(.white)
|
|
30
|
-
.frame(width: FABConstants.iconSize, height: FABConstants.iconSize)
|
|
31
|
-
.background(Color.blue, in: Circle())
|
|
32
|
-
.overlay(
|
|
33
|
-
Circle()
|
|
34
|
-
.stroke(Color.blue.opacity(0.5), lineWidth: 4)
|
|
35
|
-
.frame(width: FABConstants.iconSize + 4, height: FABConstants.iconSize + 4)
|
|
36
|
-
)
|
|
37
|
-
.opacity(isPressed ? 0.8 : 1.0)
|
|
36
|
+
VStack(spacing: 8) {
|
|
37
|
+
actionButton
|
|
38
38
|
.scaleEffect(isPressed ? 0.9 : 1.0)
|
|
39
39
|
.animation(.easeInOut(duration: 0.1), value: isPressed)
|
|
40
|
+
.onChange(of: isInteracting) { interacting in
|
|
41
|
+
if interacting {
|
|
42
|
+
idleTask?.cancel()
|
|
43
|
+
withAnimation {
|
|
44
|
+
isIdle = false
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
startIdleTimer()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
.onAppear {
|
|
51
|
+
startIdleTimer()
|
|
52
|
+
}
|
|
40
53
|
|
|
41
54
|
if showLabel {
|
|
42
55
|
Text("Dev tools")
|
|
@@ -49,7 +62,12 @@ struct FabPill: View {
|
|
|
49
62
|
.transition(.opacity.combined(with: .scale(scale: 0.8)))
|
|
50
63
|
}
|
|
51
64
|
}
|
|
65
|
+
.saturation(isIdle ? 0 : 1)
|
|
66
|
+
.opacity(isIdle ? 0.5 : 1)
|
|
67
|
+
.animation(.easeInOut(duration: 0.3), value: isIdle)
|
|
52
68
|
.task {
|
|
69
|
+
// [Alan] This is poor practice but without it, the label is not included in the drag gesture
|
|
70
|
+
// and remains in it's original posistion.
|
|
53
71
|
try? await Task.sleep(nanoseconds: UInt64(1_000_000_000 * FABConstants.labelDismissDelay))
|
|
54
72
|
await MainActor.run {
|
|
55
73
|
withAnimation(.easeOut(duration: 0.3)) {
|
|
@@ -58,19 +76,78 @@ struct FabPill: View {
|
|
|
58
76
|
}
|
|
59
77
|
}
|
|
60
78
|
}
|
|
79
|
+
|
|
80
|
+
@ViewBuilder
|
|
81
|
+
private var actionButton: some View {
|
|
82
|
+
if #available(iOS 26.0, *) {
|
|
83
|
+
liquidGlassButton
|
|
84
|
+
} else {
|
|
85
|
+
classicButton
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private func startIdleTimer() {
|
|
90
|
+
idleTask?.cancel()
|
|
91
|
+
idleTask = Task {
|
|
92
|
+
try? await Task.sleep(nanoseconds: FABConstants.idleTimeout)
|
|
93
|
+
guard !Task.isCancelled else { return }
|
|
94
|
+
await MainActor.run {
|
|
95
|
+
withAnimation {
|
|
96
|
+
isIdle = true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@available(iOS 26.0, *)
|
|
103
|
+
private var liquidGlassButton: some View {
|
|
104
|
+
Image(systemName: "gearshape.fill")
|
|
105
|
+
.resizable()
|
|
106
|
+
.frame(width: FABConstants.imageSize, height: FABConstants.imageSize)
|
|
107
|
+
.foregroundStyle(.white)
|
|
108
|
+
.frame(width: FABConstants.iconSize, height: FABConstants.iconSize)
|
|
109
|
+
.background(
|
|
110
|
+
Circle()
|
|
111
|
+
.frame(width: FABConstants.iconSize + 10, height: FABConstants.iconSize + 10)
|
|
112
|
+
.glassEffect(.clear, in: Circle())
|
|
113
|
+
)
|
|
114
|
+
.shadow(color: .black.opacity(0.4), radius: 8, x: 0, y: 4)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private var classicButton: some View {
|
|
118
|
+
Image(systemName: "gearshape.fill")
|
|
119
|
+
.resizable()
|
|
120
|
+
.frame(width: FABConstants.imageSize, height: FABConstants.imageSize)
|
|
121
|
+
.foregroundStyle(.white)
|
|
122
|
+
.frame(width: FABConstants.iconSize, height: FABConstants.iconSize)
|
|
123
|
+
.background(Color.blue, in: Circle())
|
|
124
|
+
.background(
|
|
125
|
+
Circle()
|
|
126
|
+
.stroke(Color.blue.opacity(0.5), lineWidth: 4)
|
|
127
|
+
.frame(width: FABConstants.iconSize + 4, height: FABConstants.iconSize + 4)
|
|
128
|
+
)
|
|
129
|
+
.shadow(color: .black.opacity(0.4), radius: 8, x: 0, y: 4)
|
|
130
|
+
}
|
|
61
131
|
}
|
|
62
132
|
|
|
63
133
|
struct DevMenuFABView: View {
|
|
64
134
|
let onOpenMenu: () -> Void
|
|
65
135
|
let onFrameChange: (CGRect) -> Void
|
|
66
136
|
|
|
67
|
-
private let fabSize = CGSize(width: 72, height: FABConstants.iconSize +
|
|
137
|
+
private let fabSize = CGSize(width: 72, height: FABConstants.iconSize + 50)
|
|
68
138
|
|
|
69
139
|
@State private var position: CGPoint = .zero
|
|
140
|
+
@State private var targetPosition: CGPoint = .zero
|
|
70
141
|
@State private var isDragging = false
|
|
71
142
|
@State private var isPressed = false
|
|
72
143
|
@State private var dragStartPosition: CGPoint = .zero
|
|
73
144
|
|
|
145
|
+
private let dragSpring: Animation = .spring(
|
|
146
|
+
response: 0.35,
|
|
147
|
+
dampingFraction: 0.35,
|
|
148
|
+
blendDuration: 0
|
|
149
|
+
)
|
|
150
|
+
|
|
74
151
|
private var currentFrame: CGRect {
|
|
75
152
|
CGRect(origin: position, size: fabSize)
|
|
76
153
|
}
|
|
@@ -78,7 +155,8 @@ struct DevMenuFABView: View {
|
|
|
78
155
|
var body: some View {
|
|
79
156
|
GeometryReader { geometry in
|
|
80
157
|
let safeArea = geometry.safeAreaInsets
|
|
81
|
-
|
|
158
|
+
|
|
159
|
+
FabPill(isPressed: $isPressed, isDragging: $isDragging)
|
|
82
160
|
.frame(width: fabSize.width, height: fabSize.height)
|
|
83
161
|
.position(x: currentFrame.midX, y: currentFrame.midY)
|
|
84
162
|
.gesture(dragGesture(bounds: geometry.size, safeArea: safeArea))
|
|
@@ -97,7 +175,7 @@ struct DevMenuFABView: View {
|
|
|
97
175
|
position = newPos
|
|
98
176
|
onFrameChange(CGRect(origin: newPos, size: fabSize))
|
|
99
177
|
}
|
|
100
|
-
.animation(isDragging ?
|
|
178
|
+
.animation(isDragging ? dragSpring : FABConstants.snapAnimation, value: position)
|
|
101
179
|
}
|
|
102
180
|
.ignoresSafeArea()
|
|
103
181
|
}
|
|
@@ -114,9 +192,18 @@ struct DevMenuFABView: View {
|
|
|
114
192
|
dragStartPosition = position
|
|
115
193
|
}
|
|
116
194
|
if isDragging {
|
|
195
|
+
let margin = FABConstants.margin
|
|
196
|
+
let minX = margin
|
|
197
|
+
let maxX = bounds.width - fabSize.width - margin
|
|
198
|
+
let minY = margin + safeArea.top + FABConstants.verticalPadding
|
|
199
|
+
let maxY = bounds.height - fabSize.height - margin - safeArea.bottom - FABConstants.verticalPadding
|
|
200
|
+
|
|
201
|
+
let rawX = dragStartPosition.x + value.translation.width
|
|
202
|
+
let rawY = dragStartPosition.y + value.translation.height
|
|
203
|
+
|
|
117
204
|
position = CGPoint(
|
|
118
|
-
x:
|
|
119
|
-
y:
|
|
205
|
+
x: rawX.clamped(to: minX...maxX),
|
|
206
|
+
y: rawY.clamped(to: minY...maxY)
|
|
120
207
|
)
|
|
121
208
|
onFrameChange(currentFrame)
|
|
122
209
|
}
|
|
@@ -153,7 +240,7 @@ struct DevMenuFABView: View {
|
|
|
153
240
|
private func defaultPosition(bounds: CGSize, safeArea: EdgeInsets) -> CGPoint {
|
|
154
241
|
CGPoint(
|
|
155
242
|
x: bounds.width - fabSize.width - FABConstants.margin,
|
|
156
|
-
y:
|
|
243
|
+
y: bounds.height * 0.25
|
|
157
244
|
)
|
|
158
245
|
}
|
|
159
246
|
|
|
@@ -172,8 +259,8 @@ struct DevMenuFABView: View {
|
|
|
172
259
|
? margin
|
|
173
260
|
: bounds.width - self.fabSize.width - margin
|
|
174
261
|
|
|
175
|
-
let minY = margin + safeArea.top
|
|
176
|
-
let maxY = bounds.height - self.fabSize.height - margin - safeArea.bottom
|
|
262
|
+
let minY = margin + safeArea.top + FABConstants.verticalPadding
|
|
263
|
+
let maxY = bounds.height - self.fabSize.height - margin - safeArea.bottom - FABConstants.verticalPadding
|
|
177
264
|
let targetY = (point.y + momentumY).clamped(to: minY...maxY)
|
|
178
265
|
|
|
179
266
|
return CGPoint(x: targetX, y: targetY)
|
|
@@ -5,12 +5,7 @@ import ExpoModulesCore
|
|
|
5
5
|
|
|
6
6
|
@objc
|
|
7
7
|
public class ExpoDevMenuReactDelegateHandler: ExpoReactDelegateHandler {
|
|
8
|
-
public override func
|
|
9
|
-
reactDelegate: ExpoReactDelegate,
|
|
10
|
-
moduleName: String,
|
|
11
|
-
initialProperties: [AnyHashable: Any]?,
|
|
12
|
-
launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
13
|
-
) -> UIView? {
|
|
8
|
+
public override func createRootViewController() -> UIViewController? {
|
|
14
9
|
if EXAppDefines.APP_DEBUG {
|
|
15
10
|
DevMenuManager.shared.updateCurrentBridge(RCTBridge.current())
|
|
16
11
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
func getDevMenuBundle() -> Bundle? {
|
|
6
|
+
if let bundleURL = Bundle.main.url(forResource: "EXDevMenu", withExtension: "bundle"),
|
|
7
|
+
let bundle = Bundle(url: bundleURL) {
|
|
8
|
+
return bundle
|
|
9
|
+
}
|
|
10
|
+
return .main
|
|
11
|
+
}
|