@momo-kits/native-kits 0.163.1-sp.7-debug → 0.163.1-sp.8-debug
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/.claude/settings.local.json +6 -1
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/Navigation/NavigationContainer.swift +6 -3
- package/ios/Application/Navigation/Navigator.swift +5 -41
- package/ios/Application/Navigation/NavigatorManager.swift +49 -0
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/package.json +1 -1
- package/ios/Application/Navigation/SwipeBackGesture.swift +0 -116
|
@@ -147,7 +147,12 @@
|
|
|
147
147
|
"Bash(sips -c 700 1170 --cropOffset 150 0 t3.png --out t3c.png)",
|
|
148
148
|
"Bash(sips -c 1200 1170 --cropOffset 600 0 t4.png --out t4c.png)",
|
|
149
149
|
"Bash(sips -c 600 1170 --cropOffset 150 0 t5.png --out t5c.png)",
|
|
150
|
-
"Bash(sips -c 500 1170 --cropOffset 150 0 u1.png --out u1c.png)"
|
|
150
|
+
"Bash(sips -c 500 1170 --cropOffset 150 0 u1.png --out u1c.png)",
|
|
151
|
+
"Bash(cp /Users/sophia/Workspace/momo/momo-native-kits/ios/Application/Navigation/NavigationContainer.swift /Users/sophia/Workspace/momo/momo-app/node_modules/@momo-kits/native-kits/ios/Application/Navigation/NavigationContainer.swift)",
|
|
152
|
+
"Bash(cp /Users/sophia/Workspace/momo/momo-native-kits/ios/Application/Navigation/Navigator.swift /Users/sophia/Workspace/momo/momo-app/node_modules/@momo-kits/native-kits/ios/Application/Navigation/Navigator.swift)",
|
|
153
|
+
"Bash(cp /Users/sophia/Workspace/momo/momo-native-kits/ios/Application/Navigation/NavigatorManager.swift /Users/sophia/Workspace/momo/momo-app/node_modules/@momo-kits/native-kits/ios/Application/Navigation/NavigatorManager.swift)",
|
|
154
|
+
"Bash(cp /Users/sophia/Workspace/momo/momo-native-kits/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift /Users/sophia/Workspace/momo/momo-app/node_modules/@momo-kits/native-kits/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift)",
|
|
155
|
+
"Bash(xcodebuild -workspace MoMoPlatform.xcworkspace -scheme MoMoPlatform-UAT -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath build/DerivedData build)"
|
|
151
156
|
]
|
|
152
157
|
}
|
|
153
158
|
}
|
package/compose/build.gradle.kts
CHANGED
package/compose/compose.podspec
CHANGED
package/gradle.properties
CHANGED
|
@@ -92,6 +92,9 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
92
92
|
navigator.composeApi = composeApi
|
|
93
93
|
}
|
|
94
94
|
setNavigator?(navigator)
|
|
95
|
+
// Register with the host swipe-back bridge (mirrors ComposeAppContainer's
|
|
96
|
+
// ComposeNavigatorManager.push). Idempotent, so a re-appear won't double-count.
|
|
97
|
+
NavigatorManager.shared.push(navigator)
|
|
95
98
|
// AI-GENERATED START: demote the MaxApi language pull to a seed of last resort
|
|
96
99
|
// Only a seed: a Localize with a language source already tracks the host in both
|
|
97
100
|
// directions, so asking maxApi would just race it. `changeLanguage` normalizes the
|
|
@@ -102,6 +105,9 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
102
105
|
}
|
|
103
106
|
// AI-GENERATED END: demote the MaxApi language pull to a seed of last resort
|
|
104
107
|
}
|
|
108
|
+
.onDisappear {
|
|
109
|
+
NavigatorManager.shared.remove(navigator)
|
|
110
|
+
}
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
@ViewBuilder
|
|
@@ -109,9 +115,6 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
109
115
|
if let screen = navigator.registry.getScreen(id: route.id) {
|
|
110
116
|
ScreenHost(screen: screen, navigator: navigator)
|
|
111
117
|
.navigationBarBackButtonHidden(true)
|
|
112
|
-
// Hiding the back button is what kills the edge swipe; this puts it back
|
|
113
|
-
// and sends it through `onBackSafe`, like Compose's BackHandler.
|
|
114
|
-
.momoSwipeBack(navigator)
|
|
115
118
|
} else {
|
|
116
119
|
EmptyView()
|
|
117
120
|
}
|
|
@@ -90,8 +90,8 @@ public struct OverplayItem: Identifiable {
|
|
|
90
90
|
@available(iOS 16.0, *)
|
|
91
91
|
public final class Navigator: ObservableObject {
|
|
92
92
|
@Published public var path: [DynamicScreenRoute] = [] {
|
|
93
|
-
//
|
|
94
|
-
//
|
|
93
|
+
// Disposal hangs off the path so every route that leaves the stack — pop,
|
|
94
|
+
// replace, reset — is released the same way, after its exit transition.
|
|
95
95
|
didSet { disposeScreens(droppedFrom: oldValue) }
|
|
96
96
|
}
|
|
97
97
|
@Published public var presented: DynamicDialogRoute?
|
|
@@ -265,42 +265,6 @@ public final class Navigator: ObservableObject {
|
|
|
265
265
|
registry.getScreen(id: presented?.id ?? path.last?.id ?? rootRoute.id)
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
/// Whether the current screen takes back navigation over itself, i.e. back must
|
|
269
|
-
/// not pop it (mirrors the two early returns in Compose's `onBackSafe`).
|
|
270
|
-
private var currentScreenHandlesBack: Bool {
|
|
271
|
-
let options = currentScreen()?.options
|
|
272
|
-
return options?.onBackHandler != nil || options?.headerBackProps?.preventBack != nil
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/// Asked by the edge-swipe delegate before UIKit starts an interactive pop, so the
|
|
276
|
-
/// gesture ends up where Compose's `BackHandler(true) { onBackSafe() }` does. Returning
|
|
277
|
-
/// `false` means the gesture is swallowed — the side effect it stands for (closing an
|
|
278
|
-
/// overplay, running `onBackHandler`, raising the prevent-back popup) has been run here.
|
|
279
|
-
/// Returning `true` hands the pop to UIKit so the swipe stays interactive; `path.didSet`
|
|
280
|
-
/// then disposes the screen exactly as `pop` would.
|
|
281
|
-
func shouldBeginInteractivePop() -> Bool {
|
|
282
|
-
// A presented screen covers the stack; its own chrome owns the dismissal.
|
|
283
|
-
if presented != nil { return false }
|
|
284
|
-
|
|
285
|
-
if overplay?.type == .snackBar {
|
|
286
|
-
// Same as `pop`: a snackbar doesn't block back, it is cleared on the way out.
|
|
287
|
-
overplayDismissHandler = nil
|
|
288
|
-
overplay = nil
|
|
289
|
-
} else if overplay != nil {
|
|
290
|
-
// Compose: the modal/bottom-sheet BackHandler wins and plays its exit animation.
|
|
291
|
-
hideOverplay()
|
|
292
|
-
return false
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if currentScreenHandlesBack {
|
|
296
|
-
onBackSafe()
|
|
297
|
-
return false
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Nothing of ours left to pop: leave the swipe to whatever stack hosts the kit.
|
|
301
|
-
return !path.isEmpty
|
|
302
|
-
}
|
|
303
|
-
|
|
304
268
|
public func onBackSafe(callback: (() -> Void)? = nil) {
|
|
305
269
|
let latest = currentScreen()
|
|
306
270
|
if let onBack = latest?.options?.onBackHandler {
|
|
@@ -452,7 +416,7 @@ public final class Navigator: ObservableObject {
|
|
|
452
416
|
// MARK: Screen disposal
|
|
453
417
|
|
|
454
418
|
/// Matches the `delay(300)` Compose waits in `dismissScreen` before unregistering,
|
|
455
|
-
/// with headroom for the
|
|
419
|
+
/// with a little headroom for the pop transition.
|
|
456
420
|
private static let screenDisposeDelay: TimeInterval = 0.35
|
|
457
421
|
|
|
458
422
|
/// Longest overplay exit animation (the bottom sheet's), so a queued pop step runs
|
|
@@ -466,8 +430,8 @@ public final class Navigator: ObservableObject {
|
|
|
466
430
|
|
|
467
431
|
/// Screens are released only once the pop transition has played out: the outgoing
|
|
468
432
|
/// view is still on screen while it animates and the registry is what renders it —
|
|
469
|
-
/// dropping it right away blanks the screen mid-
|
|
470
|
-
/// the timer fires, so a
|
|
433
|
+
/// dropping it right away blanks the screen mid-transition. Liveness is re-checked
|
|
434
|
+
/// when the timer fires, so a route pushed back before then keeps its screen.
|
|
471
435
|
private func disposeScreens(_ ids: [Int]) {
|
|
472
436
|
guard !ids.isEmpty else { return }
|
|
473
437
|
DispatchQueue.main.asyncAfter(deadline: .now() + Self.screenDisposeDelay) { [weak self] in
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NavigatorManager.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// Host bridge for the native iOS edge-swipe back — the SwiftUI mirror of Compose's
|
|
6
|
+
// `ComposeNavigatorManager` (momo-app `vn.momo.compose.uikits.ComposeNavigatorManager`).
|
|
7
|
+
//
|
|
8
|
+
// The kit does NOT own the swipe gesture. In the host app a kit screen lives inside a
|
|
9
|
+
// `UIHostingController` hosted by the mini-app container VC, and that VC owns the
|
|
10
|
+
// `UIScreenEdgePanGestureRecognizer`. When the swipe fires the host asks this manager
|
|
11
|
+
// whether a kit stack is mounted (`checkCanPop`) and, if so, routes the back into it
|
|
12
|
+
// (`pop` → `Navigator.onBackSafe`) instead of dismissing the whole mini-app. Standalone
|
|
13
|
+
// (no such host, e.g. the demo app) there is no edge swipe — same as Compose on iOS,
|
|
14
|
+
// whose `BackHandler` is a no-op and which relies entirely on the host.
|
|
15
|
+
//
|
|
16
|
+
|
|
17
|
+
import Foundation
|
|
18
|
+
|
|
19
|
+
@available(iOS 16.0, *)
|
|
20
|
+
public final class NavigatorManager {
|
|
21
|
+
public static let shared = NavigatorManager()
|
|
22
|
+
private init() {}
|
|
23
|
+
|
|
24
|
+
/// Stack of mounted navigators; the last one is the visible top (nested mini-apps
|
|
25
|
+
/// push more than one). Held strongly, mirroring `ComposeNavigatorManager` — entries
|
|
26
|
+
/// are released by `NavigationContainer` on disappear, not by ARC.
|
|
27
|
+
private var navigators: [Navigator] = []
|
|
28
|
+
|
|
29
|
+
/// Idempotent: SwiftUI can re-run a view's `onAppear`, and a double push would need a
|
|
30
|
+
/// matching double remove to unwind.
|
|
31
|
+
public func push(_ navigator: Navigator) {
|
|
32
|
+
guard !navigators.contains(where: { $0 === navigator }) else { return }
|
|
33
|
+
navigators.append(navigator)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Removes by identity rather than `removeLast` so a spurious disappear of a covered
|
|
37
|
+
/// (non-top) container can't drop the wrong navigator.
|
|
38
|
+
public func remove(_ navigator: Navigator) {
|
|
39
|
+
navigators.removeAll { $0 === navigator }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// True while any kit stack is mounted — tells the host to route the back into the kit
|
|
43
|
+
/// instead of dismissing the mini-app. Matches `ComposeNavigatorManager.checkCanPop`:
|
|
44
|
+
/// it does not inspect stack depth; `onBackSafe` handles the at-root case itself by
|
|
45
|
+
/// asking the host to dismiss (via `composeApi`).
|
|
46
|
+
public func checkCanPop() -> Bool { !navigators.isEmpty }
|
|
47
|
+
|
|
48
|
+
public func pop() { navigators.last?.onBackSafe() }
|
|
49
|
+
}
|
package/ios/native-kits.podspec
CHANGED
package/package.json
CHANGED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// SwipeBackGesture.swift
|
|
3
|
-
// MoMoUIKits
|
|
4
|
-
//
|
|
5
|
-
// Makes the iOS edge swipe behave like Compose's `BackHandler(true) { navigator.onBackSafe() }`.
|
|
6
|
-
// Every kit screen draws its own header and therefore hides the system back button, which is
|
|
7
|
-
// exactly what makes SwiftUI turn `interactivePopGestureRecognizer` off — so the gesture is
|
|
8
|
-
// re-enabled here and its start is arbitrated by the Navigator.
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
import SwiftUI
|
|
12
|
-
import UIKit
|
|
13
|
-
|
|
14
|
-
@available(iOS 16.0, *)
|
|
15
|
-
extension View {
|
|
16
|
-
/// Re-enables the interactive pop gesture for a `NavigationStack` destination and
|
|
17
|
-
/// routes it through `navigator`. Apply it to every destination: SwiftUI reconfigures
|
|
18
|
-
/// the recognizer as screens come and go, so the install has to be repeated.
|
|
19
|
-
func momoSwipeBack(_ navigator: Navigator) -> some View {
|
|
20
|
-
background(SwipeBackInstaller(navigator: navigator).allowsHitTesting(false))
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
@available(iOS 16.0, *)
|
|
25
|
-
private struct SwipeBackInstaller: UIViewRepresentable {
|
|
26
|
-
let navigator: Navigator
|
|
27
|
-
|
|
28
|
-
func makeUIView(context: Context) -> SwipeBackProbeView {
|
|
29
|
-
let view = SwipeBackProbeView()
|
|
30
|
-
view.navigator = navigator
|
|
31
|
-
return view
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
func updateUIView(_ uiView: SwipeBackProbeView, context: Context) {
|
|
35
|
-
uiView.navigator = navigator
|
|
36
|
-
uiView.install()
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// Zero-sized view whose only job is to reach the `UINavigationController` backing the
|
|
41
|
-
/// enclosing `NavigationStack` — SwiftUI exposes no other handle on it.
|
|
42
|
-
@available(iOS 16.0, *)
|
|
43
|
-
private final class SwipeBackProbeView: UIView {
|
|
44
|
-
weak var navigator: Navigator?
|
|
45
|
-
|
|
46
|
-
override func didMoveToWindow() {
|
|
47
|
-
super.didMoveToWindow()
|
|
48
|
-
install()
|
|
49
|
-
// SwiftUI applies the destination's bar configuration around the same runloop
|
|
50
|
-
// turn, and it is the thing that disables the recognizer — so claim it again
|
|
51
|
-
// once that has settled.
|
|
52
|
-
DispatchQueue.main.async { [weak self] in self?.install() }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
func install() {
|
|
56
|
-
guard window != nil, let navigator, let controller = navigationController() else { return }
|
|
57
|
-
SwipeBackGestureDelegate.shared.install(on: controller, navigator: navigator)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
private func navigationController() -> UINavigationController? {
|
|
61
|
-
var responder: UIResponder? = self
|
|
62
|
-
while let current = responder {
|
|
63
|
-
if let nav = current as? UINavigationController { return nav }
|
|
64
|
-
if let controller = current as? UIViewController, let nav = controller.navigationController { return nav }
|
|
65
|
-
responder = current.next
|
|
66
|
-
}
|
|
67
|
-
return nil
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/// Shared `interactivePopGestureRecognizer` delegate. One instance serves every kit stack
|
|
72
|
-
/// in the process (a bottom-tab surface or a host screen can have its own), keyed by the
|
|
73
|
-
/// navigation controller the recognizer belongs to. Being long-lived matters: the delegate
|
|
74
|
-
/// is held weakly by UIKit, and a nil delegate lets the gesture fire on a single-screen
|
|
75
|
-
/// stack, which corrupts it.
|
|
76
|
-
@available(iOS 16.0, *)
|
|
77
|
-
private final class SwipeBackGestureDelegate: NSObject, UIGestureRecognizerDelegate {
|
|
78
|
-
static let shared = SwipeBackGestureDelegate()
|
|
79
|
-
|
|
80
|
-
private struct Entry {
|
|
81
|
-
weak var controller: UINavigationController?
|
|
82
|
-
weak var navigator: Navigator?
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private var entries: [ObjectIdentifier: Entry] = [:]
|
|
86
|
-
|
|
87
|
-
func install(on controller: UINavigationController, navigator: Navigator) {
|
|
88
|
-
entries = entries.filter { $0.value.controller != nil && $0.value.navigator != nil }
|
|
89
|
-
entries[ObjectIdentifier(controller)] = Entry(controller: controller, navigator: navigator)
|
|
90
|
-
|
|
91
|
-
guard let gesture = controller.interactivePopGestureRecognizer else { return }
|
|
92
|
-
gesture.delegate = self
|
|
93
|
-
gesture.isEnabled = true
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
97
|
-
guard let entry = entry(for: gestureRecognizer), let controller = entry.controller else { return false }
|
|
98
|
-
// Guards the classic UIKit hang: popping the only view controller on the stack.
|
|
99
|
-
guard controller.viewControllers.count > 1 else { return false }
|
|
100
|
-
guard let navigator = entry.navigator else { return true }
|
|
101
|
-
return navigator.shouldBeginInteractivePop()
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/// The edge swipe stays exclusive — letting it run alongside a horizontal scroll or a
|
|
105
|
-
/// swipeable cell would pop the screen while the user is dragging inside it.
|
|
106
|
-
func gestureRecognizer(
|
|
107
|
-
_ gestureRecognizer: UIGestureRecognizer,
|
|
108
|
-
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
|
|
109
|
-
) -> Bool {
|
|
110
|
-
false
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private func entry(for gestureRecognizer: UIGestureRecognizer) -> Entry? {
|
|
114
|
-
entries.values.first { $0.controller?.interactivePopGestureRecognizer === gestureRecognizer }
|
|
115
|
-
}
|
|
116
|
-
}
|