@momo-kits/native-kits 0.164.1-beta.5-debug → 0.164.1-beta.6-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.5-debug"
43
+ version = "0.164.1-beta.6-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.5-debug'
3
+ spec.version = '0.164.1-beta.6-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -94,8 +94,6 @@ fun NavigationContainer(
94
94
  setNavigator?.invoke(navigator)
95
95
  }
96
96
 
97
- SwipeBackHaptic(maxApi)
98
-
99
97
  // Gesture pops never reach Navigator.dismissScreen(), so prune the registry from
100
98
  // the back stack itself. collectLatest lets a rapid re-push cancel a pending prune.
101
99
  LaunchedEffect(navController, registry) {
@@ -4,11 +4,14 @@ import androidx.compose.foundation.layout.Box
4
4
  import androidx.compose.foundation.layout.fillMaxSize
5
5
  import androidx.compose.runtime.Composable
6
6
  import androidx.compose.runtime.MutableState
7
+ import androidx.compose.runtime.collectAsState
8
+ import androidx.compose.runtime.getValue
7
9
  import androidx.compose.runtime.mutableStateOf
8
10
  import androidx.compose.runtime.staticCompositionLocalOf
9
11
  import androidx.compose.ui.Alignment
10
12
  import androidx.compose.ui.Modifier
11
13
  import androidx.navigation.NavController
14
+ import androidx.navigation.NavGraph
12
15
  import androidx.navigation.toRoute
13
16
  import kotlinx.coroutines.CoroutineScope
14
17
  import kotlinx.coroutines.Dispatchers
@@ -79,8 +82,14 @@ class Navigator(
79
82
  }
80
83
 
81
84
  // The swipe-back opt-out predicate in StackScreen needs this: the root screen must keep custom
82
- // back handling so it still reaches maxApi.dismiss.
83
- internal fun canPop(): Boolean = navController.previousBackStackEntry != null
85
+ // back handling so it still reaches maxApi.dismiss. It has to read currentBackStack rather than
86
+ // previousBackStackEntry the latter walks a plain ArrayDeque, so a composable reading it never
87
+ // recomposes and the predicate would freeze at the value it had on first composition.
88
+ @Composable
89
+ internal fun rememberCanPop(): Boolean {
90
+ val backStack by navController.currentBackStack.collectAsState()
91
+ return backStack.count { it.destination !is NavGraph } > 1
92
+ }
84
93
 
85
94
  fun pop(count: Int = 1, callBack: (() -> Unit)? = null) {
86
95
  scope.launch {
@@ -169,6 +169,10 @@ internal fun StackScreen(
169
169
  RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
170
170
  }
171
171
 
172
+ // Hoisted out of the `||` chain below: short-circuiting would drop the back-stack subscription
173
+ // whenever an earlier term is true.
174
+ val canPop = navigator.rememberCanPop()
175
+
172
176
  // Port of RN's `hasCustomBackHandling`: this handler is registered inside the NavHost
173
177
  // destination, so while it is enabled it beats NavHost's own predictive-back handler and
174
178
  // swallows the swipe. Enable it only when a plain pop is the wrong answer.
@@ -177,9 +181,13 @@ internal fun StackScreen(
177
181
  options.onBackHandler != null ||
178
182
  options.headerBackProps.preventBack != null ||
179
183
  OverplayComponentRegistry.isOverplayShowing() ||
180
- !navigator.canPop()
184
+ !canPop
185
+
186
+ // BottomTab supplies a tab-aware handler; falling through to onBackSafe() there would dismiss
187
+ // the whole mini-app instead of returning to tab 0.
188
+ val onBack = onBackHandler ?: { navigator.onBackSafe() }
181
189
 
182
- BackHandler(hasCustomBackHandling) { navigator.onBackSafe() }
190
+ BackHandler(hasCustomBackHandling, onBack)
183
191
 
184
192
  CompositionLocalProvider(
185
193
  StackScreenScrollableState provides options.scrollData.scrollState,
@@ -18,7 +18,9 @@ import androidx.compose.runtime.Stable
18
18
  import androidx.compose.runtime.derivedStateOf
19
19
  import androidx.compose.runtime.getValue
20
20
  import androidx.compose.runtime.mutableStateMapOf
21
+ import androidx.compose.runtime.mutableStateOf
21
22
  import androidx.compose.runtime.remember
23
+ import androidx.compose.runtime.setValue
22
24
  import androidx.compose.runtime.staticCompositionLocalOf
23
25
  import androidx.compose.ui.Modifier
24
26
  import androidx.compose.ui.draw.drawBehind
@@ -43,12 +45,14 @@ private const val EDGE_SHADOW_MAX_ALPHA = 0.3f
43
45
  private val EDGE_SHADOW_WIDTH = 8.dp
44
46
 
45
47
  /**
46
- * Kill switch for the swipe-back gesture. Set to `false` to restore pre-DS-760 behaviour (kit
47
- * back handling always wins, iOS edge pan inert) without a kit release. Screen transitions stay
48
- * on the new parallax curve either way.
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
51
  */
50
52
  object SwipeBackConfig {
51
- var enabled: Boolean = true
53
+ // Snapshot-backed so a host toggle takes effect on the next frame instead of waiting for an
54
+ // unrelated recomposition of StackScreen.
55
+ var enabled: Boolean by mutableStateOf(true)
52
56
  }
53
57
 
54
58
  // LinearEasing is mandatory, not a style choice: SeekableTransitionState.seekTo() maps the drag
@@ -106,8 +110,10 @@ internal fun AnimatedVisibilityScope.StackTransitionDecor(
106
110
  content: @Composable () -> Unit,
107
111
  ) {
108
112
  val coordinator = LocalStackTransition.current
109
- coordinator.attach(id, isStackRoute)
110
- DisposableEffect(coordinator, id) {
113
+ // attach() writes state that derivedStateOf { topStackId } reads in the same pass, so it has to
114
+ // stay out of the composable body.
115
+ DisposableEffect(coordinator, id, isStackRoute) {
116
+ coordinator.attach(id, isStackRoute)
111
117
  onDispose { coordinator.detach(id) }
112
118
  }
113
119
 
@@ -17,7 +17,6 @@ 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
21
20
  import androidx.compose.ui.graphics.Color
22
21
  import androidx.compose.ui.graphics.NativePaint
23
22
  import androidx.compose.ui.graphics.toArgb
@@ -39,7 +38,6 @@ import platform.Foundation.NSBundle
39
38
  import platform.UIKit.UIColor
40
39
  import platform.UIKit.UIDevice
41
40
  import platform.UIKit.UIScreen
42
- import vn.momo.kits.navigation.SwipeBackConfig
43
41
 
44
42
  actual fun getPlatformName(): String = "iOS"
45
43
 
@@ -62,30 +60,14 @@ actual fun getStatusBarHeight(): Dp {
62
60
  return WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
63
61
  }
64
62
 
65
- // An enabled kit BackHandler sits innermost and therefore consumes the edge-pan gesture — that is
66
- // how a screen opts out of swipe-back (the port of RN's `hasCustomBackHandling`).
67
- @Suppress("DEPRECATION")
68
- @OptIn(ExperimentalComposeUiApi::class)
69
63
  @Composable
70
64
  actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
71
- ComposeBackHandler(enabled = enabled, onBack = onBack)
72
65
  }
73
66
 
74
67
  @OptIn(InternalComposeUiApi::class)
75
68
  @Composable
76
69
  actual fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit) {
77
- val platformOwner = LocalCompatNavigationEventDispatcherOwner.current
78
- if (platformOwner != null && SwipeBackConfig.enabled) {
79
- // UIKitNavigationEventInput is registered on the scene's dispatcher. Providing a fresh
80
- // NavigationEventDispatcher() here would shadow it with an input-less one that never sees
81
- // a single edge-pan event, enabled or not — so pass the platform owner straight through.
82
- content()
83
- return
84
- }
85
-
86
- // Bare UIView hosts have no scene dispatcher, and BackHandler/PredictiveBackHandler error() on
87
- // a null owner. This detached fallback is also what the kill switch falls back to.
88
- val fallbackOwner = remember {
70
+ val navigationEventDispatcherOwner = remember {
89
71
  object : NavigationEventDispatcherOwner {
90
72
  override val navigationEventDispatcher = NavigationEventDispatcher().apply {
91
73
  isEnabled = false
@@ -94,7 +76,7 @@ actual fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit
94
76
  }
95
77
 
96
78
  CompositionLocalProvider(
97
- LocalCompatNavigationEventDispatcherOwner provides fallbackOwner,
79
+ LocalCompatNavigationEventDispatcherOwner provides navigationEventDispatcherOwner,
98
80
  content = content
99
81
  )
100
82
  }
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.5
21
+ version=0.164.1-beta.6
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.5'
3
+ s.version = '0.164.1-beta.6'
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.5'
3
+ s.version = '0.164.1-beta.6'
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.5-debug",
3
+ "version": "0.164.1-beta.6-debug",
4
4
  "private": false,
5
5
  "dependeNavigationncies": {},
6
6
  "devDependencies": {},
@@ -1,12 +0,0 @@
1
- package vn.momo.kits.platform
2
-
3
- import androidx.compose.runtime.Composable
4
- import androidx.compose.runtime.State
5
- import androidx.compose.runtime.mutableStateOf
6
- import androidx.compose.runtime.remember
7
-
8
- // No passive observer exists at androidx.activity 1.10.x — a second enabled callback would steal
9
- // the gesture from NavHost. Android's system back gesture emits its own platform haptic on commit.
10
- // Revisit if androidx.activity >= 1.12 is adopted; this then becomes a copy of the iOS actual.
11
- @Composable
12
- actual fun rememberBackGestureProgress(): State<Float?> = remember { mutableStateOf(null) }
@@ -1,39 +0,0 @@
1
- package vn.momo.kits.navigation
2
-
3
- import androidx.compose.runtime.Composable
4
- import androidx.compose.runtime.LaunchedEffect
5
- import androidx.compose.runtime.snapshotFlow
6
- import vn.momo.kits.platform.getScreenDimensions
7
- import vn.momo.kits.platform.rememberBackGestureProgress
8
- import vn.momo.maxapi.IMaxApi
9
-
10
- // RN fires the tick at a flat 100 px of thumb travel; keep it unscaled so the physical distance is
11
- // the same on every device.
12
- private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 100f
13
-
14
- /**
15
- * Fires one light tick once the back gesture has travelled ~100dp, re-arming when the finger drops
16
- * back below the threshold. iOS only — [rememberBackGestureProgress] is always `null` on Android.
17
- */
18
- @Composable
19
- internal fun SwipeBackHaptic(maxApi: IMaxApi?) {
20
- if (maxApi == null) return
21
-
22
- val progress = rememberBackGestureProgress()
23
- val screenWidth = getScreenDimensions().width.toFloat()
24
-
25
- LaunchedEffect(maxApi, screenWidth) {
26
- if (screenWidth <= 0f) return@LaunchedEffect
27
- val threshold = SWIPE_BACK_HAPTIC_THRESHOLD_DP / screenWidth
28
- var isSendHaptic = false
29
- snapshotFlow { progress.value }.collect { fraction ->
30
- val travelled = fraction ?: 0f
31
- if (travelled >= threshold && !isSendHaptic) {
32
- runCatching { maxApi.triggerEventVibration("light", callback = null) }
33
- isSendHaptic = true
34
- } else if (travelled < threshold && isSendHaptic) {
35
- isSendHaptic = false
36
- }
37
- }
38
- }
39
- }
@@ -1,15 +0,0 @@
1
- package vn.momo.kits.platform
2
-
3
- import androidx.compose.runtime.Composable
4
- import androidx.compose.runtime.State
5
-
6
- /**
7
- * Progress of an in-flight back gesture as a fraction of the container width, or `null` while no
8
- * gesture is running. Observing only — it must never consume the gesture, otherwise it would steal
9
- * it from NavHost.
10
- *
11
- * Android always returns `null`: its NavHost runs on `androidx.activity`'s OnBackPressedDispatcher,
12
- * which exposes progress only to the top-most enabled callback, and that has to stay NavHost's.
13
- */
14
- @Composable
15
- expect fun rememberBackGestureProgress(): State<Float?>
@@ -1,35 +0,0 @@
1
- package vn.momo.kits.platform
2
-
3
- import androidx.compose.runtime.Composable
4
- import androidx.compose.runtime.LaunchedEffect
5
- import androidx.compose.runtime.State
6
- import androidx.compose.runtime.mutableStateOf
7
- import androidx.compose.runtime.remember
8
- import androidx.compose.ui.InternalComposeUiApi
9
- import androidx.compose.ui.backhandler.LocalCompatNavigationEventDispatcherOwner
10
- import androidx.navigationevent.NavigationEventTransitionState
11
-
12
- // NavigationEventDispatcher.transitionState is a passive observer: UIKitNavigationEventInput
13
- // publishes every edge-pan event to it without any handler consuming the gesture. Its `progress`
14
- // is touch.x / view width, i.e. a plain fraction of the screen width.
15
- @OptIn(InternalComposeUiApi::class)
16
- @Composable
17
- actual fun rememberBackGestureProgress(): State<Float?> {
18
- val dispatcher = LocalCompatNavigationEventDispatcherOwner.current?.navigationEventDispatcher
19
- val progress = remember { mutableStateOf<Float?>(null) }
20
-
21
- LaunchedEffect(dispatcher) {
22
- if (dispatcher == null) {
23
- progress.value = null
24
- return@LaunchedEffect
25
- }
26
- dispatcher.transitionState.collect { state ->
27
- progress.value = (state as? NavigationEventTransitionState.InProgress)
28
- ?.takeIf { it.direction == NavigationEventTransitionState.TRANSITIONING_BACK }
29
- ?.latestEvent
30
- ?.progress
31
- }
32
- }
33
-
34
- return progress
35
- }