@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.
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +13 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +5 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +3 -2
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +118 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +16 -3
- 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/build.gradle.kts
CHANGED
package/compose/compose.podspec
CHANGED
|
@@ -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.
|
|
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
|
|
50
|
-
*
|
|
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()
|
|
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
package/ios/native-kits.podspec
CHANGED