@momo-kits/native-kits 0.163.1-beta.5-debug → 0.163.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.
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/Components.swift +16 -0
- package/ios/Application/Localize.swift +277 -0
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +1 -0
- package/ios/Application/Navigation/NavigationContainer.swift +6 -0
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +87 -19
- package/ios/Application/StackScreen.swift +8 -1
- package/ios/Colors+Radius+Spacing/Colors.swift +9 -6
- package/ios/Colors+Radius+Spacing/Spacing.swift +3 -1
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +188 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +2 -0
- package/package.json +1 -1
package/compose/build.gradle.kts
CHANGED
package/compose/compose.podspec
CHANGED
package/gradle.properties
CHANGED
|
@@ -143,6 +143,22 @@ public struct HeaderRightWidthKey: PreferenceKey {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// MARK: - Bottom-tab root preference
|
|
147
|
+
|
|
148
|
+
/// Set by `BottomTab` on appear so its enclosing `StackScreen` can detect that
|
|
149
|
+
/// it's hosting a self-contained bottom-tab surface and skip its own bottom
|
|
150
|
+
/// safe-area reservation — mirrors Compose's `Navigation.markAsBottomTabRoot()`
|
|
151
|
+
/// (`navigation.isBottomTabRoot`, read by `StackScreen.kt`'s `MainContent`).
|
|
152
|
+
/// Without this, `BottomTab`'s own internal safe-area handling gets an extra,
|
|
153
|
+
/// unwanted inset stacked on top of it by the outer `StackScreen`, pushing the
|
|
154
|
+
/// tab bar up higher than the true bottom inset.
|
|
155
|
+
public struct IsBottomTabRootKey: PreferenceKey {
|
|
156
|
+
public static var defaultValue: Bool = false
|
|
157
|
+
public static func reduce(value: inout Bool, nextValue: () -> Bool) {
|
|
158
|
+
value = value || nextValue()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
146
162
|
// MARK: - Header View
|
|
147
163
|
|
|
148
164
|
public struct Header: View {
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Localize.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `application/Localize.kt`, itself a port of
|
|
6
|
+
// momo-kits React Native's `Application/Localize.ts`. Holds the merged
|
|
7
|
+
// dictionary (kit defaults + host overrides) and the active language,
|
|
8
|
+
// switched at runtime via `changeLanguage`. `Localize` is an `ObservableObject`
|
|
9
|
+
// so a language switch republishes to consumers of `translate`/`translateData`,
|
|
10
|
+
// mirroring Compose's snapshot-state-backed `language`.
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
import Foundation
|
|
14
|
+
|
|
15
|
+
/// Translation dictionary keyed by language; both `vi` and `en` are required.
|
|
16
|
+
public struct LocalizationObject {
|
|
17
|
+
public var vi: [String: String]
|
|
18
|
+
public var en: [String: String]
|
|
19
|
+
|
|
20
|
+
public init(vi: [String: String] = [:], en: [String: String] = [:]) {
|
|
21
|
+
self.vi = vi
|
|
22
|
+
self.en = en
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public final class Localize: ObservableObject {
|
|
27
|
+
public static let VI = "vi"
|
|
28
|
+
public static let EN = "en"
|
|
29
|
+
|
|
30
|
+
private var assets: LocalizationObject
|
|
31
|
+
@Published public private(set) var currentLanguage: String = Localize.VI
|
|
32
|
+
|
|
33
|
+
public init(_ translations: LocalizationObject = LocalizationObject()) {
|
|
34
|
+
self.assets = Localize.merge(base: Localize.defaultLanguage, override: translations)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public func changeLanguage(_ language: String?) {
|
|
38
|
+
currentLanguage = (language == Localize.EN) ? Localize.EN : Localize.VI
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public func translate(_ key: String) -> String {
|
|
42
|
+
let dictionary = currentLanguage == Localize.EN ? assets.en : assets.vi
|
|
43
|
+
if let value = dictionary[key], !value.isEmpty { return value }
|
|
44
|
+
return key
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public func translateData(_ data: [String: String]) -> String {
|
|
48
|
+
if let value = data[currentLanguage] { return value }
|
|
49
|
+
let body = data.map { "\"\($0.key)\":\"\($0.value)\"" }.joined(separator: ",")
|
|
50
|
+
return "{\(body)}"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public func translateData(vi: String, en: String) -> String {
|
|
54
|
+
currentLanguage == Localize.EN ? en : vi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Existing keys win (mirrors Compose: `merge(translations, assets)`).
|
|
58
|
+
public func addTranslations(_ translations: LocalizationObject) {
|
|
59
|
+
assets = Localize.merge(base: translations, override: assets)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static func merge(base: LocalizationObject, override: LocalizationObject) -> LocalizationObject {
|
|
63
|
+
LocalizationObject(
|
|
64
|
+
vi: base.vi.merging(override.vi) { _, new in new },
|
|
65
|
+
en: base.en.merging(override.en) { _, new in new }
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private static let defaultLanguage = LocalizationObject(
|
|
70
|
+
vi: [
|
|
71
|
+
"errorCode": "Mã lỗi: ",
|
|
72
|
+
"seeMore": "Xem thêm",
|
|
73
|
+
"cancel": "Huỷ",
|
|
74
|
+
"done": "Xong",
|
|
75
|
+
"confirm": "Xác nhận",
|
|
76
|
+
"month": "tháng",
|
|
77
|
+
"day": "ngày",
|
|
78
|
+
"choose": "Chọn",
|
|
79
|
+
"sent": "Đã gửi",
|
|
80
|
+
"received": "Đã tiếp nhận",
|
|
81
|
+
"processing": "Đang xử lý",
|
|
82
|
+
"processed": "Đã xử lý",
|
|
83
|
+
"addImg": "Thêm hình",
|
|
84
|
+
"chooseBtn": "Chọn button",
|
|
85
|
+
"imgEg": "Hình ảnh tham khảo",
|
|
86
|
+
"filter": "Sắp xếp",
|
|
87
|
+
"shipping": "Đang giao hàng",
|
|
88
|
+
"men": "Nam",
|
|
89
|
+
"women": "Nữ",
|
|
90
|
+
"more": "Khác",
|
|
91
|
+
"expDate": "Ngày hết hạn",
|
|
92
|
+
"exp": "HSD",
|
|
93
|
+
"inActive": "Chưa kích hoạt",
|
|
94
|
+
"voucherRemindHour": "Hết hạn sau {hours} giờ",
|
|
95
|
+
"voucherRemindMinute": "Hết hạn sau {minutes} phút",
|
|
96
|
+
"voucherRemindSecond": "Hết hạn sau {seconds} giây",
|
|
97
|
+
"voucherRemindDay": "Hết hạn sau {days} ngày",
|
|
98
|
+
"chooseRoundtrip": "Chọn vé khứ hồi",
|
|
99
|
+
"depart": "Ngày đi",
|
|
100
|
+
"return": "Ngày về",
|
|
101
|
+
"departing": "Đi",
|
|
102
|
+
"returning": "Về",
|
|
103
|
+
"jan": "Tháng 1",
|
|
104
|
+
"feb": "Tháng 2",
|
|
105
|
+
"mar": "Tháng 3",
|
|
106
|
+
"apr": "Tháng 4",
|
|
107
|
+
"may": "Tháng 5",
|
|
108
|
+
"jun": "Tháng 6",
|
|
109
|
+
"jul": "Tháng 7",
|
|
110
|
+
"aug": "Tháng 8",
|
|
111
|
+
"sep": "Tháng 9",
|
|
112
|
+
"oct": "Tháng 10",
|
|
113
|
+
"nov": "Tháng 11",
|
|
114
|
+
"dec": "Tháng 12",
|
|
115
|
+
"mon": "T2",
|
|
116
|
+
"tue": "T3",
|
|
117
|
+
"wed": "T4",
|
|
118
|
+
"thu": "T5",
|
|
119
|
+
"fri": "T6",
|
|
120
|
+
"sat": "T7",
|
|
121
|
+
"sun": "CN",
|
|
122
|
+
"showLunar": "Hiển thị lịch âm",
|
|
123
|
+
"newYear": "Tết Dương lịch",
|
|
124
|
+
"valentine": "Ngày Lễ Tình nhân",
|
|
125
|
+
"womenDay": "Ngày Quốc tế Phụ nữ",
|
|
126
|
+
"liberationDay": "Ngày giải phóng Miền Nam thống nhất đất nước",
|
|
127
|
+
"laborDay": "Ngày Quốc tế Lao động",
|
|
128
|
+
"childrenDay": "Ngày Quốc tế Thiếu nhi",
|
|
129
|
+
"nationalDay": "Quốc khánh nước CHXHCN Việt Nam",
|
|
130
|
+
"womenDayVN": "Ngày Phụ nữ Việt Nam",
|
|
131
|
+
"teacherDay": "Ngày Nhà giáo Việt Nam",
|
|
132
|
+
"christmasEve": "Ngày Lễ Giáng Sinh",
|
|
133
|
+
"christmas": "Ngày Lễ Giáng Sinh",
|
|
134
|
+
"lunarNewYear": "Tết Nguyên Đán",
|
|
135
|
+
"hungKingDay": "Giỗ Tổ Hùng Vương",
|
|
136
|
+
"balance": "Số dư trong ví",
|
|
137
|
+
"chooseDate": "Chọn ngày tháng năm",
|
|
138
|
+
"year": "năm",
|
|
139
|
+
"hour": "giờ",
|
|
140
|
+
"minute": "phút",
|
|
141
|
+
"from": "Từ",
|
|
142
|
+
"to": "Đến",
|
|
143
|
+
"connected": "Đã kết nối",
|
|
144
|
+
"disconnected": "Mất kết nối",
|
|
145
|
+
"hide": "Ẩn",
|
|
146
|
+
"viewAllTitle": "Xem tất cả",
|
|
147
|
+
"headerTitle": "Lịch sử thanh toán",
|
|
148
|
+
"SHORTEN": "THU GỌN",
|
|
149
|
+
"viewMoreBank": "XEM THÊM (${otherItemCount} ngân hàng)",
|
|
150
|
+
"transaction_success": "Thành công",
|
|
151
|
+
"transaction_fail": "Thất bại",
|
|
152
|
+
"transaction_processing": "Đang xử lý",
|
|
153
|
+
"viewMore": "Xem thêm",
|
|
154
|
+
"shorten": "Thu gọn",
|
|
155
|
+
"Payment": "Thanh toán ${serviceName}",
|
|
156
|
+
"currencyUnit": "đ",
|
|
157
|
+
"Month": "Tháng",
|
|
158
|
+
"save": "Lưu",
|
|
159
|
+
"favoriteIn": "Thêm dịch vụ yêu thích",
|
|
160
|
+
"favoriteOut": "Xóa dịch vụ yêu thích",
|
|
161
|
+
"deviceIn": "Thêm vào thiết bị",
|
|
162
|
+
"setting": "Cài đặt",
|
|
163
|
+
"transaction": "Lịch sử giao dịch",
|
|
164
|
+
"share": "Chia sẽ dịch vụ",
|
|
165
|
+
"information": "Thông tin chung",
|
|
166
|
+
"tutorial": "Hướng dẫn sử dụng",
|
|
167
|
+
"question": "Câu hỏi thường gặp",
|
|
168
|
+
"support": "Trung tâm hỗ trợ",
|
|
169
|
+
"skip": "Bỏ qua",
|
|
170
|
+
"enterPhoneNumber": "Vui lòng nhập số điện thoại",
|
|
171
|
+
"invalidPhoneNumber": "Số điện thoại không đúng",
|
|
172
|
+
],
|
|
173
|
+
en: [
|
|
174
|
+
"seeMore": "See more",
|
|
175
|
+
"cancel": "Cancel",
|
|
176
|
+
"done": "Done",
|
|
177
|
+
"confirm": "Confirm",
|
|
178
|
+
"month": "month",
|
|
179
|
+
"day": "day",
|
|
180
|
+
"choose": "Choose",
|
|
181
|
+
"sent": "Sent",
|
|
182
|
+
"received": "Received",
|
|
183
|
+
"processing": "Processing",
|
|
184
|
+
"processed": "Done",
|
|
185
|
+
"addImg": "Add image",
|
|
186
|
+
"chooseBtn": "Choose button",
|
|
187
|
+
"imgEg": "Example images",
|
|
188
|
+
"filter": "Filter",
|
|
189
|
+
"shipping": "Shipping",
|
|
190
|
+
"men": "Men",
|
|
191
|
+
"women": "Women",
|
|
192
|
+
"more": "More",
|
|
193
|
+
"expDate": "Expiration date",
|
|
194
|
+
"exp": "EXP",
|
|
195
|
+
"inActive": "Not Activated",
|
|
196
|
+
"voucherRemindHour": "Revoke after {hours} hours",
|
|
197
|
+
"voucherRemindMinute": "Revoke after {minutes} minutes",
|
|
198
|
+
"voucherRemindSecond": "Revoke after {seconds} seconds",
|
|
199
|
+
"voucherRemindDay": "Revoke after {days} days",
|
|
200
|
+
"chooseRoundtrip": "Choose roundtrip ticket",
|
|
201
|
+
"depart": "Departing",
|
|
202
|
+
"return": "Returning",
|
|
203
|
+
"departing": "De",
|
|
204
|
+
"returning": "Re",
|
|
205
|
+
"jan": "January",
|
|
206
|
+
"feb": "February",
|
|
207
|
+
"mar": "March",
|
|
208
|
+
"apr": "April",
|
|
209
|
+
"may": "May",
|
|
210
|
+
"jun": "June",
|
|
211
|
+
"jul": "July",
|
|
212
|
+
"aug": "August",
|
|
213
|
+
"sep": "September",
|
|
214
|
+
"oct": "October",
|
|
215
|
+
"nov": "November",
|
|
216
|
+
"dec": "December",
|
|
217
|
+
"mon": "Mon",
|
|
218
|
+
"tue": "Tue",
|
|
219
|
+
"wed": "Wed",
|
|
220
|
+
"thu": "Thu",
|
|
221
|
+
"fri": "Fri",
|
|
222
|
+
"sat": "Sat",
|
|
223
|
+
"sun": "Sun",
|
|
224
|
+
"showLunar": "Show Lunar Calendar",
|
|
225
|
+
"newYear": "New Year's Day",
|
|
226
|
+
"valentine": "Valentine's Day",
|
|
227
|
+
"womenDay": "International Women's Day",
|
|
228
|
+
"liberationDay": "Liberation Day",
|
|
229
|
+
"laborDay": "Internation Labor's Day",
|
|
230
|
+
"childrenDay": "International Labor's Day",
|
|
231
|
+
"nationalDay": "National Day",
|
|
232
|
+
"womenDayVN": "Vietnamese Women's Day",
|
|
233
|
+
"teacherDay": "Teacher's Day",
|
|
234
|
+
"christmasEve": "Christmas Eve",
|
|
235
|
+
"christmas": "Christmas Day",
|
|
236
|
+
"lunarNewYear": "Lunar New Year",
|
|
237
|
+
"hungKingDay": "Hung Kings' Festival",
|
|
238
|
+
"balance": "Wallet Balance",
|
|
239
|
+
"chooseDate": "Choose date",
|
|
240
|
+
"year": "year",
|
|
241
|
+
"hour": "hour",
|
|
242
|
+
"minute": "minute",
|
|
243
|
+
"from": "From",
|
|
244
|
+
"to": "To",
|
|
245
|
+
"connected": "Connected",
|
|
246
|
+
"disconnected": "Disconnected",
|
|
247
|
+
"hide": "Hide",
|
|
248
|
+
"viewAllTitle": "View all",
|
|
249
|
+
"headerTitle": "Transaction history",
|
|
250
|
+
"SHORTEN": "SHORTEN",
|
|
251
|
+
"viewMoreBank": "VIEW MORE (${otherItemCount} banks)",
|
|
252
|
+
"transaction_success": "Success",
|
|
253
|
+
"transaction_fail": "Fail",
|
|
254
|
+
"transaction_processing": "Processing",
|
|
255
|
+
"viewMore": "View more",
|
|
256
|
+
"shorten": "Shorten",
|
|
257
|
+
"Payment": "${serviceName} payment",
|
|
258
|
+
"currencyUnit": "VND",
|
|
259
|
+
"Month": "Month",
|
|
260
|
+
"save": "Save",
|
|
261
|
+
"favoriteIn": "Add to favorite services",
|
|
262
|
+
"favoriteOut": "Remove to favorite services",
|
|
263
|
+
"deviceIn": "Add to device",
|
|
264
|
+
"setting": "Settings",
|
|
265
|
+
"transaction": "Transaction History",
|
|
266
|
+
"share": "Share",
|
|
267
|
+
"information": "Information",
|
|
268
|
+
"tutorial": "Instruction",
|
|
269
|
+
"question": "FAQ",
|
|
270
|
+
"support": "Support center",
|
|
271
|
+
"errorCode": "Error code: ",
|
|
272
|
+
"skip": "Skip",
|
|
273
|
+
"enterPhoneNumber": "Please enter your phone number",
|
|
274
|
+
"invalidPhoneNumber": "Invalid phone number",
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
}
|
|
@@ -14,6 +14,7 @@ import SwiftUI
|
|
|
14
14
|
public struct NavigationContainer<Initial: View>: View {
|
|
15
15
|
@StateObject private var navigator: Navigator
|
|
16
16
|
@StateObject private var applicationEnvironment: ApplicationEnvironment
|
|
17
|
+
@StateObject private var localize: Localize
|
|
17
18
|
|
|
18
19
|
private let setNavigator: ((Navigator) -> Void)?
|
|
19
20
|
private let composeApi: KitComposeApi?
|
|
@@ -25,6 +26,7 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
25
26
|
applicationContext: MiniAppContext? = nil,
|
|
26
27
|
composeApi: KitComposeApi? = nil,
|
|
27
28
|
config: KitConfig? = nil,
|
|
29
|
+
localize: Localize? = nil,
|
|
28
30
|
setNavigator: ((Navigator) -> Void)? = nil
|
|
29
31
|
) {
|
|
30
32
|
let nav = Navigator(
|
|
@@ -41,6 +43,7 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
41
43
|
config: config
|
|
42
44
|
)
|
|
43
45
|
)
|
|
46
|
+
_localize = StateObject(wrappedValue: localize ?? Localize())
|
|
44
47
|
self.composeApi = composeApi
|
|
45
48
|
self.setNavigator = setNavigator
|
|
46
49
|
}
|
|
@@ -55,11 +58,13 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
55
58
|
.environmentObject(navigator)
|
|
56
59
|
.environmentObject(applicationEnvironment)
|
|
57
60
|
.environment(\.applicationEnvironment, applicationEnvironment)
|
|
61
|
+
.environmentObject(localize)
|
|
58
62
|
.fullScreenCover(item: $navigator.presented) { route in
|
|
59
63
|
screenView(forDialog: route)
|
|
60
64
|
.environmentObject(navigator)
|
|
61
65
|
.environmentObject(applicationEnvironment)
|
|
62
66
|
.environment(\.applicationEnvironment, applicationEnvironment)
|
|
67
|
+
.environmentObject(localize)
|
|
63
68
|
}
|
|
64
69
|
.overlay {
|
|
65
70
|
if let item = navigator.overplay {
|
|
@@ -67,6 +72,7 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
67
72
|
.environmentObject(navigator)
|
|
68
73
|
.environmentObject(applicationEnvironment)
|
|
69
74
|
.environment(\.applicationEnvironment, applicationEnvironment)
|
|
75
|
+
.environmentObject(localize)
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
.onAppear {
|
|
@@ -43,42 +43,84 @@ struct BottomSheetView<Content: View>: View {
|
|
|
43
43
|
let content: () -> Content
|
|
44
44
|
|
|
45
45
|
@EnvironmentObject private var navigator: Navigator
|
|
46
|
+
@Environment(\.appTheme) private var theme
|
|
47
|
+
@Environment(\.applicationEnvironment) private var appEnv
|
|
46
48
|
|
|
47
49
|
/// Vertical translation of the sheet; starts off-screen and animates to 0.
|
|
48
50
|
@State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
|
|
49
|
-
|
|
51
|
+
/// Mirrors Compose's `backgroundAlpha`: only touched by open()/close(), never by drag.
|
|
52
|
+
@State private var backgroundAlpha: CGFloat = 0
|
|
50
53
|
@State private var sheetHeight: CGFloat = 0
|
|
51
54
|
@State private var didAppear = false
|
|
55
|
+
@State private var keyboardHeight: CGFloat = 0
|
|
56
|
+
/// Live drag offset, isolated from `sheetOffset` so a touch-move only invalidates
|
|
57
|
+
/// this gesture-scoped value instead of forcing the whole body (including
|
|
58
|
+
/// `content()`) to reconstruct every frame — mirrors the pattern already used in
|
|
59
|
+
/// `ios/PopupView/PopupView.swift` for smooth drag-to-dismiss.
|
|
60
|
+
@GestureState private var dragTranslation: CGFloat = 0
|
|
52
61
|
|
|
53
62
|
/// Mirrors Compose's `sheetCloseOffset` (100dp drag threshold to dismiss).
|
|
54
63
|
private let closeThreshold: CGFloat = 100
|
|
55
64
|
|
|
65
|
+
/// Mirrors Compose's per-term peak (0.3f). Combined with `backgroundAlpha` the
|
|
66
|
+
/// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
|
|
67
|
+
private let maxDynamicAlpha: CGFloat = 0.3
|
|
68
|
+
|
|
56
69
|
private var screenHeight: CGFloat { UIScreen.main.bounds.height }
|
|
57
70
|
private var bottomInset: CGFloat { safeAreaBottomInset() }
|
|
71
|
+
/// While the keyboard is up, it already covers the home-indicator area, so it
|
|
72
|
+
/// replaces (rather than adds to) the usual bottom-inset filler.
|
|
73
|
+
private var effectiveBottomInset: CGFloat { keyboardHeight > 0 ? keyboardHeight : bottomInset }
|
|
58
74
|
|
|
59
75
|
private var backgroundColor: Color {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
isSurface ? theme.colors.background.surface : theme.colors.background.default
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private var effectiveOffset: CGFloat { sheetOffset + dragTranslation }
|
|
80
|
+
|
|
81
|
+
/// Mirrors Compose's `dynamicAlpha`: a plain derived value (not itself animated)
|
|
82
|
+
/// that tracks the current offset live, shrinking toward 0 as the sheet is
|
|
83
|
+
/// dragged down.
|
|
84
|
+
private var dynamicAlpha: CGFloat {
|
|
85
|
+
let denom = sheetHeight > 0 ? sheetHeight : screenHeight
|
|
86
|
+
guard denom > 0 else { return maxDynamicAlpha }
|
|
87
|
+
let progress = min(max(1 - effectiveOffset / denom, 0), 1)
|
|
88
|
+
return progress * maxDynamicAlpha
|
|
63
89
|
}
|
|
64
90
|
|
|
91
|
+
/// Mirrors Compose's `alpha = backgroundAlpha + dynamicAlpha`: additive, so the
|
|
92
|
+
/// scrim peaks at 0.6 at rest and only fades to a 0.3 floor while dragging
|
|
93
|
+
/// (backgroundAlpha holds steady until an explicit close()).
|
|
94
|
+
private var totalScrimAlpha: CGFloat { backgroundAlpha + dynamicAlpha }
|
|
95
|
+
|
|
65
96
|
var body: some View {
|
|
66
97
|
ZStack(alignment: .bottom) {
|
|
67
|
-
// Dimmed scrim
|
|
68
|
-
Color.black.opacity(
|
|
69
|
-
.ignoresSafeArea()
|
|
98
|
+
// Dimmed scrim — additive backgroundAlpha + dynamicAlpha (see above).
|
|
99
|
+
Color.black.opacity(totalScrimAlpha)
|
|
70
100
|
.contentShape(Rectangle())
|
|
71
101
|
.onTapGesture {
|
|
72
102
|
if barrierDismissible { close() }
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
sheet
|
|
76
|
-
.offset(y:
|
|
106
|
+
.offset(y: effectiveOffset)
|
|
77
107
|
}
|
|
78
108
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
109
|
+
.ignoresSafeArea()
|
|
110
|
+
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
|
|
111
|
+
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
112
|
+
withAnimation(.easeOut(duration: 0.25)) {
|
|
113
|
+
keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
|
|
114
|
+
}
|
|
115
|
+
}
|
|
79
116
|
.onAppear {
|
|
80
117
|
guard !didAppear else { return }
|
|
81
118
|
didAppear = true
|
|
119
|
+
ScreenTracker.trackPopupDisplayed(
|
|
120
|
+
maxApi: appEnv.maxApi,
|
|
121
|
+
context: appEnv.applicationContext,
|
|
122
|
+
parentScreenName: ScreenTracker.getLastScreenName()
|
|
123
|
+
)
|
|
82
124
|
navigator.registerOverplayDismiss { close() }
|
|
83
125
|
open()
|
|
84
126
|
}
|
|
@@ -103,16 +145,16 @@ struct BottomSheetView<Content: View>: View {
|
|
|
103
145
|
.gesture(dragGesture)
|
|
104
146
|
|
|
105
147
|
Rectangle()
|
|
106
|
-
.fill(
|
|
148
|
+
.fill(theme.colors.border.default)
|
|
107
149
|
.frame(height: 1)
|
|
108
150
|
|
|
109
151
|
content()
|
|
110
152
|
|
|
111
153
|
backgroundColor
|
|
112
|
-
.frame(height:
|
|
154
|
+
.frame(height: effectiveBottomInset)
|
|
113
155
|
}
|
|
114
156
|
.frame(maxWidth: .infinity)
|
|
115
|
-
.frame(
|
|
157
|
+
.frame(alignment: .bottom)
|
|
116
158
|
.background(
|
|
117
159
|
GeometryReader { geo in
|
|
118
160
|
Color.clear.onAppear { sheetHeight = geo.size.height }
|
|
@@ -143,13 +185,33 @@ struct BottomSheetView<Content: View>: View {
|
|
|
143
185
|
// MARK: - Drag handling (mirrors Compose's detectDragGestures)
|
|
144
186
|
|
|
145
187
|
private var dragGesture: some Gesture {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
188
|
+
// `.global` (not the default `.local`) so the gesture tracks against the
|
|
189
|
+
// window rather than the handle's own view, which is itself repositioned
|
|
190
|
+
// every frame by this same gesture's output (`sheet.offset(y: effectiveOffset)`
|
|
191
|
+
// in `body`) — the local coordinate space chasing its own moving reference
|
|
192
|
+
// frame is a plausible source of a continuous drag-time jitter.
|
|
193
|
+
DragGesture(coordinateSpace: .global)
|
|
194
|
+
.onChanged { _ in
|
|
195
|
+
// Touching the handle should immediately settle `sheetOffset` to its
|
|
196
|
+
// resting value (0) with no animation, so a drag that starts while the
|
|
197
|
+
// open animation — or the keyboard-driven resize, which shifts this
|
|
198
|
+
// same bottom-anchored top edge — is still in flight doesn't fight a
|
|
199
|
+
// moving target. Without this, the live drag value chases an animated
|
|
200
|
+
// one, producing a couple of corrective jumps before it settles.
|
|
201
|
+
if sheetOffset != 0 {
|
|
202
|
+
sheetOffset = 0
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
.updating($dragTranslation) { value, state, _ in
|
|
206
|
+
state = max(value.translation.height, 0)
|
|
150
207
|
}
|
|
151
|
-
.onEnded {
|
|
152
|
-
|
|
208
|
+
.onEnded { value in
|
|
209
|
+
let finalOffset = max(value.translation.height, 0)
|
|
210
|
+
// Capture into persisted state BEFORE @GestureState resets to 0,
|
|
211
|
+
// avoiding a one-frame snap back to rest before close()'s own
|
|
212
|
+
// slide-out animation starts (mirrors PopupView's `lastDragPosition`).
|
|
213
|
+
sheetOffset = finalOffset
|
|
214
|
+
if finalOffset > closeThreshold {
|
|
153
215
|
close()
|
|
154
216
|
} else {
|
|
155
217
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
|
|
@@ -163,17 +225,23 @@ struct BottomSheetView<Content: View>: View {
|
|
|
163
225
|
|
|
164
226
|
private func open() {
|
|
165
227
|
DispatchQueue.main.async {
|
|
166
|
-
withAnimation(.
|
|
228
|
+
withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.1)) { backgroundAlpha = maxDynamicAlpha }
|
|
167
229
|
withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.35)) { sheetOffset = 0 }
|
|
168
230
|
}
|
|
169
231
|
}
|
|
170
232
|
|
|
233
|
+
/// Mirrors Compose's `closeEvent`: slide out over 200ms, then fade the scrim
|
|
234
|
+
/// out over 100ms only after the slide finishes, then dismiss.
|
|
171
235
|
private func close() {
|
|
172
236
|
withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.2)) {
|
|
173
237
|
sheetOffset = max(sheetHeight, screenHeight)
|
|
174
|
-
scrimAlpha = 0
|
|
175
238
|
}
|
|
176
239
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
240
|
+
withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.1)) {
|
|
241
|
+
backgroundAlpha = 0
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
177
245
|
navigator.finishOverplayDismiss()
|
|
178
246
|
}
|
|
179
247
|
}
|
|
@@ -98,6 +98,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
98
98
|
@State private var scrollOffset: CGFloat = 0
|
|
99
99
|
@State private var keyboardHeight: CGFloat = 0
|
|
100
100
|
@State private var headerRightWidth: CGFloat = 0
|
|
101
|
+
@State private var isBottomTabRoot: Bool = false
|
|
101
102
|
|
|
102
103
|
/// Header background fade tied to scroll: 0 → transparent (over a hero image
|
|
103
104
|
/// or transparent header), 1 → opaque surface with the drop shadow.
|
|
@@ -135,7 +136,10 @@ public struct StackScreen<Content: View>: View {
|
|
|
135
136
|
GeometryReader { geometry in
|
|
136
137
|
let keyboardSize: CGFloat = useAvoidKeyboard ? max(keyboardHeight, 0) : 0
|
|
137
138
|
let bottomInset = min(geometry.safeAreaInsets.bottom, 21)
|
|
138
|
-
|
|
139
|
+
// Skip this screen's own bottom-inset reservation when hosting a footer
|
|
140
|
+
// OR a self-contained BottomTab surface (mirrors Compose's
|
|
141
|
+
// `if (options.footerComponent != null || isBottomTab) 0.dp else navigationBar`).
|
|
142
|
+
let contentBottomInset = (footer == nil && !isBottomTabRoot) ? bottomInset : 0
|
|
139
143
|
|
|
140
144
|
ZStack(alignment: .top) {
|
|
141
145
|
// Background color
|
|
@@ -175,6 +179,9 @@ public struct StackScreen<Content: View>: View {
|
|
|
175
179
|
.onPreferenceChange(HeaderRightWidthKey.self) { width in
|
|
176
180
|
if headerRightWidth != width { headerRightWidth = width }
|
|
177
181
|
}
|
|
182
|
+
.onPreferenceChange(IsBottomTabRootKey.self) { isRoot in
|
|
183
|
+
if isBottomTabRoot != isRoot { isBottomTabRoot = isRoot }
|
|
184
|
+
}
|
|
178
185
|
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
|
|
179
186
|
guard useAvoidKeyboard,
|
|
180
187
|
let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
@@ -13,7 +13,7 @@ public enum Colors {
|
|
|
13
13
|
public static let textSecondary = Color.momoAsset("TextSecondary")
|
|
14
14
|
public static let card = Color.momoAsset("Card")
|
|
15
15
|
public static let error = Color.momoAsset("Error")
|
|
16
|
-
public static let info =
|
|
16
|
+
public static let info = blue03
|
|
17
17
|
public static let success = Color.momoAsset("Success")
|
|
18
18
|
|
|
19
19
|
public static let black20 = Color(hex: "000000")
|
|
@@ -49,15 +49,18 @@ public enum Colors {
|
|
|
49
49
|
public static let pink03 = Color(hex: "eb2f96")
|
|
50
50
|
public static let pink02 = Color(hex: "d42a87")
|
|
51
51
|
public static let pink01 = Color(hex: "bc2678")
|
|
52
|
+
public static let pinkMoMoBranding = Color(hex: "a50064")
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
public static let
|
|
55
|
-
public static let
|
|
55
|
+
public static let violet11 = Color(hex: "fcf8fe")
|
|
56
|
+
public static let violet11Stroke = Color(hex: "dfe1e5")
|
|
57
|
+
public static let violet10 = Color(hex: "faf4fe")
|
|
58
|
+
public static let violet09 = Color(hex: "f4e9fd")
|
|
56
59
|
public static let violet08 = Color(hex: "ead4fc")
|
|
57
60
|
public static let violet07 = Color(hex: "d5aaf9")
|
|
58
|
-
public static let violet06 = Color(hex: "
|
|
59
|
-
public static let violet05 = Color(hex: "
|
|
60
|
-
public static let violet04 = Color(hex: "
|
|
61
|
+
public static let violet06 = Color(hex: "c07ff6")
|
|
62
|
+
public static let violet05 = Color(hex: "ab55f3")
|
|
63
|
+
public static let violet04 = Color(hex: "a03ff1")
|
|
61
64
|
public static let violet03 = Color(hex: "962af0")
|
|
62
65
|
public static let violet02 = Color(hex: "8726d8")
|
|
63
66
|
public static let violet01 = Color(hex: "7822c0")
|
|
@@ -8,5 +8,7 @@ public enum Spacing {
|
|
|
8
8
|
public static let L: CGFloat = 16
|
|
9
9
|
public static let XL: CGFloat = 24
|
|
10
10
|
public static let XXL: CGFloat = 32
|
|
11
|
-
public static let
|
|
11
|
+
public static let Size3XL: CGFloat = 48
|
|
12
|
+
public static let Size4XL: CGFloat = 56
|
|
13
|
+
public static let Size5XL: CGFloat = 64
|
|
12
14
|
}
|
package/ios/native-kits.podspec
CHANGED
|
@@ -89,6 +89,7 @@ public let ScreenUsages: [ScreenUsage] = [
|
|
|
89
89
|
ScreenUsage(title: "Bottom Tab", destination: AnyView(BottomTabDemo()), icon: getIcon(icon: "Layout")),
|
|
90
90
|
ScreenUsage(title: "Animated Header", destination: AnyView(AnimatedHeaderDemo()), icon: getIcon(icon: "Layout")),
|
|
91
91
|
ScreenUsage(title: "Theme", destination: AnyView(ThemeDemo()), icon: getIcon(icon: "Typography")),
|
|
92
|
+
ScreenUsage(title: "Localize", destination: AnyView(LocalizeDemo()), icon: getIcon(icon: "Typography")),
|
|
92
93
|
]
|
|
93
94
|
|
|
94
95
|
@available(iOS 16.0, *)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
//
|
|
2
|
+
// LocalizeDemo.swift
|
|
3
|
+
// MoMoUIKitsDemo
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI port of the Compose demo `sample/shared/.../screens/LocalizeUsage.kt`,
|
|
6
|
+
// showcasing the `Localize` i18n port (ios/Application/Localize.swift).
|
|
7
|
+
// Synced from sample/iosApp/iosApp/Demo/LocalizeDemo.swift.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import MoMoUIKits
|
|
11
|
+
import SwiftUI
|
|
12
|
+
|
|
13
|
+
// App-specific dictionary. Keys here do NOT exist in the kit defaults, so addTranslations
|
|
14
|
+
// registers them cleanly (addTranslations keeps existing keys on clash, host-default wins).
|
|
15
|
+
private let AppTranslations = LocalizationObject(
|
|
16
|
+
vi: [
|
|
17
|
+
"demo_greeting": "Xin chào MoMo!",
|
|
18
|
+
"demo_cart_title": "Giỏ hàng của bạn",
|
|
19
|
+
],
|
|
20
|
+
en: [
|
|
21
|
+
"demo_greeting": "Hello MoMo!",
|
|
22
|
+
"demo_cart_title": "Your cart",
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
struct LocalizeDemo: View {
|
|
27
|
+
@EnvironmentObject private var localize: Localize
|
|
28
|
+
|
|
29
|
+
private var isEnglish: Bool { localize.currentLanguage == Localize.EN }
|
|
30
|
+
|
|
31
|
+
var body: some View {
|
|
32
|
+
ScrollView {
|
|
33
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
34
|
+
|
|
35
|
+
LocalizeSectionBox(title: "Đổi ngôn ngữ (toàn app)") {
|
|
36
|
+
MomoText(
|
|
37
|
+
"localize.changeLanguage(...) đổi ngôn ngữ cho cả NavigationContainer — mọi màn hình đang đọc translate() sẽ cập nhật theo.",
|
|
38
|
+
typography: .descriptionDefaultRegular
|
|
39
|
+
)
|
|
40
|
+
Switch(
|
|
41
|
+
.constant(isEnglish),
|
|
42
|
+
onChange: { _ in localize.changeLanguage(isEnglish ? Localize.VI : Localize.EN) },
|
|
43
|
+
title: "English"
|
|
44
|
+
)
|
|
45
|
+
MomoText("currentLanguage = \(localize.currentLanguage)", typography: .labelDefaultMedium)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
LocalizeSectionBox(title: "translate(key) — từ điển mặc định của kit") {
|
|
49
|
+
TranslationRow(key: "confirm", value: localize.translate("confirm"))
|
|
50
|
+
TranslationRow(key: "cancel", value: localize.translate("cancel"))
|
|
51
|
+
TranslationRow(key: "done", value: localize.translate("done"))
|
|
52
|
+
TranslationRow(key: "seeMore", value: localize.translate("seeMore"))
|
|
53
|
+
TranslationRow(key: "balance", value: localize.translate("balance"))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
LocalizeSectionBox(title: "Key thiếu → trả về chính key") {
|
|
57
|
+
MomoText(
|
|
58
|
+
"translate() với key không có (hoặc giá trị rỗng) sẽ trả lại đúng key để dễ phát hiện thiếu bản dịch.",
|
|
59
|
+
typography: .descriptionDefaultRegular
|
|
60
|
+
)
|
|
61
|
+
TranslationRow(key: "not_a_real_key", value: localize.translate("not_a_real_key"))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
LocalizeSectionBox(title: "Chèn tham số vào bản dịch") {
|
|
65
|
+
MomoText(
|
|
66
|
+
"Bản port không tự nội suy {placeholder} — tự replace ở nơi dùng.",
|
|
67
|
+
typography: .descriptionDefaultRegular
|
|
68
|
+
)
|
|
69
|
+
TranslationRow(
|
|
70
|
+
key: "voucherRemindHour",
|
|
71
|
+
value: localize.translate("voucherRemindHour").replacingOccurrences(of: "{hours}", with: "3")
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
LocalizeSectionBox(title: "translateData(vi:en:) — dịch nội tuyến") {
|
|
76
|
+
MomoText(
|
|
77
|
+
"Cho chuỗi dùng một lần, không cần khai báo key trong từ điển.",
|
|
78
|
+
typography: .descriptionDefaultRegular
|
|
79
|
+
)
|
|
80
|
+
MomoText(localize.translateData(vi: "Thanh toán ngay", en: "Pay now"), typography: .headerDefaultBold)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
LocalizeSectionBox(title: "translateData(map) — chọn theo ngôn ngữ") {
|
|
84
|
+
MomoText(
|
|
85
|
+
localize.translateData([
|
|
86
|
+
Localize.VI: "Lịch sử giao dịch",
|
|
87
|
+
Localize.EN: "Transaction history",
|
|
88
|
+
]),
|
|
89
|
+
typography: .headerDefaultBold
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
LocalizeSectionBox(title: "addTranslations — từ điển riêng của app") {
|
|
94
|
+
MomoText(
|
|
95
|
+
"Host đăng ký key riêng (đã add khi màn hình xuất hiện), rồi dùng translate() như key mặc định.",
|
|
96
|
+
typography: .descriptionDefaultRegular
|
|
97
|
+
)
|
|
98
|
+
TranslationRow(key: "demo_greeting", value: localize.translate("demo_greeting"))
|
|
99
|
+
TranslationRow(key: "demo_cart_title", value: localize.translate("demo_cart_title"))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
LocalizeSectionBox(title: "Ví dụ thực tế") {
|
|
103
|
+
// errorCode already ends with a separator ("Mã lỗi: " / "Error code: ").
|
|
104
|
+
MomoText(localize.translate("errorCode") + "E-4097", typography: .bodyDefaultRegular)
|
|
105
|
+
HStack(spacing: Spacing.M) {
|
|
106
|
+
Button(
|
|
107
|
+
title: localize.translate("cancel"),
|
|
108
|
+
action: {},
|
|
109
|
+
type: .outline,
|
|
110
|
+
size: .medium,
|
|
111
|
+
isFull: false
|
|
112
|
+
)
|
|
113
|
+
Button(
|
|
114
|
+
title: localize.translate("confirm"),
|
|
115
|
+
action: {},
|
|
116
|
+
type: .primary,
|
|
117
|
+
size: .medium,
|
|
118
|
+
isFull: false
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Spacer(minLength: 32)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Registers the app-specific dictionary once per appearance; addTranslations'
|
|
127
|
+
// merge is idempotent, so re-entry (e.g. re-navigating back to this screen) is safe.
|
|
128
|
+
.onAppear {
|
|
129
|
+
localize.addTranslations(AppTranslations)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// MARK: - LocalizeSectionBox
|
|
135
|
+
|
|
136
|
+
/// Titled rounded-card section container, mirroring Compose's `BasicColumnBox`
|
|
137
|
+
/// (title label above a `theme.colors.background.surface` card with `Radius.M` corners).
|
|
138
|
+
private struct LocalizeSectionBox<Content: View>: View {
|
|
139
|
+
@Environment(\.appTheme) private var theme
|
|
140
|
+
let title: String
|
|
141
|
+
@ViewBuilder let content: () -> Content
|
|
142
|
+
|
|
143
|
+
var body: some View {
|
|
144
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
145
|
+
if !title.isEmpty {
|
|
146
|
+
MomoText(title, typography: .headerMBold)
|
|
147
|
+
.padding(.top, 8)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
151
|
+
content()
|
|
152
|
+
}
|
|
153
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
154
|
+
.padding(12)
|
|
155
|
+
.background(theme.colors.background.surface)
|
|
156
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.M))
|
|
157
|
+
}
|
|
158
|
+
.padding(12)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// MARK: - TranslationRow
|
|
163
|
+
|
|
164
|
+
private struct TranslationRow: View {
|
|
165
|
+
@Environment(\.appTheme) private var theme
|
|
166
|
+
let key: String
|
|
167
|
+
let value: String
|
|
168
|
+
|
|
169
|
+
var body: some View {
|
|
170
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
171
|
+
MomoText(key, typography: .labelDefaultMedium, color: theme.colors.text.secondary)
|
|
172
|
+
MomoText(value, typography: .bodyDefaultRegular)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// MARK: - Preview
|
|
178
|
+
|
|
179
|
+
#Preview {
|
|
180
|
+
if #available(iOS 16.0, *) {
|
|
181
|
+
NavigationStack {
|
|
182
|
+
LocalizeDemo()
|
|
183
|
+
.environmentObject(Localize())
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Fallback on earlier versions
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -23,6 +23,7 @@ struct NavigationDemo: View {
|
|
|
23
23
|
@available(iOS 16.0, *)
|
|
24
24
|
private struct NavigationDemoContent: View {
|
|
25
25
|
@EnvironmentObject private var navigator: Navigator
|
|
26
|
+
@State private var textValue1 = ""
|
|
26
27
|
|
|
27
28
|
var body: some View {
|
|
28
29
|
ScrollView {
|
|
@@ -72,6 +73,7 @@ private struct NavigationDemoContent: View {
|
|
|
72
73
|
row("Show Bottom Sheet") {
|
|
73
74
|
navigator.showBottomSheet({
|
|
74
75
|
VStack(spacing: 12) {
|
|
76
|
+
Input(text: $textValue1, placeholder: "Test Bottom Sheet")
|
|
75
77
|
ForEach(0..<4, id: \.self) { i in
|
|
76
78
|
MomoText("Bottom sheet row \(i + 1)")
|
|
77
79
|
.frame(maxWidth: .infinity, alignment: .leading)
|