@momo-kits/native-kits 0.162.44-debug → 0.162.45-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 +45 -3
- package/README.md +2 -2
- package/compose/build.gradle.kts +9 -1
- package/compose/build.gradle.kts.backup +8 -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/application/LiteScreen.kt +351 -111
- 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/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 +55 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
- 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/const/Typography.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +314 -0
- 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/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +127 -83
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -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 +116 -26
- 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 +192 -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 +13 -0
- 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 +94 -0
- package/gradle/libs.versions.toml +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/MaxApi.swift +30 -3
- 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/Colors+Radius+Spacing/Radius.swift +6 -0
- 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 +107 -77
- 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 +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +385 -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 +169 -29
- package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +154 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +425 -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 +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +195 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
- package/ios/Navigation/StackScreen.swift +930 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -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 -135
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +140 -0
- package/ios/Popup/PopupNotify.swift +59 -69
- 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/FontScale.swift +3 -3
- package/ios/Typography/Text.swift +81 -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 +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/package.json +1 -1
- package/scripts/gen-ios-icons.mjs +36 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- 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/NavigationContainer.swift +0 -213
- 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/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
package/ios/Layout/Section.swift
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
|
|
3
|
-
///
|
|
4
|
-
/// wrapping row of children that read their span
|
|
5
|
-
/// Mirrors `vn.momo.kits.layout.Section`.
|
|
3
|
+
/// Grid section container (12 columns, 24 when the content is wider than 600pt): horizontal
|
|
4
|
+
/// margin, optional background image, and a wrapping row of children that read their span
|
|
5
|
+
/// size from `\.gridContext`. Mirrors `vn.momo.kits.layout.Section`.
|
|
6
6
|
public struct SectionView<Content: View>: View {
|
|
7
7
|
// MARK: Lifecycle
|
|
8
8
|
|
|
@@ -19,116 +19,20 @@ public struct SectionView<Content: View>: View {
|
|
|
19
19
|
// MARK: Public
|
|
20
20
|
|
|
21
21
|
public var body: some View {
|
|
22
|
-
|
|
23
|
-
.environment(\.gridContext, grid)
|
|
24
|
-
.frame(maxWidth: .infinity, alignment: .topLeading)
|
|
25
|
-
.background(widthReader)
|
|
22
|
+
GridFlow(gutter: Spacing.M, content: content)
|
|
26
23
|
.background(background)
|
|
27
24
|
.padding(.horizontal, useMargin ? Spacing.M : 0)
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
// MARK: Private
|
|
31
28
|
|
|
32
|
-
private static var columns: Int { 12 }
|
|
33
|
-
private static var gutter: CGFloat { Spacing.M }
|
|
34
|
-
|
|
35
29
|
private let useMargin: Bool
|
|
36
30
|
private let backgroundImage: String?
|
|
37
31
|
private let content: () -> Content
|
|
38
32
|
|
|
39
|
-
/// Width available *inside* the horizontal margin — the Compose `BoxWithConstraints` equivalent.
|
|
40
|
-
@State private var availableWidth: CGFloat = 0
|
|
41
|
-
|
|
42
|
-
private var grid: GridContext {
|
|
43
|
-
let columns = CGFloat(Self.columns)
|
|
44
|
-
// The trailing `- 1 / columns` mirrors Compose: it absorbs rounding so 12 spans
|
|
45
|
-
// plus their gutters never overflow the row by a sub-point.
|
|
46
|
-
let sizePerSpan = (availableWidth - Self.gutter * (columns - 1)) / columns - 1 / columns
|
|
47
|
-
return GridContext(numberOfColumns: Self.columns, gutter: Self.gutter, sizePerSpan: max(0, sizePerSpan))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
@ViewBuilder private var flow: some View {
|
|
51
|
-
if #available(iOS 16.0, *) {
|
|
52
|
-
FlowRowLayout(gutter: Self.gutter) { content() }
|
|
53
|
-
} else {
|
|
54
|
-
// No `Layout` protocol before iOS 16: children stack instead of wrapping.
|
|
55
|
-
// Full-width (span 12) items — the common case — look identical.
|
|
56
|
-
VStack(alignment: .leading, spacing: Self.gutter) { content() }
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
33
|
@ViewBuilder private var background: some View {
|
|
61
34
|
if let backgroundImage {
|
|
62
35
|
ImageView(backgroundImage, contentMode: .fill).clipped()
|
|
63
36
|
}
|
|
64
37
|
}
|
|
65
|
-
|
|
66
|
-
private var widthReader: some View {
|
|
67
|
-
GeometryReader { proxy in
|
|
68
|
-
Color.clear.preference(key: SectionWidthKey.self, value: proxy.size.width)
|
|
69
|
-
}
|
|
70
|
-
.onPreferenceChange(SectionWidthKey.self) { availableWidth = $0 }
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private struct SectionWidthKey: PreferenceKey {
|
|
75
|
-
static var defaultValue: CGFloat = 0
|
|
76
|
-
|
|
77
|
-
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
78
|
-
value = max(value, nextValue())
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/// Wrapping row with equal horizontal/vertical spacing — the Compose `FlowRow` equivalent.
|
|
83
|
-
@available(iOS 16.0, *)
|
|
84
|
-
private struct FlowRowLayout: Layout {
|
|
85
|
-
struct Row {
|
|
86
|
-
var items: [(index: Int, size: CGSize)] = []
|
|
87
|
-
var width: CGFloat = 0
|
|
88
|
-
var height: CGFloat = 0
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let gutter: CGFloat
|
|
92
|
-
|
|
93
|
-
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
|
|
94
|
-
let maxWidth = proposal.width ?? .infinity
|
|
95
|
-
let rows = rows(for: subviews, maxWidth: maxWidth)
|
|
96
|
-
let height = rows.reduce(0) { $0 + $1.height } + gutter * CGFloat(max(0, rows.count - 1))
|
|
97
|
-
return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
|
|
101
|
-
var y = bounds.minY
|
|
102
|
-
for row in rows(for: subviews, maxWidth: bounds.width) {
|
|
103
|
-
var x = bounds.minX
|
|
104
|
-
for item in row.items {
|
|
105
|
-
subviews[item.index].place(
|
|
106
|
-
at: CGPoint(x: x, y: y),
|
|
107
|
-
anchor: .topLeading,
|
|
108
|
-
proposal: ProposedViewSize(item.size)
|
|
109
|
-
)
|
|
110
|
-
x += item.size.width + gutter
|
|
111
|
-
}
|
|
112
|
-
y += row.height + gutter
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private func rows(for subviews: Subviews, maxWidth: CGFloat) -> [Row] {
|
|
117
|
-
var rows: [Row] = []
|
|
118
|
-
var current = Row()
|
|
119
|
-
for index in subviews.indices {
|
|
120
|
-
let size = subviews[index].sizeThatFits(ProposedViewSize(width: maxWidth, height: nil))
|
|
121
|
-
let widthIfAppended = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
122
|
-
// Sub-point tolerance: spans are computed to land exactly on `maxWidth`.
|
|
123
|
-
if !current.items.isEmpty, widthIfAppended > maxWidth + 0.5 {
|
|
124
|
-
rows.append(current)
|
|
125
|
-
current = Row()
|
|
126
|
-
}
|
|
127
|
-
current.width = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
128
|
-
current.height = max(current.height, size.height)
|
|
129
|
-
current.items.append((index, size))
|
|
130
|
-
}
|
|
131
|
-
if !current.items.isEmpty { rows.append(current) }
|
|
132
|
-
return rows
|
|
133
|
-
}
|
|
134
38
|
}
|
package/ios/Loader/Loader.swift
CHANGED
|
@@ -12,8 +12,7 @@ public enum LoaderType {
|
|
|
12
12
|
/// Mirrors Compose `Loader(type:, tintColor:, modifier:)` which delegates to
|
|
13
13
|
/// `DotLoader` / `SpinnerLoader`. Both variants render Lottie animations
|
|
14
14
|
/// (`files/dot_loading` and `files/lottie_circle_loader`) and optionally tint
|
|
15
|
-
/// the fill/stroke
|
|
16
|
-
/// with the (monochrome) Lottie animation.
|
|
15
|
+
/// the fill/stroke through a Lottie color value provider.
|
|
17
16
|
public struct Loader: View {
|
|
18
17
|
private let type: LoaderType
|
|
19
18
|
private let tintColor: Color?
|
|
@@ -97,17 +96,14 @@ public struct ProgressBar: View {
|
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
|
|
99
|
+
// Tinted inside Lottie, as Compose does: SwiftUI masks/color effects over a UIKit-backed view aren't reliable.
|
|
101
100
|
private struct TintedLottie: View {
|
|
102
101
|
let name: String
|
|
103
102
|
let tintColor: Color?
|
|
104
103
|
|
|
105
104
|
var body: some View {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} else {
|
|
110
|
-
LottieView(name: name, loopMode: .loop)
|
|
111
|
-
}
|
|
105
|
+
LottieView(name: name, loopMode: .loop, tintColor: tintColor.map { UIColor($0) })
|
|
106
|
+
// The tint is applied once in makeUIView; a new tint needs a new view.
|
|
107
|
+
.id(tintColor)
|
|
112
108
|
}
|
|
113
109
|
}
|
|
@@ -15,6 +15,8 @@ struct LottieView: UIViewRepresentable {
|
|
|
15
15
|
var loopMode: LottieLoopMode = .playOnce
|
|
16
16
|
var contentMode : UIView.ContentMode = .scaleAspectFit
|
|
17
17
|
var onDone: (() -> Void)?
|
|
18
|
+
// Recolors every fill and stroke, as Compose's iOS LottieAnimation does; nil keeps the file's colors.
|
|
19
|
+
var tintColor: UIColor?
|
|
18
20
|
|
|
19
21
|
// Add static cache for animations
|
|
20
22
|
private static var animationCache = NSCache<NSString, LottieAnimation>()
|
|
@@ -36,7 +38,8 @@ struct LottieView: UIViewRepresentable {
|
|
|
36
38
|
animationView.animation = cachedAnimation
|
|
37
39
|
setupAnimationView(animationView, view)
|
|
38
40
|
} else {
|
|
39
|
-
|
|
41
|
+
guard let animationURL = URL(string: url) else { return view }
|
|
42
|
+
LottieAnimation.loadedFrom(url: animationURL, closure: { animation in
|
|
40
43
|
if let animation = animation {
|
|
41
44
|
Self.animationCache.setObject(animation, forKey: url as NSString)
|
|
42
45
|
animationView.animation = animation
|
|
@@ -49,7 +52,8 @@ struct LottieView: UIViewRepresentable {
|
|
|
49
52
|
if let cachedAnimation = Self.animationCache.object(forKey: name as NSString) {
|
|
50
53
|
animationView.animation = cachedAnimation
|
|
51
54
|
setupAnimationView(animationView, view)
|
|
52
|
-
} else if let animation = LottieAnimation.named(name)
|
|
55
|
+
} else if let animation = LottieAnimation.named(name, bundle: MoMoUIKitsResources.bundle)
|
|
56
|
+
?? LottieAnimation.named(name) {
|
|
53
57
|
Self.animationCache.setObject(animation, forKey: name as NSString)
|
|
54
58
|
animationView.animation = animation
|
|
55
59
|
setupAnimationView(animationView, view)
|
|
@@ -62,6 +66,12 @@ struct LottieView: UIViewRepresentable {
|
|
|
62
66
|
private func setupAnimationView(_ animationView: LottieAnimationView, _ containerView: UIView) {
|
|
63
67
|
animationView.contentMode = contentMode
|
|
64
68
|
animationView.loopMode = loopMode
|
|
69
|
+
if let tintColor {
|
|
70
|
+
animationView.setValueProvider(
|
|
71
|
+
ColorValueProvider(tintColor.lottieColorValue),
|
|
72
|
+
keypath: AnimationKeypath(keypath: "**.Color")
|
|
73
|
+
)
|
|
74
|
+
}
|
|
65
75
|
animationView.backgroundBehavior = loopMode == .loop ? .pauseAndRestore : .pause
|
|
66
76
|
|
|
67
77
|
// Optimize rendering
|
|
@@ -71,6 +81,8 @@ struct LottieView: UIViewRepresentable {
|
|
|
71
81
|
containerView.addSubview(animationView)
|
|
72
82
|
|
|
73
83
|
NSLayoutConstraint.activate([
|
|
84
|
+
animationView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
|
|
85
|
+
animationView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
|
74
86
|
animationView.widthAnchor.constraint(equalTo: containerView.widthAnchor),
|
|
75
87
|
animationView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
|
|
76
88
|
])
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// BottomSheet.swift
|
|
3
3
|
// MoMoUIKits
|
|
4
4
|
//
|
|
5
|
-
// SwiftUI mirror of Compose's `
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/BottomSheet.kt`.
|
|
6
6
|
// A sheet that slides up from the bottom over a dimmed scrim, with a drag
|
|
7
7
|
// handle, a header (title or custom), a divider and the content body.
|
|
8
8
|
// Dragging down past a threshold (or tapping the scrim) dismisses it.
|
|
@@ -35,16 +35,21 @@ public struct BottomSheetHeader {
|
|
|
35
35
|
|
|
36
36
|
// MARK: - Bottom sheet view
|
|
37
37
|
|
|
38
|
-
@available(iOS 16.0, *)
|
|
39
38
|
struct BottomSheetView<Content: View>: View {
|
|
39
|
+
let overplayId: UUID
|
|
40
40
|
let header: BottomSheetHeader
|
|
41
41
|
let isSurface: Bool
|
|
42
42
|
let barrierDismissible: Bool
|
|
43
|
+
var draggable: Bool = true
|
|
44
|
+
/// Keeps the content above the home indicator (Compose `AppNavigationBar` padding).
|
|
45
|
+
var useBottomInset: Bool = true
|
|
46
|
+
var footer: (() -> AnyView)? = nil
|
|
43
47
|
let content: () -> Content
|
|
44
48
|
|
|
45
49
|
@EnvironmentObject private var navigator: Navigator
|
|
46
50
|
@Environment(\.appTheme) private var theme
|
|
47
51
|
@Environment(\.applicationEnvironment) private var appEnv
|
|
52
|
+
@Environment(\.screenLayout) private var layout
|
|
48
53
|
|
|
49
54
|
/// Vertical translation of the sheet; starts off-screen and animates to 0.
|
|
50
55
|
@State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
|
|
@@ -52,6 +57,7 @@ struct BottomSheetView<Content: View>: View {
|
|
|
52
57
|
@State private var backgroundAlpha: CGFloat = 0
|
|
53
58
|
@State private var sheetHeight: CGFloat = 0
|
|
54
59
|
@State private var didAppear = false
|
|
60
|
+
@State private var isClosing = false
|
|
55
61
|
@State private var keyboardHeight: CGFloat = 0
|
|
56
62
|
/// Live drag offset, isolated from `sheetOffset` so a touch-move only invalidates
|
|
57
63
|
/// this gesture-scoped value instead of forcing the whole body (including
|
|
@@ -66,8 +72,35 @@ struct BottomSheetView<Content: View>: View {
|
|
|
66
72
|
/// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
|
|
67
73
|
private let maxDynamicAlpha: CGFloat = 0.3
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
private var
|
|
75
|
+
// UIScreen stays on the outer display on iPhone Duo, even when unfolded
|
|
76
|
+
private var screenHeight: CGFloat { isDuo ? layout.height : UIScreen.main.bounds.height }
|
|
77
|
+
|
|
78
|
+
private var isDuo: Bool { layout.isDuoDevice }
|
|
79
|
+
// iPhone Duo inner display: center the sheet instead of spanning the width
|
|
80
|
+
private var isDuoCentered: Bool { isDuo && layout.width >= 600 }
|
|
81
|
+
|
|
82
|
+
/// Keeps the portrait width in landscape, like system sheets.
|
|
83
|
+
private var duoCenteredWidth: CGFloat {
|
|
84
|
+
min(
|
|
85
|
+
min(layout.width, layout.height) - Spacing.L * 2,
|
|
86
|
+
layout.width - max(layout.insets.leading, layout.insets.trailing, Spacing.L) * 2
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Landscape side buttons span the full height, so the content is inset; in portrait the
|
|
91
|
+
/// side status bar only covers the top corner.
|
|
92
|
+
private var sideInsetLeading: CGFloat {
|
|
93
|
+
isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.leading : 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private var sideInsetTrailing: CGFloat {
|
|
97
|
+
isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.trailing : 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private var maxHeight: CGFloat {
|
|
101
|
+
isDuo ? layout.height - max(layout.duoTopInset, Spacing.XL) : screenHeight - 90
|
|
102
|
+
}
|
|
103
|
+
private var bottomInset: CGFloat { useBottomInset ? safeAreaBottomInset() : 0 }
|
|
71
104
|
/// While the keyboard is up, it already covers the home-indicator area, so it
|
|
72
105
|
/// replaces (rather than adds to) the usual bottom-inset filler.
|
|
73
106
|
private var effectiveBottomInset: CGFloat { keyboardHeight > 0 ? keyboardHeight : bottomInset }
|
|
@@ -110,7 +143,7 @@ struct BottomSheetView<Content: View>: View {
|
|
|
110
143
|
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
|
|
111
144
|
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
112
145
|
withAnimation(.easeOut(duration: 0.25)) {
|
|
113
|
-
keyboardHeight =
|
|
146
|
+
keyboardHeight = screenHeight - endFrame.origin.y
|
|
114
147
|
}
|
|
115
148
|
}
|
|
116
149
|
.onAppear {
|
|
@@ -128,33 +161,48 @@ struct BottomSheetView<Content: View>: View {
|
|
|
128
161
|
|
|
129
162
|
private var sheet: some View {
|
|
130
163
|
VStack(spacing: 0) {
|
|
131
|
-
// Drag handle + header (the draggable region).
|
|
132
164
|
VStack(spacing: 0) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.frame(height:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
165
|
+
// Drag handle + header (the draggable region).
|
|
166
|
+
VStack(spacing: 0) {
|
|
167
|
+
Spacer().frame(height: 8)
|
|
168
|
+
Capsule()
|
|
169
|
+
.fill(Colors.black06)
|
|
170
|
+
.frame(width: 40, height: 4)
|
|
171
|
+
Spacer().frame(height: 4)
|
|
172
|
+
headerView
|
|
173
|
+
.frame(height: 56)
|
|
174
|
+
.padding(.horizontal, Spacing.M)
|
|
175
|
+
.padding(.leading, sideInsetLeading)
|
|
176
|
+
.padding(.trailing, sideInsetTrailing)
|
|
177
|
+
}
|
|
178
|
+
.frame(maxWidth: .infinity)
|
|
179
|
+
.frame(height: 72)
|
|
180
|
+
.contentShape(Rectangle())
|
|
181
|
+
.gesture(dragGesture, including: draggable ? .all : .subviews)
|
|
146
182
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
Rectangle()
|
|
184
|
+
.fill(theme.colors.border.default)
|
|
185
|
+
.frame(height: 1)
|
|
150
186
|
|
|
151
|
-
|
|
187
|
+
content()
|
|
188
|
+
.padding(.leading, sideInsetLeading)
|
|
189
|
+
.padding(.trailing, sideInsetTrailing)
|
|
190
|
+
// a tall sheet keeps its header (and footer) and gives the content what is left
|
|
191
|
+
.layoutPriority(-1)
|
|
192
|
+
|
|
193
|
+
if let footer {
|
|
194
|
+
BottomSheetFooter(content: footer)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// The filler stays outside the cap, which shrinks by it instead: the sheet still ends at
|
|
198
|
+
// `maxHeight`, and the iOS 15 fallback never measures the keyboard as content.
|
|
199
|
+
.modifier(CapHeight(maxHeight: maxHeight - effectiveBottomInset))
|
|
152
200
|
|
|
153
|
-
backgroundColor
|
|
201
|
+
(footer != nil ? theme.colors.background.surface : backgroundColor)
|
|
154
202
|
.frame(height: effectiveBottomInset)
|
|
155
203
|
}
|
|
156
|
-
.frame(maxWidth: .infinity)
|
|
157
|
-
.
|
|
204
|
+
.frame(maxWidth: isDuoCentered ? duoCenteredWidth : .infinity)
|
|
205
|
+
.clipped()
|
|
158
206
|
.background(
|
|
159
207
|
GeometryReader { geo in
|
|
160
208
|
Color.clear.onAppear { sheetHeight = geo.size.height }
|
|
@@ -198,14 +246,16 @@ struct BottomSheetView<Content: View>: View {
|
|
|
198
246
|
// same bottom-anchored top edge — is still in flight doesn't fight a
|
|
199
247
|
// moving target. Without this, the live drag value chases an animated
|
|
200
248
|
// one, producing a couple of corrective jumps before it settles.
|
|
201
|
-
|
|
249
|
+
// A closing sheet keeps going down.
|
|
250
|
+
if sheetOffset != 0 && !isClosing {
|
|
202
251
|
sheetOffset = 0
|
|
203
252
|
}
|
|
204
253
|
}
|
|
205
254
|
.updating($dragTranslation) { value, state, _ in
|
|
206
|
-
state = max(value.translation.height, 0)
|
|
255
|
+
state = isClosing ? 0 : max(value.translation.height, 0)
|
|
207
256
|
}
|
|
208
257
|
.onEnded { value in
|
|
258
|
+
guard !isClosing else { return }
|
|
209
259
|
let finalOffset = max(value.translation.height, 0)
|
|
210
260
|
// Capture into persisted state BEFORE @GestureState resets to 0,
|
|
211
261
|
// avoiding a one-frame snap back to rest before close()'s own
|
|
@@ -233,6 +283,8 @@ struct BottomSheetView<Content: View>: View {
|
|
|
233
283
|
/// Mirrors Compose's `closeEvent`: slide out over 200ms, then fade the scrim
|
|
234
284
|
/// out over 100ms only after the slide finishes, then dismiss.
|
|
235
285
|
private func close() {
|
|
286
|
+
guard !isClosing else { return }
|
|
287
|
+
isClosing = true
|
|
236
288
|
withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.2)) {
|
|
237
289
|
sheetOffset = max(sheetHeight, screenHeight)
|
|
238
290
|
}
|
|
@@ -242,12 +294,100 @@ struct BottomSheetView<Content: View>: View {
|
|
|
242
294
|
}
|
|
243
295
|
}
|
|
244
296
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
245
|
-
navigator.finishOverplayDismiss()
|
|
297
|
+
navigator.finishOverplayDismiss(id: overplayId)
|
|
246
298
|
}
|
|
247
299
|
}
|
|
248
300
|
}
|
|
249
301
|
|
|
302
|
+
/// Compose's `heightIn(max:)`. `.frame(maxHeight:)` grows to the cap whenever the proposal is
|
|
303
|
+
/// larger, leaving a gap above the header; this sizes the sheet to its content and only caps it
|
|
304
|
+
/// when taller.
|
|
305
|
+
private struct CapHeight: ViewModifier {
|
|
306
|
+
let maxHeight: CGFloat
|
|
307
|
+
|
|
308
|
+
func body(content: Content) -> some View {
|
|
309
|
+
if #available(iOS 16.0, *) {
|
|
310
|
+
CapHeightLayout(maxHeight: maxHeight) { content }
|
|
311
|
+
} else {
|
|
312
|
+
content.modifier(CapHeightFallback(maxHeight: maxHeight))
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// iOS 15 has no `Layout`: the sheet hugs its ideal height until that outgrows the cap, then is
|
|
318
|
+
/// held at the cap. A capped sheet can't read its ideal height (that needs a second copy of the
|
|
319
|
+
/// content), so it is re-measured only when the cap grows, e.g. once the keyboard goes down.
|
|
320
|
+
private struct CapHeightFallback: ViewModifier {
|
|
321
|
+
let maxHeight: CGFloat
|
|
322
|
+
@State private var capped = false
|
|
323
|
+
@State private var idealHeight: CGFloat = 0
|
|
324
|
+
|
|
325
|
+
func body(content: Content) -> some View {
|
|
326
|
+
content
|
|
327
|
+
// both modifiers stay in place whatever `capped` is, so the content keeps its identity
|
|
328
|
+
.fixedSize(horizontal: false, vertical: !capped)
|
|
329
|
+
.background(
|
|
330
|
+
GeometryReader { proxy in
|
|
331
|
+
Color.clear
|
|
332
|
+
.onAppear { measure(proxy.size.height) }
|
|
333
|
+
.onChange(of: proxy.size.height) { measure($0) }
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
.frame(height: capped ? maxHeight : nil, alignment: .top)
|
|
337
|
+
.onChange(of: maxHeight) { cap in
|
|
338
|
+
if capped {
|
|
339
|
+
// the ideal height was over the old cap, so only a taller cap can release it
|
|
340
|
+
if cap > idealHeight { capped = false }
|
|
341
|
+
} else if idealHeight > cap {
|
|
342
|
+
capped = true
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private func measure(_ height: CGFloat) {
|
|
348
|
+
guard !capped else { return }
|
|
349
|
+
idealHeight = height
|
|
350
|
+
if height > maxHeight { capped = true }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
250
354
|
@available(iOS 16.0, *)
|
|
355
|
+
private struct CapHeightLayout: Layout {
|
|
356
|
+
let maxHeight: CGFloat
|
|
357
|
+
|
|
358
|
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
|
359
|
+
guard let subview = subviews.first else { return .zero }
|
|
360
|
+
let cap = min(maxHeight, proposal.height ?? .infinity)
|
|
361
|
+
// ideal first: a scroll view would otherwise fill whatever height it is offered
|
|
362
|
+
let ideal = subview.sizeThatFits(ProposedViewSize(width: proposal.width, height: nil))
|
|
363
|
+
return subview.sizeThatFits(ProposedViewSize(width: proposal.width, height: min(ideal.height, cap)))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
|
367
|
+
subviews.first?.place(at: bounds.origin, proposal: ProposedViewSize(bounds.size))
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// Mirrors Compose's `BottomSheetFooter`: a surface band with a soft shadow cast on the content.
|
|
372
|
+
private struct BottomSheetFooter: View {
|
|
373
|
+
let content: () -> AnyView
|
|
374
|
+
|
|
375
|
+
@Environment(\.appTheme) private var theme
|
|
376
|
+
|
|
377
|
+
var body: some View {
|
|
378
|
+
content()
|
|
379
|
+
.frame(maxWidth: .infinity)
|
|
380
|
+
.padding(.vertical, Spacing.S)
|
|
381
|
+
.background(theme.colors.background.surface)
|
|
382
|
+
.overlay(alignment: .top) {
|
|
383
|
+
LinearGradient(colors: [.clear, Color.black.opacity(0.05)], startPoint: .top, endPoint: .bottom)
|
|
384
|
+
.frame(height: 6)
|
|
385
|
+
.offset(y: -6)
|
|
386
|
+
.allowsHitTesting(false)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
251
391
|
private func safeAreaBottomInset() -> CGFloat {
|
|
252
392
|
let bottomInset = UIApplication.shared.connectedScenes
|
|
253
393
|
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
|