@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-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/CLAUDE.md +32 -0
- package/compose/build.gradle.kts +15 -1
- package/compose/build.gradle.kts.backup +14 -0
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +66 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/ComposeApi.swift +2 -1
- package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
- package/ios/Application/Screen.swift +3 -2
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +0 -1
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +160 -147
- package/ios/Input/InputPhoneNumber.swift +12 -9
- package/ios/Input/InputSearch.swift +102 -75
- package/ios/Input/InputTextArea.swift +128 -145
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +33 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +123 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +312 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +97 -8
- package/ios/Navigation/BottomTab/BottomTab.swift +187 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
- package/ios/Navigation/Component/Header.swift +493 -0
- package/ios/Navigation/Component/HeaderBackground.swift +134 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +386 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +176 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
- package/ios/Navigation/HostBackGestureBridge.swift +57 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
- package/ios/Navigation/Navigation.swift +190 -0
- package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
- package/ios/Navigation/StackScreen.swift +782 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -136
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +140 -0
- package/ios/Popup/PopupNotify.swift +56 -73
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +6 -1
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/Text.swift +61 -19
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/package.json +1 -1
- package/scripts/gen-ios-icons.mjs +36 -0
- package/.claude/settings.local.json +0 -185
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/StackScreen.swift +0 -492
- package/local.properties +0 -8
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Navigator.swift
|
|
3
3
|
// MoMoUIKits
|
|
4
4
|
//
|
|
5
|
-
// SwiftUI mirror of Compose's Navigator
|
|
6
|
-
// Screens are registered by Int id; the
|
|
7
|
-
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/Navigator.kt`: Navigator, DynamicScreenRegistry and
|
|
6
|
+
// the overplay model. Screens are registered by Int id; the stack paths store routes by id and
|
|
7
|
+
// content is looked up from the registry on render.
|
|
8
8
|
//
|
|
9
9
|
|
|
10
10
|
import SwiftUI
|
|
@@ -31,6 +31,7 @@ public struct DynamicScreen {
|
|
|
31
31
|
|
|
32
32
|
public final class DynamicScreenRegistry {
|
|
33
33
|
private var screens: [Int: DynamicScreen] = [:]
|
|
34
|
+
private var navigations: [Int: WeakNavigation] = [:]
|
|
34
35
|
private var idCounter: Int = 1
|
|
35
36
|
|
|
36
37
|
public init() {}
|
|
@@ -49,12 +50,20 @@ public final class DynamicScreenRegistry {
|
|
|
49
50
|
|
|
50
51
|
public func unregister(id: Int) {
|
|
51
52
|
screens.removeValue(forKey: id)
|
|
53
|
+
navigations.removeValue(forKey: id)
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
public func unregisterAll() {
|
|
55
57
|
screens.removeAll()
|
|
58
|
+
navigations.removeAll()
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
func bind(_ navigation: Navigation) {
|
|
62
|
+
navigations[navigation.id] = WeakNavigation(value: navigation)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func navigation(id: Int) -> Navigation? { navigations[id]?.value }
|
|
66
|
+
|
|
58
67
|
public func getScreen(id: Int) -> DynamicScreen? { screens[id] }
|
|
59
68
|
|
|
60
69
|
public func latest() -> DynamicScreen? {
|
|
@@ -66,6 +75,10 @@ public final class DynamicScreenRegistry {
|
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
|
|
78
|
+
private struct WeakNavigation {
|
|
79
|
+
weak var value: Navigation?
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
// MARK: - Overplay
|
|
70
83
|
|
|
71
84
|
public enum OverplayType {
|
|
@@ -81,8 +94,14 @@ public struct OverplayItem: Identifiable {
|
|
|
81
94
|
// Bottom-sheet params (mirrors Compose's OverplayComponentParams.BottomSheet).
|
|
82
95
|
public var isSurface: Bool = false
|
|
83
96
|
public var header: BottomSheetHeader? = nil
|
|
97
|
+
public var draggable: Bool = true
|
|
98
|
+
public var useBottomInset: Bool = true
|
|
99
|
+
public var footer: (() -> AnyView)? = nil
|
|
84
100
|
// Snack-bar payload (mirrors Compose's SnackBar value).
|
|
85
101
|
public var snackBar: SnackBar? = nil
|
|
102
|
+
/// The presented screen it was shown over, nil for the main stack. Like Compose's overplay
|
|
103
|
+
/// root id, it pins the item to that context instead of following later presents/dismisses.
|
|
104
|
+
public internal(set) var presentedId: Int? = nil
|
|
86
105
|
}
|
|
87
106
|
|
|
88
107
|
// MARK: - Navigator
|
|
@@ -92,11 +111,24 @@ public final class Navigator: ObservableObject {
|
|
|
92
111
|
@Published public var path: [DynamicScreenRoute] = [] {
|
|
93
112
|
// Disposal hangs off the path so every route that leaves the stack — pop,
|
|
94
113
|
// replace, reset — is released the same way, after its exit transition.
|
|
95
|
-
didSet { disposeScreens(droppedFrom: oldValue) }
|
|
114
|
+
didSet { disposeScreens(droppedFrom: oldValue, keeping: path) }
|
|
115
|
+
}
|
|
116
|
+
@Published public var presented: DynamicDialogRoute? {
|
|
117
|
+
didSet { syncCover() }
|
|
118
|
+
}
|
|
119
|
+
/// Screens pushed while a screen is presented. Compose's dialog route is a plain back-stack
|
|
120
|
+
/// entry, so later pushes stack on top of it; the cover hosts its own stack for the same result.
|
|
121
|
+
@Published public var presentedPath: [DynamicScreenRoute] = [] {
|
|
122
|
+
didSet { disposeScreens(droppedFrom: oldValue, keeping: presentedPath) }
|
|
96
123
|
}
|
|
97
|
-
@Published public var presented: DynamicDialogRoute?
|
|
98
124
|
@Published public var overplay: OverplayItem?
|
|
99
125
|
|
|
126
|
+
/// What the cover shows. It trails `presented` while the previous cover is still sliding out:
|
|
127
|
+
/// SwiftUI can drop a presentation issued during another one's dismissal.
|
|
128
|
+
@Published private(set) var coverRoute: DynamicDialogRoute?
|
|
129
|
+
private var coverOnScreen = false
|
|
130
|
+
private var presentationGeneration = 0
|
|
131
|
+
|
|
100
132
|
/// Owned per-Navigator (mirrors Compose's `remember { DynamicScreenRegistry() }` +
|
|
101
133
|
/// disposal): screens registered by this navigator are released via ARC when the
|
|
102
134
|
/// navigator itself deallocates, instead of accumulating in a process-wide singleton.
|
|
@@ -108,8 +140,15 @@ public final class Navigator: ObservableObject {
|
|
|
108
140
|
/// actually removed via `finishOverplayDismiss`.
|
|
109
141
|
private var overplayDismissHandler: (() -> Void)?
|
|
110
142
|
|
|
111
|
-
public private(set) var rootRoute: DynamicScreenRoute
|
|
143
|
+
@Published public private(set) var rootRoute: DynamicScreenRoute
|
|
112
144
|
public var composeApi: KitComposeApi?
|
|
145
|
+
/// Host dismiss fallback when there is no `composeApi` (Compose's `maxApi.dismiss`).
|
|
146
|
+
public var maxApi: MaxApi?
|
|
147
|
+
|
|
148
|
+
/// Set by a bottom tab off its first tab: back on the screen hosting the tabs returns to the
|
|
149
|
+
/// first tab (Compose `tabBackHandler`). Keyed by that screen, so a screen pushed or presented
|
|
150
|
+
/// over the tabs still pops normally.
|
|
151
|
+
var tabBackHandler: (screenId: Int, handler: () -> Void)?
|
|
113
152
|
|
|
114
153
|
public init(
|
|
115
154
|
initialScreenName: String = "",
|
|
@@ -133,7 +172,7 @@ public final class Navigator: ObservableObject {
|
|
|
133
172
|
content: { AnyView(content()) },
|
|
134
173
|
options: options
|
|
135
174
|
)
|
|
136
|
-
|
|
175
|
+
appendToActiveStack(route)
|
|
137
176
|
}
|
|
138
177
|
|
|
139
178
|
public func push<V: View>(
|
|
@@ -146,12 +185,11 @@ public final class Navigator: ObservableObject {
|
|
|
146
185
|
content: { AnyView(content()) },
|
|
147
186
|
options: options
|
|
148
187
|
)
|
|
149
|
-
|
|
188
|
+
appendToActiveStack(route)
|
|
150
189
|
}
|
|
151
190
|
|
|
152
191
|
public func replace<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
|
|
153
|
-
guard
|
|
154
|
-
path.removeLast()
|
|
192
|
+
guard removeLastFromActiveStack() else { return }
|
|
155
193
|
push(content, options: options)
|
|
156
194
|
}
|
|
157
195
|
|
|
@@ -160,13 +198,37 @@ public final class Navigator: ObservableObject {
|
|
|
160
198
|
@ViewBuilder _ content: @escaping () -> V,
|
|
161
199
|
options: NavigationOptions? = nil
|
|
162
200
|
) {
|
|
163
|
-
guard
|
|
164
|
-
path.removeLast()
|
|
201
|
+
guard removeLastFromActiveStack() else { return }
|
|
165
202
|
push(screenName: screenName, content, options: options)
|
|
166
203
|
}
|
|
167
204
|
|
|
205
|
+
/// The presented flow while one is up, the main stack otherwise.
|
|
206
|
+
private func appendToActiveStack(_ route: DynamicScreenRoute) {
|
|
207
|
+
if presented != nil {
|
|
208
|
+
presentedPath.append(route)
|
|
209
|
+
} else {
|
|
210
|
+
path.append(route)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private func removeLastFromActiveStack() -> Bool {
|
|
215
|
+
if presented != nil {
|
|
216
|
+
// Compose pops the dialog route itself, so the replacement lands on the main stack.
|
|
217
|
+
if presentedPath.isEmpty {
|
|
218
|
+
clearPresented()
|
|
219
|
+
} else {
|
|
220
|
+
presentedPath.removeLast()
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
guard !path.isEmpty else { return false }
|
|
224
|
+
path.removeLast()
|
|
225
|
+
}
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
|
|
168
229
|
public func reset<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
|
|
169
230
|
let previousRoot = rootRoute.id
|
|
231
|
+
clearPresented()
|
|
170
232
|
path.removeAll()
|
|
171
233
|
let route = registry.register(
|
|
172
234
|
screenName: options?.screenName ?? "",
|
|
@@ -183,6 +245,7 @@ public final class Navigator: ObservableObject {
|
|
|
183
245
|
options: NavigationOptions? = nil
|
|
184
246
|
) {
|
|
185
247
|
let previousRoot = rootRoute.id
|
|
248
|
+
clearPresented()
|
|
186
249
|
path.removeAll()
|
|
187
250
|
let route = registry.register(
|
|
188
251
|
screenName: screenName,
|
|
@@ -241,38 +304,64 @@ public final class Navigator: ObservableObject {
|
|
|
241
304
|
@discardableResult
|
|
242
305
|
private func popScreen() -> Bool {
|
|
243
306
|
if presented != nil {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
307
|
+
if presentedPath.isEmpty {
|
|
308
|
+
clearPresented()
|
|
309
|
+
} else {
|
|
310
|
+
presentedPath.removeLast()
|
|
311
|
+
}
|
|
247
312
|
return true
|
|
248
313
|
} else if !path.isEmpty {
|
|
249
314
|
path.removeLast()
|
|
250
315
|
return true
|
|
251
316
|
} else {
|
|
252
|
-
|
|
317
|
+
requestHostDismiss()
|
|
253
318
|
return false
|
|
254
319
|
}
|
|
255
320
|
}
|
|
256
321
|
|
|
322
|
+
private func requestHostDismiss() {
|
|
323
|
+
if let composeApi {
|
|
324
|
+
composeApi.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
|
|
325
|
+
} else {
|
|
326
|
+
maxApi?.dismiss(completion: nil)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// Whether the active stack has a screen to go back to (Compose's `canPop`); at the root a
|
|
331
|
+
/// back dismisses the mini app instead.
|
|
332
|
+
public func canPop() -> Bool {
|
|
333
|
+
presented != nil || !path.isEmpty
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// Empties both stacks; a presented screen stays up on its own root.
|
|
257
337
|
public func popToRoot() {
|
|
338
|
+
presentedPath.removeAll()
|
|
258
339
|
path.removeAll()
|
|
259
340
|
}
|
|
260
341
|
|
|
261
342
|
/// The entry a back action applies to — mirrors Compose's `currentScreen()`
|
|
262
|
-
/// (`navController.currentBackStackEntry`):
|
|
343
|
+
/// (`navController.currentBackStackEntry`): the presented flow sits on top of
|
|
263
344
|
/// the stack, otherwise it is the last pushed screen, otherwise the root.
|
|
264
345
|
private func currentScreen() -> DynamicScreen? {
|
|
265
|
-
|
|
346
|
+
if let presented {
|
|
347
|
+
return registry.getScreen(id: presentedPath.last?.id ?? presented.id)
|
|
348
|
+
}
|
|
349
|
+
return registry.getScreen(id: path.last?.id ?? rootRoute.id)
|
|
266
350
|
}
|
|
267
351
|
|
|
268
352
|
public func onBackSafe(callback: (() -> Void)? = nil) {
|
|
269
353
|
let latest = currentScreen()
|
|
354
|
+
if let tabBackHandler, tabBackHandler.screenId == -1 || tabBackHandler.screenId == latest?.id {
|
|
355
|
+
tabBackHandler.handler()
|
|
356
|
+
return
|
|
357
|
+
}
|
|
270
358
|
if let onBack = latest?.options?.onBackHandler {
|
|
271
359
|
onBack()
|
|
272
360
|
return
|
|
273
361
|
}
|
|
274
362
|
if let preventBack = latest?.options?.headerBackProps?.preventBack {
|
|
275
|
-
|
|
363
|
+
// stored in `overplay`, so a strong self would cycle until the popup closes
|
|
364
|
+
showModal { [weak self] in
|
|
276
365
|
PopupNotify(
|
|
277
366
|
props: PopupNotifyProps(
|
|
278
367
|
image: preventBack.image,
|
|
@@ -281,7 +370,7 @@ public final class Navigator: ObservableObject {
|
|
|
281
370
|
error: preventBack.error,
|
|
282
371
|
primary: PopupAction(
|
|
283
372
|
title: preventBack.primary?.title ?? "",
|
|
284
|
-
onPress: {
|
|
373
|
+
onPress: {
|
|
285
374
|
preventBack.primary?.onPress?()
|
|
286
375
|
// Close the popup, then leave the screen (matches Compose's pop(2)).
|
|
287
376
|
self?.pop(2, callback: callback)
|
|
@@ -290,14 +379,14 @@ public final class Navigator: ObservableObject {
|
|
|
290
379
|
secondary: preventBack.secondary.map { secondary in
|
|
291
380
|
PopupAction(
|
|
292
381
|
title: secondary.title,
|
|
293
|
-
onPress: {
|
|
382
|
+
onPress: {
|
|
294
383
|
secondary.onPress?()
|
|
295
384
|
self?.hideOverplay()
|
|
296
385
|
}
|
|
297
386
|
)
|
|
298
387
|
},
|
|
299
388
|
buttonDirection: preventBack.buttonDirection,
|
|
300
|
-
onIconClose: {
|
|
389
|
+
onIconClose: {
|
|
301
390
|
preventBack.onIconClose?()
|
|
302
391
|
self?.hideOverplay()
|
|
303
392
|
},
|
|
@@ -318,7 +407,7 @@ public final class Navigator: ObservableObject {
|
|
|
318
407
|
content: { AnyView(content()) },
|
|
319
408
|
options: options
|
|
320
409
|
)
|
|
321
|
-
|
|
410
|
+
presentScreen(route)
|
|
322
411
|
}
|
|
323
412
|
|
|
324
413
|
public func present<V: View>(
|
|
@@ -331,14 +420,54 @@ public final class Navigator: ObservableObject {
|
|
|
331
420
|
content: { AnyView(content()) },
|
|
332
421
|
options: options
|
|
333
422
|
)
|
|
334
|
-
|
|
423
|
+
presentScreen(route)
|
|
335
424
|
}
|
|
336
425
|
|
|
337
426
|
public func dismiss() {
|
|
338
|
-
|
|
339
|
-
|
|
427
|
+
clearPresented()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// One cover per container: presenting from a presented flow stacks onto that flow, the way
|
|
431
|
+
/// Compose stacks a second dialog route.
|
|
432
|
+
private func presentScreen(_ route: DynamicScreenRoute) {
|
|
433
|
+
if presented != nil {
|
|
434
|
+
presentedPath.append(route)
|
|
435
|
+
} else {
|
|
436
|
+
presentationGeneration += 1
|
|
437
|
+
presentedPath = []
|
|
438
|
+
presented = DynamicDialogRoute(id: route.id)
|
|
340
439
|
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/// The cover keeps rendering its stack while it slides out, so the flow is torn down only
|
|
443
|
+
/// once the exit animation is over.
|
|
444
|
+
private func clearPresented() {
|
|
445
|
+
guard let id = presented?.id else { return }
|
|
341
446
|
presented = nil
|
|
447
|
+
let generation = presentationGeneration
|
|
448
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Self.presentedDisposeDelay) { [weak self] in
|
|
449
|
+
guard let self, self.presentationGeneration == generation else { return }
|
|
450
|
+
self.presentedPath.removeAll()
|
|
451
|
+
}
|
|
452
|
+
disposeScreens([id], after: Self.presentedDisposeDelay)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private func syncCover() {
|
|
456
|
+
if presented == nil {
|
|
457
|
+
coverRoute = nil
|
|
458
|
+
} else if !coverOnScreen || coverRoute != nil {
|
|
459
|
+
coverRoute = presented
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/// Reported by the container from the cover's appear / disappear.
|
|
464
|
+
func coverDidAppear() {
|
|
465
|
+
coverOnScreen = true
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
func coverDidDisappear() {
|
|
469
|
+
coverOnScreen = false
|
|
470
|
+
syncCover()
|
|
342
471
|
}
|
|
343
472
|
|
|
344
473
|
// MARK: Overplay
|
|
@@ -353,7 +482,8 @@ public final class Navigator: ObservableObject {
|
|
|
353
482
|
type: .modal,
|
|
354
483
|
content: { AnyView(content()) },
|
|
355
484
|
barrierDismissible: barrierDismissible,
|
|
356
|
-
onDismiss: onDismiss
|
|
485
|
+
onDismiss: onDismiss,
|
|
486
|
+
presentedId: presented?.id
|
|
357
487
|
)
|
|
358
488
|
}
|
|
359
489
|
|
|
@@ -362,7 +492,10 @@ public final class Navigator: ObservableObject {
|
|
|
362
492
|
isSurface: Bool = false,
|
|
363
493
|
barrierDismissible: Bool = true,
|
|
364
494
|
header: BottomSheetHeader? = nil,
|
|
365
|
-
onDismiss: (() -> Void)? = nil
|
|
495
|
+
onDismiss: (() -> Void)? = nil,
|
|
496
|
+
draggable: Bool = true,
|
|
497
|
+
useBottomInset: Bool = true,
|
|
498
|
+
footer: (() -> AnyView)? = nil
|
|
366
499
|
) {
|
|
367
500
|
overplayDismissHandler = nil
|
|
368
501
|
overplay = OverplayItem(
|
|
@@ -371,7 +504,11 @@ public final class Navigator: ObservableObject {
|
|
|
371
504
|
barrierDismissible: barrierDismissible,
|
|
372
505
|
onDismiss: onDismiss,
|
|
373
506
|
isSurface: isSurface,
|
|
374
|
-
header: header
|
|
507
|
+
header: header,
|
|
508
|
+
draggable: draggable,
|
|
509
|
+
useBottomInset: useBottomInset,
|
|
510
|
+
footer: footer,
|
|
511
|
+
presentedId: presented?.id
|
|
375
512
|
)
|
|
376
513
|
}
|
|
377
514
|
|
|
@@ -383,7 +520,8 @@ public final class Navigator: ObservableObject {
|
|
|
383
520
|
content: { AnyView(EmptyView()) },
|
|
384
521
|
barrierDismissible: false,
|
|
385
522
|
onDismiss: onDismiss,
|
|
386
|
-
snackBar: snackBar
|
|
523
|
+
snackBar: snackBar,
|
|
524
|
+
presentedId: presented?.id
|
|
387
525
|
)
|
|
388
526
|
}
|
|
389
527
|
|
|
@@ -419,6 +557,14 @@ public final class Navigator: ObservableObject {
|
|
|
419
557
|
if overplay?.type == .snackBar { hideOverplay() }
|
|
420
558
|
}
|
|
421
559
|
|
|
560
|
+
/// Room the current screen's footer (or navigation bar) takes from the window bottom.
|
|
561
|
+
func snackBarBottomInset() -> CGFloat {
|
|
562
|
+
guard let id = currentScreen()?.id, let navigation = registry.navigation(id: id) else {
|
|
563
|
+
return NavigationFooter.navigationBarInset
|
|
564
|
+
}
|
|
565
|
+
return navigation.bottomObstruction
|
|
566
|
+
}
|
|
567
|
+
|
|
422
568
|
// MARK: Screen disposal
|
|
423
569
|
|
|
424
570
|
/// Matches the `delay(300)` Compose waits in `dismissScreen` before unregistering,
|
|
@@ -429,20 +575,23 @@ public final class Navigator: ObservableObject {
|
|
|
429
575
|
/// after the component has taken itself down.
|
|
430
576
|
private static let overplayDismissDelay: TimeInterval = 0.3
|
|
431
577
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
578
|
+
/// A full-screen cover slides out slower than a stack pop.
|
|
579
|
+
private static let presentedDisposeDelay: TimeInterval = 0.6
|
|
580
|
+
|
|
581
|
+
private func disposeScreens(droppedFrom previous: [DynamicScreenRoute], keeping current: [DynamicScreenRoute]) {
|
|
582
|
+
let kept = Set(current.map(\.id))
|
|
583
|
+
disposeScreens(previous.map(\.id).filter { !kept.contains($0) })
|
|
435
584
|
}
|
|
436
585
|
|
|
437
586
|
/// Screens are released only once the pop transition has played out: the outgoing
|
|
438
587
|
/// view is still on screen while it animates and the registry is what renders it —
|
|
439
588
|
/// dropping it right away blanks the screen mid-transition. Liveness is re-checked
|
|
440
589
|
/// when the timer fires, so a route pushed back before then keeps its screen.
|
|
441
|
-
private func disposeScreens(_ ids: [Int]) {
|
|
590
|
+
private func disposeScreens(_ ids: [Int], after delay: TimeInterval = Navigator.screenDisposeDelay) {
|
|
442
591
|
guard !ids.isEmpty else { return }
|
|
443
|
-
DispatchQueue.main.asyncAfter(deadline: .now() +
|
|
592
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
444
593
|
guard let self else { return }
|
|
445
|
-
var live = Set(self.path.map(\.id))
|
|
594
|
+
var live = Set(self.path.map(\.id)).union(self.presentedPath.map(\.id))
|
|
446
595
|
live.insert(self.rootRoute.id)
|
|
447
596
|
if let presented = self.presented { live.insert(presented.id) }
|
|
448
597
|
for id in ids where !live.contains(id) {
|