@momo-kits/native-kits 0.164.1-beta.6-debug → 0.164.1-beta.7-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.
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.164.1-beta.6-debug"
43
+ version = "0.164.1-beta.7-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.164.1-beta.6-debug'
3
+ spec.version = '0.164.1-beta.7-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -161,7 +161,8 @@ fun NavigationContainer(
161
161
  id = route.id,
162
162
  name = screen.name,
163
163
  content = screen.content,
164
- navigationOptions = screen.options
164
+ navigationOptions = screen.options,
165
+ isDialogRoute = true
165
166
  )
166
167
  }
167
168
  }
@@ -10,6 +10,7 @@ import androidx.compose.runtime.mutableStateOf
10
10
  import androidx.compose.runtime.staticCompositionLocalOf
11
11
  import androidx.compose.ui.Alignment
12
12
  import androidx.compose.ui.Modifier
13
+ import androidx.navigation.NavBackStackEntry
13
14
  import androidx.navigation.NavController
14
15
  import androidx.navigation.NavGraph
15
16
  import androidx.navigation.toRoute
@@ -88,9 +89,16 @@ class Navigator(
88
89
  @Composable
89
90
  internal fun rememberCanPop(): Boolean {
90
91
  val backStack by navController.currentBackStack.collectAsState()
91
- return backStack.count { it.destination !is NavGraph } > 1
92
+ return backStack.hasPoppableEntry()
92
93
  }
93
94
 
95
+ /**
96
+ * Non-composable twin of [rememberCanPop] for the iOS host: its own back check is mount-based
97
+ * and so answers `true` even on the mini-app root, where a swipe must dismiss rather than pop.
98
+ * Reads the StateFlow value directly — correct here precisely because nothing recomposes on it.
99
+ */
100
+ fun canPop(): Boolean = navController.currentBackStack.value.hasPoppableEntry()
101
+
94
102
  fun pop(count: Int = 1, callBack: (() -> Unit)? = null) {
95
103
  scope.launch {
96
104
  repeat(count) {
@@ -188,6 +196,10 @@ class Navigator(
188
196
  }
189
197
  }
190
198
 
199
+ // The NavGraph itself sits in the back stack as an entry; only real destinations can be popped.
200
+ private fun List<NavBackStackEntry>.hasPoppableEntry(): Boolean =
201
+ count { it.destination !is NavGraph } > 1
202
+
191
203
  val LocalNavigator = staticCompositionLocalOf<Navigator> {
192
204
  error("No NavigationStack provided")
193
205
  }
@@ -93,6 +93,7 @@ internal fun StackScreen(
93
93
  id: Int = -1,
94
94
  name: String = "",
95
95
  bottomTabIndex: Int = -1,
96
+ isDialogRoute: Boolean = false,
96
97
  onBackHandler: (() -> Unit)? = null,
97
98
  ) {
98
99
  val navigator = LocalNavigator.current
@@ -176,8 +177,12 @@ internal fun StackScreen(
176
177
  // Port of RN's `hasCustomBackHandling`: this handler is registered inside the NavHost
177
178
  // destination, so while it is enabled it beats NavHost's own predictive-back handler and
178
179
  // swallows the swipe. Enable it only when a plain pop is the wrong answer.
180
+ //
181
+ // A presented route is one of those cases: NavHost arms predictive back on back-stack size
182
+ // alone, so a horizontal edge drag over a dialog would seek its *vertical* slide.
179
183
  val hasCustomBackHandling = !SwipeBackConfig.enabled ||
180
184
  isBottomTabChild ||
185
+ isDialogRoute ||
181
186
  options.onBackHandler != null ||
182
187
  options.headerBackProps.preventBack != null ||
183
188
  OverplayComponentRegistry.isOverplayShowing() ||
@@ -46,8 +46,9 @@ private val EDGE_SHADOW_WIDTH = 8.dp
46
46
 
47
47
  /**
48
48
  * Kill switch for the swipe-back gesture. Set to `false` to make kit back handling always win,
49
- * which disables Android's predictive back drag, without a kit release. It has no effect on iOS —
50
- * the host owns the edge gesture there. Screen transitions stay on the parallax curve either way.
49
+ * which disables the predictive-back drag on both platforms on iOS that means the host-driven
50
+ * seek stops following the finger, though the host's own gesture still fires a plain back. Screen
51
+ * transitions stay on the parallax curve either way.
51
52
  */
52
53
  object SwipeBackConfig {
53
54
  // Snapshot-backed so a host toggle takes effect on the next frame instead of waiting for an
@@ -0,0 +1,118 @@
1
+ package vn.momo.kits.navigation
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.DisposableEffect
5
+ import androidx.compose.runtime.remember
6
+ import androidx.navigationevent.NavigationEvent
7
+ import androidx.navigationevent.NavigationEventDispatcher
8
+ import androidx.navigationevent.NavigationEventInput
9
+ import kotlinx.cinterop.ExperimentalForeignApi
10
+ import platform.Foundation.NSThread
11
+ import platform.darwin.dispatch_async
12
+ import platform.darwin.dispatch_get_main_queue
13
+
14
+ /**
15
+ * Kit end of the host's edge-pan recogniser.
16
+ *
17
+ * iOS keeps exactly one back-gesture recogniser and it belongs to the host app — the kit never
18
+ * installs its own, so the `.failed -> completed` conflict between two competing recognisers is
19
+ * unreachable by construction. The host replays its drag here; the events are fed into the
20
+ * `NavigationEventDispatcher` that [NavigationContainer] provides, which drives NavHost's
21
+ * `PredictiveBackHandler` and therefore `SeekableTransitionState.seekTo`. Parallax, scrim and edge
22
+ * shadow are children of that same transition, so they follow the finger for free.
23
+ *
24
+ * Only primitives cross the Swift boundary: `androidx.navigationevent` is a private transitive
25
+ * dependency and is absent from the generated framework header.
26
+ *
27
+ * Call every method from the main thread; anything else is re-posted to the main queue.
28
+ */
29
+ object HostBackGestureBridge {
30
+ private val attachedInputs = mutableListOf<HostNavigationEventInput>()
31
+ private var gestureInput: HostNavigationEventInput? = null
32
+
33
+ /**
34
+ * Whether the top-most mounted container has an enabled back handler, i.e. the authoritative
35
+ * answer to "will Compose consume this back?". `false` means no container is mounted, or the
36
+ * mounted one deliberately opts out (kill switch, overlay showing, custom back handler), and
37
+ * the host should fall back to its own navigation.
38
+ */
39
+ val hasEnabledHandlers: Boolean
40
+ get() = attachedInputs.lastOrNull()?.hasEnabledHandlers == true
41
+
42
+ /** Begins a drag. Pairs with exactly one [completed] or [cancelled]. */
43
+ fun started() = onMain {
44
+ val input = attachedInputs.lastOrNull() ?: return@onMain
45
+ gestureInput = input
46
+ input.backStarted()
47
+ }
48
+
49
+ /** Drag progress in `0f..1f` — the fraction of the screen width travelled. */
50
+ fun progressed(progress: Float) = onMain {
51
+ gestureInput?.backProgressed(progress.coerceIn(0f, 1f))
52
+ }
53
+
54
+ /** Released past the commit threshold: the screen pops. */
55
+ fun completed() = onMain {
56
+ gestureInput?.backCompleted()
57
+ gestureInput = null
58
+ }
59
+
60
+ /** Released below the commit threshold: the screen springs back. */
61
+ fun cancelled() = onMain {
62
+ gestureInput?.backCancelled()
63
+ gestureInput = null
64
+ }
65
+
66
+ internal fun attach(input: HostNavigationEventInput) {
67
+ attachedInputs += input
68
+ }
69
+
70
+ internal fun detach(input: HostNavigationEventInput) {
71
+ attachedInputs -= input
72
+ // A container unmounted mid-drag must not hand its pending terminal event to whichever
73
+ // container is on top now: NavigationEventInput fabricates a non-predictive start for a
74
+ // completed/cancelled it never saw begin, which would pop a screen out of nowhere.
75
+ if (gestureInput === input) gestureInput = null
76
+ }
77
+ }
78
+
79
+ /** Wires one [NavigationContainer]'s dispatcher to [HostBackGestureBridge] while it is composed. */
80
+ @Composable
81
+ internal fun AttachHostBackGesture(dispatcher: NavigationEventDispatcher) {
82
+ val input = remember(dispatcher) { HostNavigationEventInput() }
83
+
84
+ DisposableEffect(dispatcher, input) {
85
+ dispatcher.addInput(input)
86
+ HostBackGestureBridge.attach(input)
87
+ onDispose {
88
+ HostBackGestureBridge.detach(input)
89
+ dispatcher.removeInput(input)
90
+ }
91
+ }
92
+ }
93
+
94
+ internal class HostNavigationEventInput : NavigationEventInput() {
95
+ var hasEnabledHandlers: Boolean = false
96
+ private set
97
+
98
+ override fun onHasEnabledHandlersChanged(hasEnabledHandlers: Boolean) {
99
+ this.hasEnabledHandlers = hasEnabledHandlers
100
+ }
101
+
102
+ // Touch coordinates are not forwarded — the stack transition is seeked by progress alone, and
103
+ // leaving them out keeps the bridge's Swift-facing signatures primitive.
104
+ fun backStarted() = dispatchOnBackStarted(NavigationEvent(swipeEdge = NavigationEvent.EDGE_LEFT))
105
+
106
+ fun backProgressed(progress: Float) = dispatchOnBackProgressed(
107
+ NavigationEvent(swipeEdge = NavigationEvent.EDGE_LEFT, progress = progress)
108
+ )
109
+
110
+ fun backCancelled() = dispatchOnBackCancelled()
111
+
112
+ fun backCompleted() = dispatchOnBackCompleted()
113
+ }
114
+
115
+ @OptIn(ExperimentalForeignApi::class)
116
+ private fun onMain(block: () -> Unit) {
117
+ if (NSThread.isMainThread()) block() else dispatch_async(dispatch_get_main_queue()) { block() }
118
+ }
@@ -17,6 +17,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
17
17
  import androidx.compose.ui.InternalComposeUiApi
18
18
  import androidx.compose.ui.Modifier
19
19
  import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
20
+ import androidx.compose.ui.backhandler.BackHandler as ComposeBackHandler
20
21
  import androidx.compose.ui.graphics.Color
21
22
  import androidx.compose.ui.graphics.NativePaint
22
23
  import androidx.compose.ui.graphics.toArgb
@@ -38,6 +39,7 @@ import platform.Foundation.NSBundle
38
39
  import platform.UIKit.UIColor
39
40
  import platform.UIKit.UIDevice
40
41
  import platform.UIKit.UIScreen
42
+ import vn.momo.kits.navigation.AttachHostBackGesture
41
43
 
42
44
  actual fun getPlatformName(): String = "iOS"
43
45
 
@@ -60,21 +62,32 @@ actual fun getStatusBarHeight(): Dp {
60
62
  return WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
61
63
  }
62
64
 
65
+ // An enabled kit BackHandler is registered inside the NavHost destination, so it out-ranks
66
+ // NavHost's own predictive-back handler and swallows the gesture — that is how a screen opts out
67
+ // of swipe-back (the port of RN's `hasCustomBackHandling`).
68
+ @Suppress("DEPRECATION")
69
+ @OptIn(ExperimentalComposeUiApi::class)
63
70
  @Composable
64
71
  actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
72
+ ComposeBackHandler(enabled = enabled, onBack = onBack)
65
73
  }
66
74
 
67
75
  @OptIn(InternalComposeUiApi::class)
68
76
  @Composable
69
77
  actual fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit) {
78
+ // Detached on purpose: passing the scene's owner through would arm Compose's own
79
+ // UIKitBackGestureRecognizer alongside the host's, and UIKit settles that conflict by driving
80
+ // one to .failed, which UIKitNavigationEventInput turns into a fabricated completed back.
81
+ // Enabled on purpose: every NavigationEventDispatcher.dispatchOn* early-returns while
82
+ // isEnabled is false, which would mute the host-driven seek as well.
70
83
  val navigationEventDispatcherOwner = remember {
71
84
  object : NavigationEventDispatcherOwner {
72
- override val navigationEventDispatcher = NavigationEventDispatcher().apply {
73
- isEnabled = false
74
- }
85
+ override val navigationEventDispatcher = NavigationEventDispatcher()
75
86
  }
76
87
  }
77
88
 
89
+ AttachHostBackGesture(navigationEventDispatcherOwner.navigationEventDispatcher)
90
+
78
91
  CompositionLocalProvider(
79
92
  LocalCompatNavigationEventDispatcherOwner provides navigationEventDispatcherOwner,
80
93
  content = content
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.164.1-beta.6
21
+ version=0.164.1-beta.7
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKits'
3
- s.version = '0.164.1-beta.6'
3
+ s.version = '0.164.1-beta.7'
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.164.1-beta.6'
3
+ s.version = '0.164.1-beta.7'
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.164.1-beta.6-debug",
3
+ "version": "0.164.1-beta.7-debug",
4
4
  "private": false,
5
5
  "dependeNavigationncies": {},
6
6
  "devDependencies": {},