@momo-kits/native-kits 0.162.2-debug → 0.162.2-sp.1-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/settings.local.json +55 -0
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/Components.swift +34 -43
- package/ios/Application/FloatingButton.swift +10 -8
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +102 -0
- package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +215 -0
- package/ios/Application/Navigation/BottomTab/CurvedContainer.swift +65 -0
- package/ios/Application/Navigation/HeaderBackProps.swift +66 -0
- package/ios/Application/Navigation/HeaderTitle.swift +57 -0
- package/ios/Application/Navigation/HeaderUser.swift +209 -0
- package/ios/Application/Navigation/NavigationContainer.swift +151 -0
- package/ios/Application/Navigation/NavigationOptions.swift +109 -0
- package/ios/Application/Navigation/Navigator.swift +360 -0
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +176 -0
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +68 -0
- package/ios/Application/Navigation/Overplay/SnackBar.swift +144 -0
- package/ios/Application/Navigation/component/Header.swift +178 -0
- package/ios/Application/Navigation/component/HeaderBackground.swift +83 -0
- package/ios/Application/Navigation/component/HeaderColor.swift +65 -0
- package/ios/Application/StackScreen.swift +449 -0
- package/ios/Avatar/Avatar.swift +193 -0
- package/ios/Badge/Badge.swift +11 -13
- package/ios/Badge/BadgeRibbon.swift +12 -19
- package/ios/BaselineView/BaselineView.swift +163 -0
- package/ios/Button/Button.swift +62 -23
- package/ios/Carousel/Carousel.swift +181 -0
- package/ios/Checkbox/Checkbox.swift +14 -12
- package/ios/Chip/Chip.swift +23 -9
- package/ios/Collapse/Collapse.swift +182 -0
- package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
- package/ios/Colors+Radius+Spacing/Theme.swift +295 -0
- package/ios/DateTimePicker/DateTimePicker.swift +210 -0
- package/ios/DateTimePicker/DateTimePickerTypes.swift +55 -0
- package/ios/DateTimePicker/DateTimePickerUtils.swift +167 -0
- package/ios/DateTimePicker/WheelPicker.swift +161 -0
- package/ios/Divider/Divider.swift +16 -0
- package/ios/Icon/Icon.swift +6 -1
- package/ios/IconButton/IconButton.swift +112 -0
- package/ios/Information/Information.swift +151 -0
- package/ios/Input/Input.swift +42 -25
- package/ios/Input/InputPhoneNumber.swift +21 -19
- package/ios/Input/InputSearch.swift +101 -45
- package/ios/Input/InputTextArea.swift +67 -40
- package/ios/InputDropDown/InputDropDown.swift +198 -0
- package/ios/InputMoney/InputMoney.swift +335 -0
- package/ios/InputOTP/InputOTP.swift +210 -0
- package/ios/Loader/Loader.swift +113 -0
- package/ios/MoMoUIKits.podspec +5 -0
- package/ios/OTPKeyboard/KeyboardButton.swift +1 -1
- package/ios/Pagination/PaginationDot.swift +61 -0
- package/ios/Pagination/PaginationNumber.swift +35 -0
- package/ios/Pagination/PaginationScroll.swift +101 -0
- package/ios/Pagination/PaginationWhiteDot.swift +37 -0
- package/ios/Popup/PopupDisplay.swift +10 -12
- package/ios/Popup/PopupInput.swift +2 -6
- package/ios/Popup/PopupNotify.swift +219 -0
- package/ios/ProgressInfo/ProgressInfo.swift +294 -0
- package/ios/Radio/Radio.swift +70 -0
- package/ios/Rating/Rating.swift +82 -0
- package/ios/Skeleton/Skeleton.swift +99 -0
- package/ios/Slider/Slider.swift +237 -0
- package/ios/Stepper/Stepper.swift +220 -0
- package/ios/Steps/Steps.swift +451 -0
- package/ios/SuggestAction/SuggestAction.swift +97 -0
- package/ios/Swipeable/SwipeCell.swift +185 -0
- package/ios/Swipeable/SwipeCellModel.swift +21 -0
- package/ios/TabView/TabView.swift +530 -0
- package/ios/Tag/Tag.swift +98 -0
- package/ios/Template/TrustBanner/TrustBanner.swift +2 -0
- package/ios/Title/Title.swift +269 -0
- package/ios/Tooltip/Tooltip.swift +482 -0
- package/ios/Typography/Text.swift +77 -5
- package/ios/Typography/Typography.swift +9 -0
- package/ios/Uploader/Uploader.swift +183 -0
- package/ios/native-kits.podspec +58 -8
- package/ios-demo/MoMoUIKitsDemo.podspec +18 -0
- package/ios-demo/README.md +120 -0
- package/ios-demo/Resources/dot_loading.json +558 -0
- package/ios-demo/Resources/icon.json +4052 -0
- package/ios-demo/Resources/lottie_circle_loader.json +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/DemoScaffold.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsBadgeDemoView.swift +25 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsButtonDemoView.swift +26 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsCheckboxDemoView.swift +19 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsChipDemoView.swift +33 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsInputDemoView.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsPopupDemoView.swift +21 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsRadioDemoView.swift +17 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsTypographyDemoView.swift +28 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoContext.swift +31 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoFactory.swift +72 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AnimatedHeaderDemo.swift +110 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +269 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BaselineView.swift +117 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BottomTabDemo.swift +89 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +126 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +63 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +20 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +124 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +14 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/HeaderUserDemo.swift +49 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +261 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +58 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +78 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +138 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +111 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +136 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +73 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +57 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +70 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +227 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +72 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +175 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +121 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +89 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +56 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +150 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +138 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +103 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +94 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +79 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +152 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +33 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +190 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +34 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +182 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +109 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +187 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +87 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +131 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +50 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoModels.swift +15 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoRegistry.swift +73 -0
- package/local.properties +8 -0
- package/package.json +1 -1
- package/publish.sh +50 -0
- package/scripts/gen-ios-color.mjs +136 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Navigator.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's Navigator + DynamicScreenRegistry.
|
|
6
|
+
// Screens are registered by Int id; the NavigationStack path stores routes
|
|
7
|
+
// by id and looks up content from the registry on render.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
import Combine
|
|
12
|
+
|
|
13
|
+
// MARK: - Routes
|
|
14
|
+
|
|
15
|
+
public struct DynamicScreenRoute: Hashable, Identifiable {
|
|
16
|
+
public let id: Int
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public struct DynamicDialogRoute: Hashable, Identifiable {
|
|
20
|
+
public let id: Int
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// MARK: - Registry
|
|
24
|
+
|
|
25
|
+
public struct DynamicScreen {
|
|
26
|
+
public let id: Int
|
|
27
|
+
public let name: String
|
|
28
|
+
public let content: () -> AnyView
|
|
29
|
+
public var options: NavigationOptions?
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public final class DynamicScreenRegistry {
|
|
33
|
+
public static let shared = DynamicScreenRegistry()
|
|
34
|
+
|
|
35
|
+
private var screens: [Int: DynamicScreen] = [:]
|
|
36
|
+
private var idCounter: Int = 1
|
|
37
|
+
|
|
38
|
+
private init() {}
|
|
39
|
+
|
|
40
|
+
@discardableResult
|
|
41
|
+
public func register(
|
|
42
|
+
screenName: String = "",
|
|
43
|
+
content: @escaping () -> AnyView,
|
|
44
|
+
options: NavigationOptions? = nil
|
|
45
|
+
) -> DynamicScreenRoute {
|
|
46
|
+
let id = idCounter
|
|
47
|
+
idCounter += 1
|
|
48
|
+
screens[id] = DynamicScreen(id: id, name: screenName, content: content, options: options)
|
|
49
|
+
return DynamicScreenRoute(id: id)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public func unregister(id: Int) {
|
|
53
|
+
screens.removeValue(forKey: id)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public func unregisterAll() {
|
|
57
|
+
screens.removeAll()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public func getScreen(id: Int) -> DynamicScreen? { screens[id] }
|
|
61
|
+
|
|
62
|
+
public func latest() -> DynamicScreen? {
|
|
63
|
+
screens.max(by: { $0.key < $1.key })?.value
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public func setOptions(id: Int, options: NavigationOptions) {
|
|
67
|
+
screens[id]?.options = options
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// MARK: - Overplay
|
|
72
|
+
|
|
73
|
+
public enum OverplayType {
|
|
74
|
+
case modal, bottomSheet, snackBar
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public struct OverplayItem: Identifiable {
|
|
78
|
+
public let id = UUID()
|
|
79
|
+
public let type: OverplayType
|
|
80
|
+
public let content: () -> AnyView
|
|
81
|
+
public let barrierDismissible: Bool
|
|
82
|
+
public let onDismiss: (() -> Void)?
|
|
83
|
+
// Bottom-sheet params (mirrors Compose's OverplayComponentParams.BottomSheet).
|
|
84
|
+
public var isSurface: Bool = false
|
|
85
|
+
public var header: BottomSheetHeader? = nil
|
|
86
|
+
// Snack-bar payload (mirrors Compose's SnackBar value).
|
|
87
|
+
public var snackBar: SnackBar? = nil
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MARK: - Navigator
|
|
91
|
+
|
|
92
|
+
@available(iOS 16.0, *)
|
|
93
|
+
public final class Navigator: ObservableObject {
|
|
94
|
+
@Published public var path: [DynamicScreenRoute] = []
|
|
95
|
+
@Published public var presented: DynamicDialogRoute?
|
|
96
|
+
@Published public var overplay: OverplayItem?
|
|
97
|
+
|
|
98
|
+
/// Animated-close handler registered by the active overplay component
|
|
99
|
+
/// (mirrors Compose's `OverplayComponentRegistry.bindClose`). When set, a
|
|
100
|
+
/// dismiss request runs the component's exit animation before the item is
|
|
101
|
+
/// actually removed via `finishOverplayDismiss`.
|
|
102
|
+
private var overplayDismissHandler: (() -> Void)?
|
|
103
|
+
|
|
104
|
+
public private(set) var rootRoute: DynamicScreenRoute
|
|
105
|
+
|
|
106
|
+
public init(
|
|
107
|
+
initialScreenName: String = "",
|
|
108
|
+
initialContent: @escaping () -> AnyView,
|
|
109
|
+
options: NavigationOptions? = nil
|
|
110
|
+
) {
|
|
111
|
+
self.rootRoute = DynamicScreenRegistry.shared.register(
|
|
112
|
+
screenName: initialScreenName,
|
|
113
|
+
content: initialContent,
|
|
114
|
+
options: options
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// MARK: Stack
|
|
119
|
+
|
|
120
|
+
public func push<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
|
|
121
|
+
let route = DynamicScreenRegistry.shared.register(
|
|
122
|
+
screenName: options?.screenName ?? "",
|
|
123
|
+
content: { AnyView(content()) },
|
|
124
|
+
options: options
|
|
125
|
+
)
|
|
126
|
+
path.append(route)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public func push<V: View>(
|
|
130
|
+
screenName: String,
|
|
131
|
+
@ViewBuilder _ content: @escaping () -> V,
|
|
132
|
+
options: NavigationOptions? = nil
|
|
133
|
+
) {
|
|
134
|
+
let route = DynamicScreenRegistry.shared.register(
|
|
135
|
+
screenName: screenName,
|
|
136
|
+
content: { AnyView(content()) },
|
|
137
|
+
options: options
|
|
138
|
+
)
|
|
139
|
+
path.append(route)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public func replace<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
|
|
143
|
+
guard !path.isEmpty else { return }
|
|
144
|
+
let removed = path.removeLast()
|
|
145
|
+
DynamicScreenRegistry.shared.unregister(id: removed.id)
|
|
146
|
+
push(content, options: options)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public func replace<V: View>(
|
|
150
|
+
screenName: String,
|
|
151
|
+
@ViewBuilder _ content: @escaping () -> V,
|
|
152
|
+
options: NavigationOptions? = nil
|
|
153
|
+
) {
|
|
154
|
+
guard !path.isEmpty else { return }
|
|
155
|
+
let removed = path.removeLast()
|
|
156
|
+
DynamicScreenRegistry.shared.unregister(id: removed.id)
|
|
157
|
+
push(screenName: screenName, content, options: options)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public func reset<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
|
|
161
|
+
for route in path { DynamicScreenRegistry.shared.unregister(id: route.id) }
|
|
162
|
+
DynamicScreenRegistry.shared.unregister(id: rootRoute.id)
|
|
163
|
+
path.removeAll()
|
|
164
|
+
let route = DynamicScreenRegistry.shared.register(
|
|
165
|
+
screenName: options?.screenName ?? "",
|
|
166
|
+
content: { AnyView(content()) },
|
|
167
|
+
options: options
|
|
168
|
+
)
|
|
169
|
+
rootRoute = route
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public func reset<V: View>(
|
|
173
|
+
screenName: String,
|
|
174
|
+
@ViewBuilder _ content: @escaping () -> V,
|
|
175
|
+
options: NavigationOptions? = nil
|
|
176
|
+
) {
|
|
177
|
+
for route in path { DynamicScreenRegistry.shared.unregister(id: route.id) }
|
|
178
|
+
DynamicScreenRegistry.shared.unregister(id: rootRoute.id)
|
|
179
|
+
path.removeAll()
|
|
180
|
+
let route = DynamicScreenRegistry.shared.register(
|
|
181
|
+
screenName: screenName,
|
|
182
|
+
content: { AnyView(content()) },
|
|
183
|
+
options: options
|
|
184
|
+
)
|
|
185
|
+
rootRoute = route
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public func pop(_ count: Int = 1, callback: (() -> Void)? = nil) {
|
|
189
|
+
var remaining = count
|
|
190
|
+
while remaining > 0 {
|
|
191
|
+
if overplay != nil {
|
|
192
|
+
hideOverplay()
|
|
193
|
+
} else if presented != nil {
|
|
194
|
+
let dismissedId = presented?.id
|
|
195
|
+
presented = nil
|
|
196
|
+
if let id = dismissedId { DynamicScreenRegistry.shared.unregister(id: id) }
|
|
197
|
+
} else if !path.isEmpty {
|
|
198
|
+
let removed = path.removeLast()
|
|
199
|
+
DynamicScreenRegistry.shared.unregister(id: removed.id)
|
|
200
|
+
} else {
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
remaining -= 1
|
|
204
|
+
}
|
|
205
|
+
callback?()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public func popToRoot() {
|
|
209
|
+
for route in path { DynamicScreenRegistry.shared.unregister(id: route.id) }
|
|
210
|
+
path.removeAll()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public func onBackSafe(callback: (() -> Void)? = nil) {
|
|
214
|
+
let latest = DynamicScreenRegistry.shared.latest()
|
|
215
|
+
if let onBack = latest?.options?.onBackHandler {
|
|
216
|
+
onBack()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
if let preventBack = latest?.options?.headerBackProps?.preventBack {
|
|
220
|
+
showModal {
|
|
221
|
+
PopupDisplay(
|
|
222
|
+
isPresented: .constant(true),
|
|
223
|
+
title: preventBack.title,
|
|
224
|
+
description: preventBack.description,
|
|
225
|
+
url: preventBack.image,
|
|
226
|
+
buttonDirection: preventBack.buttonDirection,
|
|
227
|
+
buttonType: .button,
|
|
228
|
+
actionButtonTitle: preventBack.primary?.title ?? "",
|
|
229
|
+
closeButtonTitle: preventBack.secondary?.title ?? "",
|
|
230
|
+
onPressAction: { [weak self] in
|
|
231
|
+
preventBack.primary?.onPress?()
|
|
232
|
+
// Close the popup, then leave the screen (matches Compose's pop(2)).
|
|
233
|
+
self?.pop(2, callback: callback)
|
|
234
|
+
},
|
|
235
|
+
onPressCloseButton: { [weak self] in
|
|
236
|
+
preventBack.secondary?.onPress?()
|
|
237
|
+
self?.hideOverplay()
|
|
238
|
+
},
|
|
239
|
+
onClose: { [weak self] in
|
|
240
|
+
preventBack.onIconClose?()
|
|
241
|
+
self?.hideOverplay()
|
|
242
|
+
},
|
|
243
|
+
errorCode: preventBack.error,
|
|
244
|
+
isShowCloseIcon: preventBack.isShowCloseIcon
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
pop(1, callback: callback)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// MARK: Modal-style screen (vertical slide)
|
|
253
|
+
|
|
254
|
+
public func present<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
|
|
255
|
+
let route = DynamicScreenRegistry.shared.register(
|
|
256
|
+
screenName: options?.screenName ?? "",
|
|
257
|
+
content: { AnyView(content()) },
|
|
258
|
+
options: options
|
|
259
|
+
)
|
|
260
|
+
presented = DynamicDialogRoute(id: route.id)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
public func present<V: View>(
|
|
264
|
+
screenName: String,
|
|
265
|
+
@ViewBuilder _ content: @escaping () -> V,
|
|
266
|
+
options: NavigationOptions? = nil
|
|
267
|
+
) {
|
|
268
|
+
let route = DynamicScreenRegistry.shared.register(
|
|
269
|
+
screenName: screenName,
|
|
270
|
+
content: { AnyView(content()) },
|
|
271
|
+
options: options
|
|
272
|
+
)
|
|
273
|
+
presented = DynamicDialogRoute(id: route.id)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public func dismiss() {
|
|
277
|
+
if let id = presented?.id {
|
|
278
|
+
DynamicScreenRegistry.shared.unregister(id: id)
|
|
279
|
+
}
|
|
280
|
+
presented = nil
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// MARK: Overplay
|
|
284
|
+
|
|
285
|
+
public func showModal<V: View>(
|
|
286
|
+
@ViewBuilder _ content: @escaping () -> V,
|
|
287
|
+
barrierDismissible: Bool = true,
|
|
288
|
+
onDismiss: (() -> Void)? = nil
|
|
289
|
+
) {
|
|
290
|
+
overplayDismissHandler = nil
|
|
291
|
+
overplay = OverplayItem(
|
|
292
|
+
type: .modal,
|
|
293
|
+
content: { AnyView(content()) },
|
|
294
|
+
barrierDismissible: barrierDismissible,
|
|
295
|
+
onDismiss: onDismiss
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public func showBottomSheet<V: View>(
|
|
300
|
+
@ViewBuilder _ content: @escaping () -> V,
|
|
301
|
+
isSurface: Bool = false,
|
|
302
|
+
barrierDismissible: Bool = true,
|
|
303
|
+
header: BottomSheetHeader? = nil,
|
|
304
|
+
onDismiss: (() -> Void)? = nil
|
|
305
|
+
) {
|
|
306
|
+
overplayDismissHandler = nil
|
|
307
|
+
overplay = OverplayItem(
|
|
308
|
+
type: .bottomSheet,
|
|
309
|
+
content: { AnyView(content()) },
|
|
310
|
+
barrierDismissible: barrierDismissible,
|
|
311
|
+
onDismiss: onDismiss,
|
|
312
|
+
isSurface: isSurface,
|
|
313
|
+
header: header
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Mirrors Compose's `showSnackBar(snackBar:)`.
|
|
318
|
+
public func showSnackBar(_ snackBar: SnackBar, onDismiss: (() -> Void)? = nil) {
|
|
319
|
+
overplayDismissHandler = nil
|
|
320
|
+
overplay = OverplayItem(
|
|
321
|
+
type: .snackBar,
|
|
322
|
+
content: { AnyView(EmptyView()) },
|
|
323
|
+
barrierDismissible: false,
|
|
324
|
+
onDismiss: onDismiss,
|
|
325
|
+
snackBar: snackBar
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// MARK: Overplay dismissal
|
|
330
|
+
|
|
331
|
+
/// Registered by the active overplay component so external dismiss requests
|
|
332
|
+
/// run its exit animation (mirrors Compose's `bindClose`).
|
|
333
|
+
public func registerOverplayDismiss(_ handler: @escaping () -> Void) {
|
|
334
|
+
overplayDismissHandler = handler
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// Removes the overplay and fires its `onDismiss` (mirrors Compose's
|
|
338
|
+
/// `hardClearAfterDismiss`). Called by a component once its exit animation
|
|
339
|
+
/// has completed.
|
|
340
|
+
public func finishOverplayDismiss() {
|
|
341
|
+
let dismissCallback = overplay?.onDismiss
|
|
342
|
+
overplayDismissHandler = nil
|
|
343
|
+
overplay = nil
|
|
344
|
+
dismissCallback?()
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
public func hideOverplay() {
|
|
348
|
+
if let close = overplayDismissHandler {
|
|
349
|
+
// Component runs its exit animation, then calls finishOverplayDismiss.
|
|
350
|
+
overplayDismissHandler = nil
|
|
351
|
+
close()
|
|
352
|
+
} else {
|
|
353
|
+
finishOverplayDismiss()
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
public func hideSnackBar() {
|
|
358
|
+
if overplay?.type == .snackBar { hideOverplay() }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
//
|
|
2
|
+
// BottomSheet.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `BottomSheet` (navigation/BottomSheet.kt).
|
|
6
|
+
// A sheet that slides up from the bottom over a dimmed scrim, with a drag
|
|
7
|
+
// handle, a header (title or custom), a divider and the content body.
|
|
8
|
+
// Dragging down past a threshold (or tapping the scrim) dismisses it.
|
|
9
|
+
// Driven by `Navigator.showBottomSheet`.
|
|
10
|
+
//
|
|
11
|
+
|
|
12
|
+
import SwiftUI
|
|
13
|
+
|
|
14
|
+
// MARK: - Header model
|
|
15
|
+
|
|
16
|
+
/// Mirrors Compose's `sealed class BottomHeader` (Title / Custom).
|
|
17
|
+
public struct BottomSheetHeader {
|
|
18
|
+
enum Style {
|
|
19
|
+
case title(String)
|
|
20
|
+
case custom(() -> AnyView)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let style: Style
|
|
24
|
+
|
|
25
|
+
/// Mirrors `BottomHeader.Title`.
|
|
26
|
+
public static func title(_ text: String = "Bottom Sheet Title") -> BottomSheetHeader {
|
|
27
|
+
BottomSheetHeader(style: .title(text))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Mirrors `BottomHeader.Custom`.
|
|
31
|
+
public static func custom<V: View>(@ViewBuilder _ content: @escaping () -> V) -> BottomSheetHeader {
|
|
32
|
+
BottomSheetHeader(style: .custom({ AnyView(content()) }))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: - Bottom sheet view
|
|
37
|
+
|
|
38
|
+
@available(iOS 16.0, *)
|
|
39
|
+
struct BottomSheetView<Content: View>: View {
|
|
40
|
+
let header: BottomSheetHeader
|
|
41
|
+
let isSurface: Bool
|
|
42
|
+
let barrierDismissible: Bool
|
|
43
|
+
let content: () -> Content
|
|
44
|
+
|
|
45
|
+
@EnvironmentObject private var navigator: Navigator
|
|
46
|
+
|
|
47
|
+
/// Vertical translation of the sheet; starts off-screen and animates to 0.
|
|
48
|
+
@State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
|
|
49
|
+
@State private var scrimAlpha: CGFloat = 0
|
|
50
|
+
@State private var sheetHeight: CGFloat = 0
|
|
51
|
+
@State private var didAppear = false
|
|
52
|
+
|
|
53
|
+
/// Mirrors Compose's `sheetCloseOffset` (100dp drag threshold to dismiss).
|
|
54
|
+
private let closeThreshold: CGFloat = 100
|
|
55
|
+
|
|
56
|
+
private var screenHeight: CGFloat { UIScreen.main.bounds.height }
|
|
57
|
+
|
|
58
|
+
private var backgroundColor: Color {
|
|
59
|
+
// Compose uses background.surface (white) for `isSurface`, otherwise
|
|
60
|
+
// background.default. Both resolve to white in the current iOS palette.
|
|
61
|
+
isSurface ? Colors.black01 : Colors.black01
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var body: some View {
|
|
65
|
+
ZStack(alignment: .bottom) {
|
|
66
|
+
// Dimmed scrim (max 0.3 alpha, mirrors Compose).
|
|
67
|
+
Color.black.opacity(scrimAlpha)
|
|
68
|
+
.ignoresSafeArea()
|
|
69
|
+
.contentShape(Rectangle())
|
|
70
|
+
.onTapGesture {
|
|
71
|
+
if barrierDismissible { close() }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
sheet
|
|
75
|
+
.offset(y: sheetOffset)
|
|
76
|
+
}
|
|
77
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
78
|
+
.onAppear {
|
|
79
|
+
guard !didAppear else { return }
|
|
80
|
+
didAppear = true
|
|
81
|
+
navigator.registerOverplayDismiss { close() }
|
|
82
|
+
open()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private var sheet: some View {
|
|
87
|
+
VStack(spacing: 0) {
|
|
88
|
+
// Drag handle + header (the draggable region).
|
|
89
|
+
VStack(spacing: 0) {
|
|
90
|
+
Spacer().frame(height: 8)
|
|
91
|
+
Capsule()
|
|
92
|
+
.fill(Colors.black06)
|
|
93
|
+
.frame(width: 40, height: 4)
|
|
94
|
+
Spacer().frame(height: 4)
|
|
95
|
+
headerView
|
|
96
|
+
.frame(height: 56)
|
|
97
|
+
.padding(.horizontal, Spacing.M)
|
|
98
|
+
}
|
|
99
|
+
.frame(maxWidth: .infinity)
|
|
100
|
+
.frame(height: 72)
|
|
101
|
+
.contentShape(Rectangle())
|
|
102
|
+
.gesture(dragGesture)
|
|
103
|
+
|
|
104
|
+
Rectangle()
|
|
105
|
+
.fill(Colors.black04)
|
|
106
|
+
.frame(height: 1)
|
|
107
|
+
|
|
108
|
+
content()
|
|
109
|
+
}
|
|
110
|
+
.frame(maxWidth: .infinity)
|
|
111
|
+
.frame(maxHeight: screenHeight - safeAreaTopInset() - 90, alignment: .bottom)
|
|
112
|
+
.background(
|
|
113
|
+
GeometryReader { geo in
|
|
114
|
+
Color.clear.onAppear { sheetHeight = geo.size.height }
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
.background(backgroundColor)
|
|
118
|
+
.clipShape(RoundedCorner(radius: Radius.M, corners: [.topLeft, .topRight]))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@ViewBuilder
|
|
122
|
+
private var headerView: some View {
|
|
123
|
+
switch header.style {
|
|
124
|
+
case .title(let text):
|
|
125
|
+
HStack(spacing: 0) {
|
|
126
|
+
Color.clear.frame(width: 24, height: 24)
|
|
127
|
+
MomoText(text, typography: .headerDefaultBold, color: Colors.black17)
|
|
128
|
+
.frame(maxWidth: .infinity, alignment: .center)
|
|
129
|
+
Icon(source: "navigation_close", size: 24, color: Colors.black17)
|
|
130
|
+
.frame(width: 24, height: 24)
|
|
131
|
+
.contentShape(Rectangle())
|
|
132
|
+
.onTapGesture { close() }
|
|
133
|
+
}
|
|
134
|
+
case .custom(let builder):
|
|
135
|
+
builder()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// MARK: - Drag handling (mirrors Compose's detectDragGestures)
|
|
140
|
+
|
|
141
|
+
private var dragGesture: some Gesture {
|
|
142
|
+
DragGesture()
|
|
143
|
+
.onChanged { value in
|
|
144
|
+
let newOffset = max(value.translation.height, 0)
|
|
145
|
+
sheetOffset = newOffset
|
|
146
|
+
}
|
|
147
|
+
.onEnded { _ in
|
|
148
|
+
if sheetOffset > closeThreshold {
|
|
149
|
+
close()
|
|
150
|
+
} else {
|
|
151
|
+
withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
|
|
152
|
+
sheetOffset = 0
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// MARK: - Open / close animations
|
|
159
|
+
|
|
160
|
+
private func open() {
|
|
161
|
+
DispatchQueue.main.async {
|
|
162
|
+
withAnimation(.easeOut(duration: 0.1)) { scrimAlpha = 0.3 }
|
|
163
|
+
withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.35)) { sheetOffset = 0 }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private func close() {
|
|
168
|
+
withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.2)) {
|
|
169
|
+
sheetOffset = max(sheetHeight, screenHeight)
|
|
170
|
+
scrimAlpha = 0
|
|
171
|
+
}
|
|
172
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
173
|
+
navigator.finishOverplayDismiss()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ModalScreen.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `ModalScreen` (navigation/ModalScreen.kt).
|
|
6
|
+
// A centered overlay that fades + scales in over a dimmed scrim, and animates
|
|
7
|
+
// back out before the overplay is removed. Driven by `Navigator.showModal`.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
@available(iOS 16.0, *)
|
|
13
|
+
struct ModalScreen<Content: View>: View {
|
|
14
|
+
let barrierDismissible: Bool
|
|
15
|
+
let content: () -> Content
|
|
16
|
+
|
|
17
|
+
@EnvironmentObject private var navigator: Navigator
|
|
18
|
+
|
|
19
|
+
@State private var scrimOpacity: CGFloat = 0
|
|
20
|
+
@State private var contentScale: CGFloat = 0.8
|
|
21
|
+
@State private var contentOpacity: CGFloat = 0
|
|
22
|
+
@State private var didAppear = false
|
|
23
|
+
|
|
24
|
+
var body: some View {
|
|
25
|
+
ZStack {
|
|
26
|
+
// Dimmed scrim (mirrors `Color.Black.copy(alpha = 0.6f * alpha)`).
|
|
27
|
+
Color.black.opacity(0.6 * scrimOpacity)
|
|
28
|
+
.ignoresSafeArea()
|
|
29
|
+
.contentShape(Rectangle())
|
|
30
|
+
.onTapGesture {
|
|
31
|
+
if barrierDismissible { navigator.hideOverplay() }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
content()
|
|
35
|
+
.scaleEffect(contentScale)
|
|
36
|
+
.opacity(contentOpacity)
|
|
37
|
+
}
|
|
38
|
+
.onAppear {
|
|
39
|
+
guard !didAppear else { return }
|
|
40
|
+
didAppear = true
|
|
41
|
+
navigator.registerOverplayDismiss { close() }
|
|
42
|
+
open()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Mirrors Compose's `openEvent`: 250ms fade + scale to 1.
|
|
47
|
+
private func open() {
|
|
48
|
+
DispatchQueue.main.async {
|
|
49
|
+
withAnimation(.easeOut(duration: 0.25)) {
|
|
50
|
+
scrimOpacity = 1
|
|
51
|
+
contentScale = 1
|
|
52
|
+
contentOpacity = 1
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Mirrors Compose's `closeEvent`: 200ms fade + scale back to 0.8, then remove.
|
|
58
|
+
private func close() {
|
|
59
|
+
withAnimation(.easeIn(duration: 0.2)) {
|
|
60
|
+
scrimOpacity = 0
|
|
61
|
+
contentScale = 0.8
|
|
62
|
+
contentOpacity = 0
|
|
63
|
+
}
|
|
64
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
65
|
+
navigator.finishOverplayDismiss()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|