@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.
@@ -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
  }
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.163.1-sp.7-debug"
43
+ version = "0.163.1-sp.8-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.163.1-sp.7-debug'
3
+ spec.version = '0.163.1-sp.8-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.163.1-sp.7
21
+ version=0.163.1-sp.8
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -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
- // The edge-swipe pop is driven by UIKit and lands straight on the binding,
94
- // never on `pop`, so screen disposal hangs off the path itself.
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 slower interactive swipe.
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-swipe. Liveness is re-checked when
470
- /// the timer fires, so a cancelled swipe (path restored) keeps its screen.
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
+ }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKits'
3
- s.version = '0.163.1-sp.7'
3
+ s.version = '0.163.1-sp.8'
4
4
  s.summary = 'MoMoUIKits for iOS'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKitsDemo'
3
- s.version = '0.163.1-sp.7'
3
+ s.version = '0.163.1-sp.8'
4
4
  s.summary = 'Demo browser for MoMoUIKits SwiftUI components'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.163.1-sp.7-debug",
3
+ "version": "0.163.1-sp.8-debug",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},
@@ -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
- }