@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,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(curl -s -o /dev/null -w \"%{http_code}\" \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.144.6/skiko-android-0.144.6.pom\")",
|
|
5
|
+
"Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/maven-metadata.xml\")",
|
|
6
|
+
"Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko/maven-metadata.xml\")",
|
|
7
|
+
"Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko/0.144.6/skiko-0.144.6.module\")",
|
|
8
|
+
"Bash(curl -s -o /dev/null -w '%{http_code}' https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.144.6/skiko-android-0.144.6.__TRACKED_VAR__)",
|
|
9
|
+
"Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.144.6/\")",
|
|
10
|
+
"Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/\")",
|
|
11
|
+
"Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.148.1/skiko-android-0.148.1.pom\")",
|
|
12
|
+
"Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.144.5/skiko-android-0.144.5.pom\")",
|
|
13
|
+
"Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" \"https://dl.google.com/dl/android/maven2/org/jetbrains/skiko/skiko-android/0.144.6/skiko-android-0.144.6.pom\")",
|
|
14
|
+
"Bash(curl -s \"https://search.maven.org/solrsearch/select?q=a:skiko-android&rows=5&wt=json\")",
|
|
15
|
+
"Bash(curl -sL -o /dev/null -w \"%{http_code} %{url_effective}\\\\n\" \"https://repo1.maven.org/maven2/org/jetbrains/skiko/skiko-android/0.144.6/skiko-android-0.144.6.pom\")",
|
|
16
|
+
"Bash(curl *)",
|
|
17
|
+
"Bash(./gradlew :sample:androidApp:dependencies --configuration debugRuntimeClasspath)",
|
|
18
|
+
"Bash(./gradlew :sample:shared:compileDebugKotlinAndroid)",
|
|
19
|
+
"Bash(./gradlew :sample:androidApp:compileDebugKotlin)",
|
|
20
|
+
"Bash(ls -d *.xcodeproj)",
|
|
21
|
+
"Bash(grep -c ChipDemo.swift __TRACKED_VAR__/project.pbxproj)",
|
|
22
|
+
"Bash(grep -c PBXFileSystemSynchronizedRootGroup __TRACKED_VAR__/project.pbxproj)",
|
|
23
|
+
"Bash(grep -c InformationDemo.swift __TRACKED_VAR__/project.pbxproj)",
|
|
24
|
+
"Bash(plutil -lint project.pbxproj)",
|
|
25
|
+
"Bash(xcode-select -p)",
|
|
26
|
+
"Bash(/usr/bin/xcrun xcodebuild *)",
|
|
27
|
+
"Bash(echo \"exit: $?\")",
|
|
28
|
+
"Bash(/Applications/Xcode26.app/Contents/Developer/usr/bin/xcodebuild -version)",
|
|
29
|
+
"Bash(./gradlew :compose:tasks --all)",
|
|
30
|
+
"Bash(grep -n \"vanniktech\\\\|mavenPublishing\\\\|^publishing\\\\|version = gitlabVersion\\\\|namespace =\\\\|compileSdk =\\\\|minSdk =\\\\|GitLabPackages\\\\|name = \\\\\"\\\\${gitlabRepo}Packages\\\\\"\")",
|
|
31
|
+
"Bash(git checkout *)",
|
|
32
|
+
"Bash(./gradlew :compose:publishAllPublicationsToGitLabPackagesRepository --dry-run)",
|
|
33
|
+
"Bash(git fetch *)",
|
|
34
|
+
"Bash(git pull *)",
|
|
35
|
+
"Bash(git push *)",
|
|
36
|
+
"Bash(git branch *)",
|
|
37
|
+
"Bash(xcodebuild -workspace iosApp.xcworkspace -list)",
|
|
38
|
+
"Bash(xcrun simctl *)",
|
|
39
|
+
"Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14 Pro' -configuration Debug build CODE_SIGNING_ALLOWED=NO)",
|
|
40
|
+
"Bash(pod install *)",
|
|
41
|
+
"Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'id=1E6F9A79-5E64-4590-BEA2-828D75D99393' -configuration Debug build CODE_SIGNING_ALLOWED=NO)",
|
|
42
|
+
"Bash(xcrun swiftc *)",
|
|
43
|
+
"Bash(echo \"=== exit: $? \\(1=none found\\) ===\")",
|
|
44
|
+
"Bash(cat Radius.swift)",
|
|
45
|
+
"Bash(cat Spacing.swift)",
|
|
46
|
+
"Bash(echo \"exit=$?\")",
|
|
47
|
+
"Bash(ruby *)",
|
|
48
|
+
"Bash(xcodebuild -list -workspace iosApp.xcworkspace)",
|
|
49
|
+
"Bash(xcodebuild -workspace iosApp.xcworkspace -scheme MoMoUIKits -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14 Pro' build)",
|
|
50
|
+
"Bash(xcodebuild -workspace iosApp.xcworkspace -scheme MoMoUIKits -sdk iphonesimulator -destination 'platform=iOS Simulator,id=E6A899C0-098E-473A-95B8-A2BBD2D42A92' build)",
|
|
51
|
+
"Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,id=E6A899C0-098E-473A-95B8-A2BBD2D42A92' build)",
|
|
52
|
+
"Bash(xcodebuild build *)"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
package/compose/build.gradle.kts
CHANGED
package/compose/compose.podspec
CHANGED
package/gradle.properties
CHANGED
|
@@ -83,9 +83,8 @@ public struct HeaderBackground: View {
|
|
|
83
83
|
self.headerTransparent = headerTransparent
|
|
84
84
|
self.fullScreenContent = fullScreenContent
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
public var body: some View {
|
|
88
|
-
|
|
89
88
|
let statusBarHeight = safeAreaTopInset()
|
|
90
89
|
let opacity = max(0, 1 - scrollState / 200)
|
|
91
90
|
let height = fullScreenContent ? 0 : statusBarHeight + 52
|
|
@@ -101,21 +100,6 @@ public struct HeaderBackground: View {
|
|
|
101
100
|
.frame(height: height)
|
|
102
101
|
.shadow(color: Colors.black20.opacity(0.2), radius: 10, x: 0, y: -2)
|
|
103
102
|
.background(backgroundColor)
|
|
104
|
-
.overlay(
|
|
105
|
-
headerTransparent ? AnyView(EmptyView()) :
|
|
106
|
-
AnyView(
|
|
107
|
-
Rectangle()
|
|
108
|
-
.fill(LinearGradient(
|
|
109
|
-
gradient: Gradient(colors: [
|
|
110
|
-
Color(red: 1, green: 0.8, blue: 0.87),
|
|
111
|
-
Color(red: 1, green: 0.8, blue: 0.87).opacity(0.5)
|
|
112
|
-
]),
|
|
113
|
-
startPoint: .top,
|
|
114
|
-
endPoint: .bottom))
|
|
115
|
-
.opacity(opacity)
|
|
116
|
-
.frame(height: height))
|
|
117
|
-
)
|
|
118
|
-
|
|
119
103
|
if !headerTransparent {
|
|
120
104
|
Rectangle()
|
|
121
105
|
.fill(Color.black.opacity(0.1))
|
|
@@ -148,12 +132,26 @@ public struct HeaderBackground: View {
|
|
|
148
132
|
}
|
|
149
133
|
}
|
|
150
134
|
|
|
135
|
+
// MARK: - Header right width preference
|
|
136
|
+
|
|
137
|
+
/// Reports the measured width of the header-right area so the animated search
|
|
138
|
+
/// header can compute its trailing inset (mirrors Compose's headerRightWidthPx).
|
|
139
|
+
public struct HeaderRightWidthKey: PreferenceKey {
|
|
140
|
+
public static var defaultValue: CGFloat = 0
|
|
141
|
+
public static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
142
|
+
value = max(value, nextValue())
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
151
146
|
// MARK: - Header View
|
|
152
147
|
|
|
153
148
|
public struct Header: View {
|
|
154
149
|
var headerType: HeaderType = .default
|
|
155
150
|
var titlePosition: TitlePosition
|
|
156
151
|
var title: String
|
|
152
|
+
/// Optional rich title (mirrors Compose's `HeaderTitle`); `.user` renders an
|
|
153
|
+
/// avatar group instead of `title`. Defaults to nil → plain `title` text.
|
|
154
|
+
var headerTitle: HeaderTitle?
|
|
157
155
|
var headerRight: (()->any View)? = {HeaderRight()}
|
|
158
156
|
var goBack: (() -> Void)?
|
|
159
157
|
var opacity: CGFloat = 1
|
|
@@ -161,22 +159,27 @@ public struct Header: View {
|
|
|
161
159
|
var scrollState: CGFloat = 0
|
|
162
160
|
var inputSearchProps: InputSearchProps?
|
|
163
161
|
var tintColor: Color?
|
|
162
|
+
/// Title opacity; used by the animated search header to fade the title out on scroll.
|
|
163
|
+
var titleOpacity: CGFloat = 1
|
|
164
164
|
|
|
165
165
|
public init(
|
|
166
166
|
headerType: HeaderType = .default,
|
|
167
167
|
titlePosition: TitlePosition,
|
|
168
168
|
title: String,
|
|
169
|
+
headerTitle: HeaderTitle? = nil,
|
|
169
170
|
headerRight: (()->any View)? = {HeaderRight()},
|
|
170
171
|
goBack: (() -> Void)? = nil,
|
|
171
172
|
opacity: CGFloat = 1,
|
|
172
173
|
animatedHeader: AnimatedHeader? = nil,
|
|
173
174
|
scrollState: CGFloat = 0,
|
|
174
175
|
inputSearchProps: InputSearchProps? = nil,
|
|
175
|
-
tintColor: Color? = nil
|
|
176
|
+
tintColor: Color? = nil,
|
|
177
|
+
titleOpacity: CGFloat = 1
|
|
176
178
|
) {
|
|
177
179
|
self.headerType = headerType
|
|
178
180
|
self.titlePosition = titlePosition
|
|
179
181
|
self.title = title
|
|
182
|
+
self.headerTitle = headerTitle
|
|
180
183
|
self.headerRight = headerRight
|
|
181
184
|
self.goBack = goBack
|
|
182
185
|
self.opacity = opacity
|
|
@@ -184,10 +187,11 @@ public struct Header: View {
|
|
|
184
187
|
self.scrollState = scrollState
|
|
185
188
|
self.inputSearchProps = inputSearchProps
|
|
186
189
|
self.tintColor = tintColor
|
|
190
|
+
self.titleOpacity = titleOpacity
|
|
187
191
|
}
|
|
188
|
-
|
|
192
|
+
|
|
189
193
|
public var body: some View {
|
|
190
|
-
|
|
194
|
+
|
|
191
195
|
let backgroundButtonColor: Color = tintColor == Colors.black01 ? Colors.black20.opacity(0.6) : Colors.black01.opacity(0.6)
|
|
192
196
|
let borderColor: Color = tintColor == Colors.black01 ? Colors.black01.opacity(0.2) : Colors.black20.opacity(0.2)
|
|
193
197
|
|
|
@@ -212,6 +216,9 @@ public struct Header: View {
|
|
|
212
216
|
|
|
213
217
|
if let headerRight = headerRight {
|
|
214
218
|
AnyView(headerRight())
|
|
219
|
+
.background(GeometryReader { geo in
|
|
220
|
+
Color.clear.preference(key: HeaderRightWidthKey.self, value: geo.size.width)
|
|
221
|
+
})
|
|
215
222
|
}
|
|
216
223
|
}
|
|
217
224
|
.padding(.horizontal, 12)
|
|
@@ -223,32 +230,16 @@ public struct Header: View {
|
|
|
223
230
|
}
|
|
224
231
|
|
|
225
232
|
if let inputProps = inputSearchProps {
|
|
226
|
-
InputSearch(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
placeholder: inputProps.placeholder,
|
|
232
|
-
onChangeText: inputProps.onChangeText,
|
|
233
|
-
onPressButtonText: inputProps.onPressButtonText,
|
|
234
|
-
error: inputProps.error,
|
|
235
|
-
disabled: inputProps.disabled,
|
|
236
|
-
icon: inputProps.icon,
|
|
237
|
-
iconColor: inputProps.iconColor,
|
|
238
|
-
onRightIconPressed: inputProps.onRightIconPressed,
|
|
239
|
-
onFocus: inputProps.onFocus,
|
|
240
|
-
onBlur: inputProps.onBlur,
|
|
241
|
-
loading: inputProps.loading,
|
|
242
|
-
fontWeight: inputProps.fontWeight,
|
|
243
|
-
keyboardType: inputProps.keyboardType
|
|
244
|
-
)
|
|
233
|
+
InputSearch(props: inputProps)
|
|
234
|
+
} else if case let .user(userData)? = headerTitle {
|
|
235
|
+
if #available(iOS 16.0, *) {
|
|
236
|
+
HeaderUserView(userData)
|
|
237
|
+
}
|
|
245
238
|
} else {
|
|
246
|
-
|
|
247
|
-
.
|
|
248
|
-
.foregroundColor(tintColor ?? Colors.black17)
|
|
239
|
+
MomoText(headerTitle?.defaultTitle ?? title, typography: .headerDefaultBold, color: tintColor ?? Colors.black17, maxLines: 1)
|
|
240
|
+
.opacity(titleOpacity)
|
|
249
241
|
.frame(maxWidth: titlePosition == .center ? UIScreen.main.bounds.width - 252 : nil)
|
|
250
242
|
.frame(maxWidth: titlePosition == .center ? .infinity : UIScreen.main.bounds.width - 172, alignment: titlePosition == .center ? .center : .leading)
|
|
251
|
-
.lineLimit(1)
|
|
252
243
|
}
|
|
253
244
|
|
|
254
245
|
Spacer()
|
|
@@ -3,11 +3,16 @@ import Combine
|
|
|
3
3
|
|
|
4
4
|
public enum FABSize {
|
|
5
5
|
case small
|
|
6
|
+
case `default`
|
|
7
|
+
/// Backward-compatible alias for older iOS call sites. Compose names this
|
|
8
|
+
/// size `DEFAULT`.
|
|
6
9
|
case large
|
|
7
|
-
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
public enum FABPosition {
|
|
13
|
+
case end
|
|
14
|
+
/// Backward-compatible alias for older iOS call sites. Compose names this
|
|
15
|
+
/// position `END`.
|
|
11
16
|
case right
|
|
12
17
|
case center
|
|
13
18
|
}
|
|
@@ -31,10 +36,10 @@ public struct FabProps {
|
|
|
31
36
|
label: String? = nil,
|
|
32
37
|
onClick: (() -> Void)? = nil,
|
|
33
38
|
containerColor: Color? = Colors.primary,
|
|
34
|
-
size: FABSize? = .
|
|
39
|
+
size: FABSize? = .default,
|
|
35
40
|
iconSize: CGFloat? = nil,
|
|
36
41
|
scrollOffset: CGFloat? = nil,
|
|
37
|
-
position: FABPosition? = .
|
|
42
|
+
position: FABPosition? = .end,
|
|
38
43
|
bottomPadding: CGFloat? = nil,
|
|
39
44
|
renderComponent: (() -> AnyView)? = nil
|
|
40
45
|
) {
|
|
@@ -65,7 +70,7 @@ public struct FloatingButton: View {
|
|
|
65
70
|
private var keyboardOffset: CGFloat
|
|
66
71
|
private var scrollOffset: CGFloat
|
|
67
72
|
private var bottomPadding: CGFloat { props.bottomPadding ?? 12 + keyboardOffset}
|
|
68
|
-
private var position: FABPosition? { props.position ?? .
|
|
73
|
+
private var position: FABPosition? { props.position ?? .end }
|
|
69
74
|
private var containerColor: Color? { props.containerColor ?? Colors.primary }
|
|
70
75
|
private var iconColor: Color? { props.iconColor ?? .white }
|
|
71
76
|
private var label: String? { props.label }
|
|
@@ -152,10 +157,7 @@ public struct FloatingButton: View {
|
|
|
152
157
|
if let label = label {
|
|
153
158
|
if showText {
|
|
154
159
|
Spacer().frame(width: 8)
|
|
155
|
-
|
|
156
|
-
.foregroundColor(.white)
|
|
157
|
-
.font(.system(size: scaleSize(16), weight: .bold))
|
|
158
|
-
.lineLimit(1)
|
|
160
|
+
MomoText(label, typography: .actionDefaultBold, color: .white, maxLines: 1)
|
|
159
161
|
.background(
|
|
160
162
|
GeometryReader { geo in
|
|
161
163
|
Color.clear.onAppear {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//
|
|
2
|
+
// BottomTab.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `BottomTab` (navigation/bottomtab/BottomTab.kt).
|
|
6
|
+
// Hosts a set of tabbed screens, each wrapped in `StackScreen` chrome built
|
|
7
|
+
// from that tab's `NavigationOptions`, with a `BottomTabBar` pinned to the
|
|
8
|
+
// bottom. Switching tabs cross-fades + scales the content (parity with Compose).
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
import SwiftUI
|
|
12
|
+
|
|
13
|
+
@available(iOS 16.0, *)
|
|
14
|
+
public struct BottomTab: View {
|
|
15
|
+
private let items: [BottomTabItem]
|
|
16
|
+
private let floatingButton: BottomTabFloatingButton?
|
|
17
|
+
|
|
18
|
+
@EnvironmentObject private var navigator: Navigator
|
|
19
|
+
@State private var selectedIndex: Int = 0
|
|
20
|
+
|
|
21
|
+
public init(items: [BottomTabItem], floatingButton: BottomTabFloatingButton? = nil) {
|
|
22
|
+
self.items = items
|
|
23
|
+
self.floatingButton = floatingButton
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private var bottomInset: CGFloat { safeAreaBottomInset() }
|
|
27
|
+
private var reservedBottom: CGFloat { BOTTOM_TAB_BAR_HEIGHT + bottomInset + Spacing.S }
|
|
28
|
+
|
|
29
|
+
public var body: some View {
|
|
30
|
+
ZStack(alignment: .bottom) {
|
|
31
|
+
// Tab content area, inset above the bar.
|
|
32
|
+
ZStack {
|
|
33
|
+
ForEach(items.indices, id: \.self) { index in
|
|
34
|
+
if index == selectedIndex {
|
|
35
|
+
tabScreen(for: index)
|
|
36
|
+
.transition(.scale(scale: 0.97).combined(with: .opacity))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
.padding(.bottom, reservedBottom)
|
|
41
|
+
.animation(.easeInOut(duration: 0.2), value: selectedIndex)
|
|
42
|
+
|
|
43
|
+
// Tab bar + home-indicator filler.
|
|
44
|
+
VStack(spacing: 0) {
|
|
45
|
+
BottomTabBar(
|
|
46
|
+
items: items,
|
|
47
|
+
floatingButton: floatingButton,
|
|
48
|
+
selectedIndex: selectedIndex,
|
|
49
|
+
onTabSelected: { index in
|
|
50
|
+
if index != selectedIndex { selectedIndex = index }
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
Colors.black01
|
|
54
|
+
.frame(height: bottomInset + Spacing.S)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
58
|
+
.ignoresSafeArea(edges: .bottom)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@ViewBuilder
|
|
62
|
+
private func tabScreen(for index: Int) -> some View {
|
|
63
|
+
let item = items[index]
|
|
64
|
+
let options = item.options ?? NavigationOptions()
|
|
65
|
+
StackScreen(
|
|
66
|
+
backgroundColor: options.backgroundColor,
|
|
67
|
+
headerTransparent: options.headerTransparent,
|
|
68
|
+
fullScreenContent: options.fullScreenContent,
|
|
69
|
+
tintColor: options.tintColor ?? Colors.black17,
|
|
70
|
+
isBack: !options.hiddenBack,
|
|
71
|
+
headerType: options.headerType,
|
|
72
|
+
useAnimated: options.useAnimated,
|
|
73
|
+
title: options.title,
|
|
74
|
+
headerTitle: options.headerTitle,
|
|
75
|
+
titlePosition: options.titlePosition,
|
|
76
|
+
// Tab back: non-first tab returns to the first tab, first tab
|
|
77
|
+
// delegates to the navigator (mirrors Compose's onBackHandler).
|
|
78
|
+
goBack: options.hiddenBack ? nil : {
|
|
79
|
+
if index != 0 { selectedIndex = 0 } else { navigator.onBackSafe() }
|
|
80
|
+
},
|
|
81
|
+
scrollable: options.scrollable,
|
|
82
|
+
scrollToTopCallback: options.scrollToTopCallback,
|
|
83
|
+
useAvoidKeyboard: options.useAvoidKeyboard,
|
|
84
|
+
keyboardShouldPersistTaps: options.keyboardShouldPersistTaps,
|
|
85
|
+
footer: options.footer,
|
|
86
|
+
headerRight: options.resolvedHeaderRight,
|
|
87
|
+
animatedHeader: options.animatedHeader,
|
|
88
|
+
layoutOffset: options.layoutOffset,
|
|
89
|
+
inputSearchProps: options.inputSearchProps,
|
|
90
|
+
fabProps: options.fabProps,
|
|
91
|
+
content: { item.screen() }
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Bottom safe-area inset (home indicator).
|
|
97
|
+
@available(iOS 16.0, *)
|
|
98
|
+
private func safeAreaBottomInset() -> CGFloat {
|
|
99
|
+
UIApplication.shared.connectedScenes
|
|
100
|
+
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
|
|
101
|
+
.first ?? 0
|
|
102
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
//
|
|
2
|
+
// BottomTabBar.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `BottomTabBar` (navigation/bottomtab/BottomTabBar.kt).
|
|
6
|
+
// Renders the row of tab items, an animated selection indicator, a top shadow,
|
|
7
|
+
// badges/dots, and an optional center floating button (with the curved notch).
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
// MARK: - Models
|
|
13
|
+
|
|
14
|
+
/// Mirrors Compose's `BottomTabItem`.
|
|
15
|
+
public struct BottomTabItem: Identifiable {
|
|
16
|
+
public let id = UUID()
|
|
17
|
+
public let name: String
|
|
18
|
+
public let label: String
|
|
19
|
+
public let icon: String
|
|
20
|
+
public let showDot: Bool
|
|
21
|
+
public let badgeLabel: String?
|
|
22
|
+
public let options: NavigationOptions?
|
|
23
|
+
public let screen: () -> AnyView
|
|
24
|
+
|
|
25
|
+
public init(
|
|
26
|
+
name: String,
|
|
27
|
+
label: String,
|
|
28
|
+
icon: String,
|
|
29
|
+
showDot: Bool = false,
|
|
30
|
+
badgeLabel: String? = nil,
|
|
31
|
+
options: NavigationOptions? = nil,
|
|
32
|
+
@ViewBuilder screen: @escaping () -> some View
|
|
33
|
+
) {
|
|
34
|
+
self.name = name
|
|
35
|
+
self.label = label
|
|
36
|
+
self.icon = icon
|
|
37
|
+
self.showDot = showDot
|
|
38
|
+
self.badgeLabel = badgeLabel
|
|
39
|
+
self.options = options
|
|
40
|
+
self.screen = { AnyView(screen()) }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Mirrors Compose's `BottomTabFloatingButton`.
|
|
45
|
+
public struct BottomTabFloatingButton {
|
|
46
|
+
public let icon: String
|
|
47
|
+
public let label: String
|
|
48
|
+
public let onPress: () -> Void
|
|
49
|
+
|
|
50
|
+
public init(icon: String, label: String, onPress: @escaping () -> Void) {
|
|
51
|
+
self.icon = icon
|
|
52
|
+
self.label = label
|
|
53
|
+
self.onPress = onPress
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let bottomTabFloatingButtonWidth: CGFloat = 75
|
|
58
|
+
let BOTTOM_TAB_BAR_HEIGHT: CGFloat = 64
|
|
59
|
+
|
|
60
|
+
// MARK: - Tab bar
|
|
61
|
+
|
|
62
|
+
struct BottomTabBar: View {
|
|
63
|
+
let items: [BottomTabItem]
|
|
64
|
+
let floatingButton: BottomTabFloatingButton?
|
|
65
|
+
let selectedIndex: Int
|
|
66
|
+
let onTabSelected: (Int) -> Void
|
|
67
|
+
|
|
68
|
+
private var screenWidth: CGFloat { UIScreen.main.bounds.width }
|
|
69
|
+
private var mid: Int { items.count / 2 }
|
|
70
|
+
|
|
71
|
+
private var tabWidth: CGFloat {
|
|
72
|
+
if floatingButton != nil {
|
|
73
|
+
return (screenWidth - bottomTabFloatingButtonWidth) / CGFloat(items.count)
|
|
74
|
+
}
|
|
75
|
+
return screenWidth / CGFloat(items.count)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private var indicatorOffset: CGFloat {
|
|
79
|
+
let adjust: CGFloat = (floatingButton != nil && selectedIndex >= mid) ? bottomTabFloatingButtonWidth : 0
|
|
80
|
+
return tabWidth * CGFloat(selectedIndex) + tabWidth / 2 - screenWidth / 2 + adjust
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var body: some View {
|
|
84
|
+
ZStack(alignment: .bottom) {
|
|
85
|
+
HStack(spacing: 0) {
|
|
86
|
+
if floatingButton == nil {
|
|
87
|
+
tabItems(from: 0, to: items.count)
|
|
88
|
+
} else {
|
|
89
|
+
tabItems(from: 0, to: mid)
|
|
90
|
+
Spacer()
|
|
91
|
+
.frame(width: bottomTabFloatingButtonWidth)
|
|
92
|
+
.padding(.horizontal, Spacing.XXS)
|
|
93
|
+
tabItems(from: mid, to: items.count)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
.frame(maxWidth: .infinity)
|
|
97
|
+
.frame(height: BOTTOM_TAB_BAR_HEIGHT)
|
|
98
|
+
.background(Colors.black01)
|
|
99
|
+
|
|
100
|
+
// Animated selection indicator.
|
|
101
|
+
RoundedRectangle(cornerRadius: 2)
|
|
102
|
+
.fill(Colors.primary)
|
|
103
|
+
.frame(width: 44, height: 2)
|
|
104
|
+
.offset(x: indicatorOffset, y: -BOTTOM_TAB_BAR_HEIGHT + 2)
|
|
105
|
+
.animation(.easeInOut(duration: 0.25), value: selectedIndex)
|
|
106
|
+
|
|
107
|
+
// Top shadow.
|
|
108
|
+
LinearGradient(
|
|
109
|
+
colors: [Color.black.opacity(0.05), .clear],
|
|
110
|
+
startPoint: .bottom,
|
|
111
|
+
endPoint: .top
|
|
112
|
+
)
|
|
113
|
+
.frame(height: 6)
|
|
114
|
+
.frame(maxWidth: .infinity)
|
|
115
|
+
.offset(y: -BOTTOM_TAB_BAR_HEIGHT)
|
|
116
|
+
|
|
117
|
+
if let floatingButton {
|
|
118
|
+
CenterFloatingButton(data: floatingButton)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@ViewBuilder
|
|
124
|
+
private func tabItems(from: Int, to: Int) -> some View {
|
|
125
|
+
ForEach(from..<to, id: \.self) { index in
|
|
126
|
+
TabBarItem(
|
|
127
|
+
item: items[index],
|
|
128
|
+
selected: index == selectedIndex,
|
|
129
|
+
onClick: { onTabSelected(index) }
|
|
130
|
+
)
|
|
131
|
+
.frame(maxWidth: .infinity)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MARK: - Tab item
|
|
137
|
+
|
|
138
|
+
private struct TabBarItem: View {
|
|
139
|
+
let item: BottomTabItem
|
|
140
|
+
let selected: Bool
|
|
141
|
+
let onClick: () -> Void
|
|
142
|
+
|
|
143
|
+
private var tintColor: Color { selected ? Colors.primary : Colors.black10 }
|
|
144
|
+
|
|
145
|
+
/// Mirrors Compose's `formatLabel`: hide a badge that is the number "0".
|
|
146
|
+
private var badgeLabel: String? {
|
|
147
|
+
guard let label = item.badgeLabel else { return nil }
|
|
148
|
+
if let n = Int(label), n == 0 { return nil }
|
|
149
|
+
return label
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var body: some View {
|
|
153
|
+
ZStack(alignment: .topTrailing) {
|
|
154
|
+
VStack(spacing: 0) {
|
|
155
|
+
Icon(source: item.icon, size: 28, color: tintColor)
|
|
156
|
+
.padding(Spacing.XS)
|
|
157
|
+
MomoText(
|
|
158
|
+
item.label,
|
|
159
|
+
typography: selected ? .labelXsMedium : .descriptionXsRegular,
|
|
160
|
+
color: tintColor,
|
|
161
|
+
textAlign: .center,
|
|
162
|
+
maxLines: 1
|
|
163
|
+
)
|
|
164
|
+
.truncationMode(.tail)
|
|
165
|
+
}
|
|
166
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
167
|
+
.padding(.horizontal, Spacing.S)
|
|
168
|
+
.padding(.vertical, Spacing.S)
|
|
169
|
+
|
|
170
|
+
if let badgeLabel {
|
|
171
|
+
if badgeLabel.isEmpty {
|
|
172
|
+
BadgeDot(size: .small)
|
|
173
|
+
.offset(x: -Spacing.S, y: Spacing.XS)
|
|
174
|
+
} else {
|
|
175
|
+
Badge(label: badgeLabel)
|
|
176
|
+
.offset(x: -Spacing.XS, y: 0)
|
|
177
|
+
}
|
|
178
|
+
} else if item.showDot {
|
|
179
|
+
BadgeDot(size: .small)
|
|
180
|
+
.offset(x: -Spacing.S, y: Spacing.XS)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
184
|
+
.padding(.horizontal, Spacing.XXS)
|
|
185
|
+
.contentShape(Rectangle())
|
|
186
|
+
.onTapGesture { onClick() }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// MARK: - Center floating button
|
|
191
|
+
|
|
192
|
+
private struct CenterFloatingButton: View {
|
|
193
|
+
let data: BottomTabFloatingButton
|
|
194
|
+
|
|
195
|
+
var body: some View {
|
|
196
|
+
VStack(spacing: 0) {
|
|
197
|
+
CurvedContainer()
|
|
198
|
+
Circle()
|
|
199
|
+
.fill(Colors.primary)
|
|
200
|
+
.frame(width: 48, height: 48)
|
|
201
|
+
.overlay(Icon(source: data.icon, size: 28, color: .white))
|
|
202
|
+
Spacer().frame(height: 4)
|
|
203
|
+
MomoText(data.label, typography: .labelXsMedium, color: Colors.black01, textAlign: .center, maxLines: 1)
|
|
204
|
+
.frame(maxWidth: .infinity)
|
|
205
|
+
.frame(height: 14)
|
|
206
|
+
.background(Colors.primary)
|
|
207
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.XL))
|
|
208
|
+
}
|
|
209
|
+
.frame(width: bottomTabFloatingButtonWidth)
|
|
210
|
+
.padding(.horizontal, Spacing.XXS)
|
|
211
|
+
.padding(.vertical, Spacing.S)
|
|
212
|
+
.contentShape(Rectangle())
|
|
213
|
+
.onTapGesture { data.onPress() }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//
|
|
2
|
+
// CurvedContainer.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `CurvedContainer` (navigation/bottomtab/CurvedContainer.kt).
|
|
6
|
+
// Draws the surface-colored notch that sits behind the center floating button,
|
|
7
|
+
// carving a circular cut-out into the tab bar's top edge.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
struct CurvedContainer: View {
|
|
13
|
+
var circleSize: CGFloat = 48
|
|
14
|
+
var tabBarItemIconSize: CGFloat = 28
|
|
15
|
+
var circleOverSize: CGFloat = 2
|
|
16
|
+
var width: CGFloat = 71
|
|
17
|
+
var height: CGFloat = 16
|
|
18
|
+
var color: Color = Colors.black01
|
|
19
|
+
|
|
20
|
+
var body: some View {
|
|
21
|
+
let circlePositionTop = circleSize - tabBarItemIconSize - 10
|
|
22
|
+
let circleOverTop = circlePositionTop + circleOverSize
|
|
23
|
+
let circleR = circleSize / 2
|
|
24
|
+
let circleOverR = circleR + circleOverSize
|
|
25
|
+
let centerX = width / 2
|
|
26
|
+
|
|
27
|
+
CurvedShape(circleOverTop: circleOverTop, centerX: centerX, circleOverR: circleOverR, width: width)
|
|
28
|
+
.fill(color)
|
|
29
|
+
.frame(width: width, height: height)
|
|
30
|
+
.offset(y: 24)
|
|
31
|
+
.zIndex(-1)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Mirrors Compose's `getPath`: a flat bar with a smooth circular dip in the
|
|
36
|
+
/// middle, sized to cradle the floating button.
|
|
37
|
+
private struct CurvedShape: Shape {
|
|
38
|
+
let circleOverTop: CGFloat
|
|
39
|
+
let centerX: CGFloat
|
|
40
|
+
let circleOverR: CGFloat
|
|
41
|
+
let width: CGFloat
|
|
42
|
+
|
|
43
|
+
func path(in rect: CGRect) -> Path {
|
|
44
|
+
var path = Path()
|
|
45
|
+
path.move(to: CGPoint(x: 0, y: circleOverTop))
|
|
46
|
+
path.addLine(to: CGPoint(x: centerX - circleOverR - 40, y: circleOverTop))
|
|
47
|
+
|
|
48
|
+
path.addCurve(
|
|
49
|
+
to: CGPoint(x: centerX, y: 0),
|
|
50
|
+
control1: CGPoint(x: centerX - circleOverR, y: circleOverTop),
|
|
51
|
+
control2: CGPoint(x: centerX - circleOverR, y: 0)
|
|
52
|
+
)
|
|
53
|
+
path.addCurve(
|
|
54
|
+
to: CGPoint(x: centerX + circleOverR + 40, y: circleOverTop),
|
|
55
|
+
control1: CGPoint(x: centerX + circleOverR, y: 0),
|
|
56
|
+
control2: CGPoint(x: centerX + circleOverR, y: circleOverTop)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
path.addLine(to: CGPoint(x: width, y: circleOverTop))
|
|
60
|
+
path.addLine(to: CGPoint(x: width, y: 60))
|
|
61
|
+
path.addLine(to: CGPoint(x: 0, y: 60))
|
|
62
|
+
path.closeSubpath()
|
|
63
|
+
return path
|
|
64
|
+
}
|
|
65
|
+
}
|