@momo-kits/native-kits 0.164.1-beta.10-debug → 0.164.1-beta.11-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/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +3 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +2 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +6 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +1 -3
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +8 -11
- package/gradle.properties +1 -1
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/package.json +1 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ComposeBackGesture.ios.kt +0 -65
package/compose/build.gradle.kts
CHANGED
package/compose/compose.podspec
CHANGED
|
@@ -4,15 +4,11 @@ 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
|
|
9
7
|
import androidx.compose.runtime.mutableStateOf
|
|
10
8
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
11
9
|
import androidx.compose.ui.Alignment
|
|
12
10
|
import androidx.compose.ui.Modifier
|
|
13
|
-
import androidx.navigation.NavBackStackEntry
|
|
14
11
|
import androidx.navigation.NavController
|
|
15
|
-
import androidx.navigation.NavGraph
|
|
16
12
|
import androidx.navigation.toRoute
|
|
17
13
|
import kotlinx.coroutines.CoroutineScope
|
|
18
14
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -83,21 +79,9 @@ class Navigator(
|
|
|
83
79
|
}
|
|
84
80
|
|
|
85
81
|
// The swipe-back opt-out predicate in StackScreen needs this: the root screen must keep custom
|
|
86
|
-
// back handling so it still reaches maxApi.dismiss.
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
@Composable
|
|
90
|
-
internal fun rememberCanPop(): Boolean {
|
|
91
|
-
val backStack by navController.currentBackStack.collectAsState()
|
|
92
|
-
return backStack.hasPoppableEntry()
|
|
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()
|
|
82
|
+
// back handling so it still reaches maxApi.dismiss.
|
|
83
|
+
// Public: the iOS host asks this to decide whether the kit owns the swipe on this screen.
|
|
84
|
+
fun canPop(): Boolean = navController.previousBackStackEntry != null
|
|
101
85
|
|
|
102
86
|
fun pop(count: Int = 1, callBack: (() -> Unit)? = null) {
|
|
103
87
|
scope.launch {
|
|
@@ -196,10 +180,6 @@ class Navigator(
|
|
|
196
180
|
}
|
|
197
181
|
}
|
|
198
182
|
|
|
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
|
-
|
|
203
183
|
val LocalNavigator = staticCompositionLocalOf<Navigator> {
|
|
204
184
|
error("No NavigationStack provided")
|
|
205
185
|
}
|
|
@@ -93,7 +93,6 @@ internal fun StackScreen(
|
|
|
93
93
|
id: Int = -1,
|
|
94
94
|
name: String = "",
|
|
95
95
|
bottomTabIndex: Int = -1,
|
|
96
|
-
isDialogRoute: Boolean = false,
|
|
97
96
|
onBackHandler: (() -> Unit)? = null,
|
|
98
97
|
) {
|
|
99
98
|
val navigator = LocalNavigator.current
|
|
@@ -170,29 +169,17 @@ internal fun StackScreen(
|
|
|
170
169
|
RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
// Hoisted out of the `||` chain below: short-circuiting would drop the back-stack subscription
|
|
174
|
-
// whenever an earlier term is true.
|
|
175
|
-
val canPop = navigator.rememberCanPop()
|
|
176
|
-
|
|
177
172
|
// Port of RN's `hasCustomBackHandling`: this handler is registered inside the NavHost
|
|
178
173
|
// destination, so while it is enabled it beats NavHost's own predictive-back handler and
|
|
179
174
|
// 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.
|
|
183
175
|
val hasCustomBackHandling = !SwipeBackConfig.enabled ||
|
|
184
176
|
isBottomTabChild ||
|
|
185
|
-
isDialogRoute ||
|
|
186
177
|
options.onBackHandler != null ||
|
|
187
178
|
options.headerBackProps.preventBack != null ||
|
|
188
179
|
OverplayComponentRegistry.isOverplayShowing() ||
|
|
189
|
-
!canPop
|
|
190
|
-
|
|
191
|
-
// BottomTab supplies a tab-aware handler; falling through to onBackSafe() there would dismiss
|
|
192
|
-
// the whole mini-app instead of returning to tab 0.
|
|
193
|
-
val onBack = onBackHandler ?: { navigator.onBackSafe() }
|
|
180
|
+
!navigator.canPop()
|
|
194
181
|
|
|
195
|
-
BackHandler(hasCustomBackHandling
|
|
182
|
+
BackHandler(hasCustomBackHandling) { navigator.onBackSafe() }
|
|
196
183
|
|
|
197
184
|
CompositionLocalProvider(
|
|
198
185
|
StackScreenScrollableState provides options.scrollData.scrollState,
|
|
@@ -18,9 +18,7 @@ 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
|
|
22
21
|
import androidx.compose.runtime.remember
|
|
23
|
-
import androidx.compose.runtime.setValue
|
|
24
22
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
25
23
|
import androidx.compose.ui.Modifier
|
|
26
24
|
import androidx.compose.ui.draw.drawBehind
|
|
@@ -45,15 +43,12 @@ private const val EDGE_SHADOW_MAX_ALPHA = 0.3f
|
|
|
45
43
|
private val EDGE_SHADOW_WIDTH = 8.dp
|
|
46
44
|
|
|
47
45
|
/**
|
|
48
|
-
* Kill switch for the swipe-back gesture. Set to `false` to
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* way.
|
|
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.
|
|
52
49
|
*/
|
|
53
50
|
object SwipeBackConfig {
|
|
54
|
-
|
|
55
|
-
// unrelated recomposition of StackScreen.
|
|
56
|
-
var enabled: Boolean by mutableStateOf(true)
|
|
51
|
+
var enabled: Boolean = true
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
// LinearEasing is mandatory, not a style choice: SeekableTransitionState.seekTo() maps the drag
|
|
@@ -111,10 +106,8 @@ internal fun AnimatedVisibilityScope.StackTransitionDecor(
|
|
|
111
106
|
content: @Composable () -> Unit,
|
|
112
107
|
) {
|
|
113
108
|
val coordinator = LocalStackTransition.current
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
DisposableEffect(coordinator, id, isStackRoute) {
|
|
117
|
-
coordinator.attach(id, isStackRoute)
|
|
109
|
+
coordinator.attach(id, isStackRoute)
|
|
110
|
+
DisposableEffect(coordinator, id) {
|
|
118
111
|
onDispose { coordinator.detach(id) }
|
|
119
112
|
}
|
|
120
113
|
|
|
@@ -17,9 +17,7 @@ private const val SWIPE_BACK_HAPTIC_THRESHOLD_DP = 50f
|
|
|
17
17
|
*/
|
|
18
18
|
@Composable
|
|
19
19
|
internal fun SwipeBackHaptic(maxApi: IMaxApi?) {
|
|
20
|
-
|
|
21
|
-
// moves — a tick there would be a phantom.
|
|
22
|
-
if (maxApi == null || !SwipeBackConfig.enabled) return
|
|
20
|
+
if (maxApi == null) return
|
|
23
21
|
|
|
24
22
|
val progress = rememberBackGestureProgress()
|
|
25
23
|
val screenWidth = getScreenDimensions().width.toFloat()
|
|
@@ -39,7 +39,7 @@ import platform.Foundation.NSBundle
|
|
|
39
39
|
import platform.UIKit.UIColor
|
|
40
40
|
import platform.UIKit.UIDevice
|
|
41
41
|
import platform.UIKit.UIScreen
|
|
42
|
-
import vn.momo.kits.navigation.
|
|
42
|
+
import vn.momo.kits.navigation.SwipeBackConfig
|
|
43
43
|
|
|
44
44
|
actual fun getPlatformName(): String = "iOS"
|
|
45
45
|
|
|
@@ -62,9 +62,8 @@ actual fun getStatusBarHeight(): Dp {
|
|
|
62
62
|
return WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// An enabled kit BackHandler
|
|
66
|
-
//
|
|
67
|
-
// of swipe-back (the port of RN's `hasCustomBackHandling`).
|
|
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`).
|
|
68
67
|
@Suppress("DEPRECATION")
|
|
69
68
|
@OptIn(ExperimentalComposeUiApi::class)
|
|
70
69
|
@Composable
|
|
@@ -76,18 +75,16 @@ actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
|
|
|
76
75
|
@Composable
|
|
77
76
|
actual fun ProvideNavigationEventDispatcherOwner(content: @Composable () -> Unit) {
|
|
78
77
|
val platformOwner = LocalCompatNavigationEventDispatcherOwner.current
|
|
79
|
-
if (platformOwner != null) {
|
|
80
|
-
// UIKitNavigationEventInput is registered on the scene's dispatcher
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// sees a single edge-pan event — no recogniser, and no finger-following seek.
|
|
84
|
-
ObserveBackGestureOwnership(platformOwner.navigationEventDispatcher)
|
|
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.
|
|
85
82
|
content()
|
|
86
83
|
return
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
// Bare UIView hosts have no scene dispatcher, and BackHandler/PredictiveBackHandler error() on
|
|
90
|
-
// a null owner.
|
|
87
|
+
// a null owner. This detached fallback is also what the kill switch falls back to.
|
|
91
88
|
val fallbackOwner = remember {
|
|
92
89
|
object : NavigationEventDispatcherOwner {
|
|
93
90
|
override val navigationEventDispatcher = NavigationEventDispatcher().apply {
|
package/gradle.properties
CHANGED
package/ios/native-kits.podspec
CHANGED
package/package.json
CHANGED
|
@@ -1,65 +0,0 @@
|
|
|
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.NavigationEventDispatcher
|
|
7
|
-
import androidx.navigationevent.NavigationEventInput
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Read-only answer to "will Compose consume this back?", for the iOS host.
|
|
11
|
-
*
|
|
12
|
-
* Compose owns the edge-pan gesture through its own `UIKitBackGestureRecognizer`; the host reads
|
|
13
|
-
* this to decide whether to stand down or keep driving its own recogniser, instead of guessing.
|
|
14
|
-
* Nothing here dispatches — [ObserveBackGestureOwnership] only listens.
|
|
15
|
-
*
|
|
16
|
-
* Pair it with [Navigator.canPop]: the mini-app root reports an enabled handler too (it swallows
|
|
17
|
-
* back to reach `maxApi.dismiss`), yet there the host still owns the gesture.
|
|
18
|
-
*/
|
|
19
|
-
object ComposeBackGesture {
|
|
20
|
-
private val observers = mutableListOf<BackHandlerObserver>()
|
|
21
|
-
|
|
22
|
-
/** State of the top-most mounted container; `false` when none is mounted. */
|
|
23
|
-
val hasEnabledHandlers: Boolean
|
|
24
|
-
get() = observers.lastOrNull()?.hasEnabledHandlers == true
|
|
25
|
-
|
|
26
|
-
internal fun attach(observer: BackHandlerObserver) {
|
|
27
|
-
observers += observer
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
internal fun detach(observer: BackHandlerObserver) {
|
|
31
|
-
observers -= observer
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Publishes one [NavigationContainer]'s back-handling state to [ComposeBackGesture] while composed.
|
|
37
|
-
*
|
|
38
|
-
* Only called for a container whose dispatcher is the scene's — a bare-`UIView` host gets the
|
|
39
|
-
* detached fallback dispatcher, which can never receive an edge pan, so reporting its handlers
|
|
40
|
-
* would tell the host to stand down in favour of a gesture that does not exist.
|
|
41
|
-
*/
|
|
42
|
-
@Composable
|
|
43
|
-
internal fun ObserveBackGestureOwnership(dispatcher: NavigationEventDispatcher) {
|
|
44
|
-
val observer = remember(dispatcher) { BackHandlerObserver() }
|
|
45
|
-
|
|
46
|
-
DisposableEffect(dispatcher, observer) {
|
|
47
|
-
dispatcher.addInput(observer)
|
|
48
|
-
ComposeBackGesture.attach(observer)
|
|
49
|
-
onDispose {
|
|
50
|
-
ComposeBackGesture.detach(observer)
|
|
51
|
-
dispatcher.removeInput(observer)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// A NavigationEventInput that never dispatches: adding an input is the only way to observe the
|
|
57
|
-
// dispatcher's enabled-handler state without registering a handler, which would consume the gesture.
|
|
58
|
-
internal class BackHandlerObserver : NavigationEventInput() {
|
|
59
|
-
var hasEnabledHandlers: Boolean = false
|
|
60
|
-
private set
|
|
61
|
-
|
|
62
|
-
override fun onHasEnabledHandlersChanged(hasEnabledHandlers: Boolean) {
|
|
63
|
-
this.hasEnabledHandlers = hasEnabledHandlers
|
|
64
|
-
}
|
|
65
|
-
}
|