@momo-kits/native-kits 0.167.1-duo.8-debug → 0.167.1-duo.9
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 +4 -1
- package/ios/Application/ApplicationEnvironment.swift +11 -0
- package/ios/Application/ComposeApi.swift +2 -1
- package/ios/Application/{Components.swift → LegacyHeader.swift} +6 -62
- package/ios/Application/Screen.swift +3 -2
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +0 -1
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +12 -20
- 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 +9 -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 +18 -0
- package/ios/Icon/Icon.swift +7 -1
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -34
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +160 -147
- package/ios/Input/InputPhoneNumber.swift +12 -9
- package/ios/Input/InputSearch.swift +102 -75
- package/ios/Input/InputTextArea.swift +128 -145
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +33 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +123 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +10 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +61 -9
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTab.swift +60 -34
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +11 -6
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
- package/ios/Navigation/Component/Header.swift +493 -0
- package/ios/Navigation/Component/HeaderBackground.swift +134 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +386 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +20 -23
- package/ios/{Application/Navigation/component → Navigation/Component}/SideBar.swift +49 -28
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
- package/ios/Navigation/HostBackGestureBridge.swift +57 -0
- package/ios/Navigation/Navigation.swift +190 -0
- package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +46 -41
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +65 -9
- package/ios/{Application → Navigation}/StackScreen.swift +236 -103
- 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 +79 -159
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +97 -2
- package/ios/Popup/PopupNotify.swift +27 -55
- package/ios/Popup/PopupPromotion.swift +22 -50
- 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/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 +41 -27
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/Text.swift +61 -19
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/package.json +1 -1
- package/build.gradle.kts +0 -11
- package/compose/build.gradle.kts +0 -204
- package/compose/build.gradle.kts.backup +0 -204
- package/compose/compose.podspec +0 -47
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -153
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +0 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
- 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/LiteScreen.kt +0 -831
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
- 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/WidgetContainer.kt +0 -56
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -82
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -356
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -113
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -475
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -266
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -240
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -264
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -361
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -108
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -310
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -323
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -199
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -91
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -552
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -88
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -498
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -234
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -188
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -188
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -297
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -232
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -338
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -137
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -708
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -203
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -232
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +0 -98
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +0 -312
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -408
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -92
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -105
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -366
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -37
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -185
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -132
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -74
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +0 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -217
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +0 -86
- package/gradle/libs.versions.toml +0 -69
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +0 -8
- package/gradle.properties +0 -26
- package/gradlew +0 -252
- package/gradlew.bat +0 -94
- package/ios/Application/HeaderRight.swift +0 -324
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -29
- package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -54
- package/ios/Application/Navigation/component/Header.swift +0 -183
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -84
- 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 -280
- package/ios-demo/README.md +0 -120
- package/settings.gradle.kts +0 -64
- /package/ios/{Application/Layout → Layout}/ScreenLayout.swift +0 -0
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
- /package/ios/{Application/Navigation → Navigation}/BottomTab/DuoBarCompression.swift +0 -0
- /package/ios/{Application/Navigation → Navigation}/BottomTab/DuoTabBar.swift +0 -0
- /package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +0 -0
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
//
|
|
2
2
|
// StackScreen.swift
|
|
3
|
-
//
|
|
3
|
+
// MoMoUIKits
|
|
4
4
|
//
|
|
5
|
-
// SwiftUI mirror of Compose's `StackScreen
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// (`FloatingContent`) and footer (`NavigationFooter`) — instead of one
|
|
9
|
-
// monolithic, partially-duplicated view. The public init matches `Screen`
|
|
10
|
-
// so call sites (e.g. NavigationContainer's ScreenHost) can swap over.
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/StackScreen.kt`: header layer, scrollable content
|
|
6
|
+
// (`StackScreenContent`), animated search, floating button, footer (`NavigationFooter`) and the
|
|
7
|
+
// iPhone Duo side bar.
|
|
11
8
|
//
|
|
12
9
|
|
|
13
10
|
import SwiftUI
|
|
@@ -34,7 +31,9 @@ public struct StackScreen<Content: View>: View {
|
|
|
34
31
|
var useAvoidKeyboard: Bool
|
|
35
32
|
var keyboardShouldPersistTaps: Bool
|
|
36
33
|
var footer: (() -> AnyView)?
|
|
37
|
-
var
|
|
34
|
+
var footerTransparent: Bool
|
|
35
|
+
var footerUseMargin: Bool
|
|
36
|
+
var headerRight: (() -> any View)? = { HeaderRight() }
|
|
38
37
|
var animatedHeader: AnimatedHeader? = nil
|
|
39
38
|
var layoutOffset: CGFloat
|
|
40
39
|
var inputSearchProps: InputSearchProps?
|
|
@@ -62,7 +61,9 @@ public struct StackScreen<Content: View>: View {
|
|
|
62
61
|
useAvoidKeyboard: Bool = true,
|
|
63
62
|
keyboardShouldPersistTaps: Bool = false,
|
|
64
63
|
footer: (() -> AnyView)? = nil,
|
|
65
|
-
|
|
64
|
+
footerTransparent: Bool = false,
|
|
65
|
+
footerUseMargin: Bool = true,
|
|
66
|
+
headerRight: (() -> any View)? = { HeaderRight() },
|
|
66
67
|
animatedHeader: AnimatedHeader? = nil,
|
|
67
68
|
layoutOffset: CGFloat = 56,
|
|
68
69
|
inputSearchProps: InputSearchProps? = nil,
|
|
@@ -89,6 +90,8 @@ public struct StackScreen<Content: View>: View {
|
|
|
89
90
|
self.useAvoidKeyboard = useAvoidKeyboard
|
|
90
91
|
self.keyboardShouldPersistTaps = keyboardShouldPersistTaps
|
|
91
92
|
self.footer = footer
|
|
93
|
+
self.footerTransparent = footerTransparent
|
|
94
|
+
self.footerUseMargin = footerUseMargin
|
|
92
95
|
self.headerRight = headerRight
|
|
93
96
|
self.animatedHeader = animatedHeader
|
|
94
97
|
self.layoutOffset = layoutOffset
|
|
@@ -105,32 +108,46 @@ public struct StackScreen<Content: View>: View {
|
|
|
105
108
|
@State private var headerHide: CGFloat = 0
|
|
106
109
|
@State private var lastScrollOffset: CGFloat = 0
|
|
107
110
|
@State private var headerSettle: Task<Void, Never>?
|
|
111
|
+
@State private var footerHeight: CGFloat = 0
|
|
108
112
|
|
|
109
113
|
@Environment(\.applicationEnvironment) private var appEnv
|
|
110
114
|
@Environment(\.screenLayout) private var layout
|
|
111
115
|
@Environment(\.duoTabBarInsets) private var duoTabBarInsets
|
|
112
|
-
@
|
|
116
|
+
@Environment(\.appTheme) private var theme
|
|
117
|
+
@OptionalEnvironmentObject private var navigation: Navigation?
|
|
113
118
|
|
|
114
|
-
// Auto-tracking state (
|
|
115
|
-
// the once-computed push/back `action`).
|
|
119
|
+
// Auto-tracking state (Compose's `remember { ScreenTrackingState() }` + the once-computed action).
|
|
116
120
|
@State private var trackingState = ScreenTrackingState()
|
|
117
121
|
@State private var trackingAction: String = ScreenTracker.getLastScreenName() != nil ? "push" : "back"
|
|
118
122
|
|
|
119
|
-
///
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
/// Compose's `headerOpacity()`: 0 over a banner or transparent header, 1 once the bar is opaque.
|
|
124
|
+
private var opacity: CGFloat {
|
|
125
|
+
headerOpacity(
|
|
126
|
+
scrollOffset: scrollOffset,
|
|
127
|
+
isAnimated: animatedHeader != nil,
|
|
128
|
+
layoutOffset: layoutOffset,
|
|
129
|
+
transparent: headerTransparent
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private var headerColor: HeaderColor {
|
|
134
|
+
getHeaderColor(
|
|
135
|
+
isAnimated: animatedHeader != nil,
|
|
136
|
+
isSurface: animatedHeader?.isSurface ?? true,
|
|
137
|
+
opacity: opacity,
|
|
138
|
+
tintColor: tintColor
|
|
139
|
+
)
|
|
140
|
+
}
|
|
122
141
|
|
|
123
|
-
// MARK: - Search header (
|
|
142
|
+
// MARK: - Search header (Compose's getInputSearchType / SearchAnimated)
|
|
124
143
|
|
|
125
144
|
private enum InputSearchType { case none, header, animated }
|
|
126
145
|
|
|
127
146
|
private var inputSearchType: InputSearchType {
|
|
128
|
-
guard inputSearchProps != nil else { return .none }
|
|
147
|
+
guard inputSearchProps != nil, headerType != .none else { return .none }
|
|
129
148
|
return useAnimated ? .animated : .header
|
|
130
149
|
}
|
|
131
150
|
|
|
132
|
-
private static var headerHeight: CGFloat { 52 }
|
|
133
|
-
|
|
134
151
|
private var toolkit: (data: HeaderRightData?, tintColor: Color?)? { toolkitHeaderRight(headerRight) }
|
|
135
152
|
|
|
136
153
|
private var showsSideBar: Bool {
|
|
@@ -176,6 +193,8 @@ public struct StackScreen<Content: View>: View {
|
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
195
|
|
|
196
|
+
private var keyboardSize: CGFloat { useAvoidKeyboard ? max(keyboardHeight, 0) : 0 }
|
|
197
|
+
|
|
179
198
|
/// What the footer, tab bar or home indicator takes from the bottom of the window, gap included.
|
|
180
199
|
private var bottomBarsHeight: CGFloat {
|
|
181
200
|
if footer != nil { return footerHeight + NavigationFooter.safeAreaBottom + Spacing.XL }
|
|
@@ -183,35 +202,43 @@ public struct StackScreen<Content: View>: View {
|
|
|
183
202
|
return NavigationFooter.navigationBarInset + Spacing.XL
|
|
184
203
|
}
|
|
185
204
|
|
|
205
|
+
/// Top of the footer (or the navigation bar) from the window bottom; snack bars sit above it.
|
|
206
|
+
private var bottomObstruction: CGFloat {
|
|
207
|
+
guard footer != nil else { return keyboardSize + NavigationFooter.navigationBarInset }
|
|
208
|
+
return footerHeight + NavigationFooter.safeAreaBottom - NavigationFooter.verticalOffset(keyboardSize: keyboardSize)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private func reportBottomObstruction() {
|
|
212
|
+
// bottom tab children leave overplays to the screen hosting the tabs, as in Compose
|
|
213
|
+
guard let navigation, navigation.bottomTabIndex == -1 else { return }
|
|
214
|
+
navigation.bottomObstruction = bottomObstruction
|
|
215
|
+
}
|
|
216
|
+
|
|
186
217
|
/// Keyboard frames are in screen coordinates; UIScreen stays on the outer display on iPhone Duo.
|
|
187
218
|
private var keyboardReferenceHeight: CGFloat {
|
|
188
219
|
layout.isIPhoneDuo ? layout.height : UIScreen.main.bounds.height
|
|
189
220
|
}
|
|
190
221
|
|
|
191
|
-
///
|
|
192
|
-
/// room below the header at scroll 0; it shrinks to 0 as the bar slides up.
|
|
222
|
+
/// Room for the animated search below the header at scroll 0; shrinks as the bar slides up.
|
|
193
223
|
private var animatedSearchInset: CGFloat {
|
|
194
224
|
guard inputSearchType == .animated else { return 0 }
|
|
195
225
|
let scroll = max(scrollOffset, 0)
|
|
196
|
-
return min(max(
|
|
226
|
+
return min(max(HEADER_HEIGHT - scroll, 0), HEADER_HEIGHT)
|
|
197
227
|
}
|
|
198
228
|
|
|
199
229
|
/// Title fades out as the animated search bar rises into the header.
|
|
200
230
|
private var titleOpacity: CGFloat {
|
|
201
231
|
guard inputSearchType == .animated else { return 1 }
|
|
202
232
|
let scroll = max(scrollOffset, 0)
|
|
203
|
-
return 1 - min(max(scroll /
|
|
233
|
+
return 1 - min(max(scroll / HEADER_HEIGHT, 0), 1)
|
|
204
234
|
}
|
|
205
235
|
|
|
206
236
|
// MARK: - Body
|
|
207
237
|
|
|
208
238
|
public var body: some View {
|
|
209
239
|
GeometryReader { _ in
|
|
210
|
-
let keyboardSize: CGFloat = useAvoidKeyboard ? max(keyboardHeight, 0) : 0
|
|
211
|
-
|
|
212
240
|
ZStack(alignment: .top) {
|
|
213
|
-
|
|
214
|
-
(backgroundColor ?? Color.white)
|
|
241
|
+
(backgroundColor ?? theme.colors.background.default)
|
|
215
242
|
.edgesIgnoringSafeArea(.all)
|
|
216
243
|
|
|
217
244
|
VStack(spacing: 0) {
|
|
@@ -229,18 +256,22 @@ public struct StackScreen<Content: View>: View {
|
|
|
229
256
|
.padding(.bottom, footer == nil ? duoTabBarInsets.overlay : 0)
|
|
230
257
|
}
|
|
231
258
|
|
|
232
|
-
NavigationFooter(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
259
|
+
NavigationFooter(
|
|
260
|
+
footerComponent: footer,
|
|
261
|
+
keyboardSize: keyboardSize,
|
|
262
|
+
transparent: footerTransparent,
|
|
263
|
+
useMargin: footerUseMargin
|
|
264
|
+
)
|
|
265
|
+
.padding(.bottom, duoTabBarInsets.overlay)
|
|
266
|
+
.background(
|
|
267
|
+
GeometryReader { proxy in
|
|
268
|
+
Color.clear
|
|
269
|
+
.onAppear { footerHeight = proxy.size.height }
|
|
270
|
+
.onChange(of: proxy.size.height) { footerHeight = $0 }
|
|
271
|
+
}
|
|
272
|
+
)
|
|
241
273
|
}
|
|
242
274
|
|
|
243
|
-
// Animated search bar overlay (mirrors Compose's SearchAnimated layer).
|
|
244
275
|
if inputSearchType == .animated, let props = inputSearchProps {
|
|
245
276
|
SearchAnimatedHeader(
|
|
246
277
|
props: props,
|
|
@@ -248,7 +279,6 @@ public struct StackScreen<Content: View>: View {
|
|
|
248
279
|
hiddenBack: !isBack,
|
|
249
280
|
hasHeaderRight: headerRight != nil,
|
|
250
281
|
headerRightWidth: headerRightWidth,
|
|
251
|
-
headerHeight: Self.headerHeight,
|
|
252
282
|
useSideBar: layout.sideBar != nil,
|
|
253
283
|
hasToolkit: toolkit != nil
|
|
254
284
|
)
|
|
@@ -259,12 +289,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
259
289
|
sideBar: sideBar,
|
|
260
290
|
goBack: goBack,
|
|
261
291
|
toolkit: toolkit,
|
|
262
|
-
headerColor:
|
|
263
|
-
isAnimated: animatedHeader != nil,
|
|
264
|
-
isSurface: animatedHeader?.isSurface ?? true,
|
|
265
|
-
opacity: scrollOpacity,
|
|
266
|
-
tintColor: tintColor
|
|
267
|
-
),
|
|
292
|
+
headerColor: headerColor,
|
|
268
293
|
bottomInset: bottomBarsHeight
|
|
269
294
|
)
|
|
270
295
|
}
|
|
@@ -284,10 +309,11 @@ public struct StackScreen<Content: View>: View {
|
|
|
284
309
|
keyboardHeight = keyboardReferenceHeight - endFrame.origin.y
|
|
285
310
|
}
|
|
286
311
|
}
|
|
287
|
-
//
|
|
288
|
-
// taps still reach buttons/fields while also dismissing the keyboard.
|
|
312
|
+
// Compose's `hideKeyboardOnTap()`: simultaneous, so taps still reach buttons and fields.
|
|
289
313
|
.modifier(HideKeyboardOnTap(enabled: keyboardShouldPersistTaps))
|
|
290
314
|
}
|
|
315
|
+
.onAppear(perform: reportBottomObstruction)
|
|
316
|
+
.onChange(of: bottomObstruction) { _ in reportBottomObstruction() }
|
|
291
317
|
.task(id: screenName) {
|
|
292
318
|
ScreenTracker.trackScreenNavigated(
|
|
293
319
|
maxApi: appEnv.maxApi,
|
|
@@ -301,7 +327,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
301
327
|
} catch {
|
|
302
328
|
return
|
|
303
329
|
}
|
|
304
|
-
let loadTime =
|
|
330
|
+
let loadTime = ScreenTracker.now() - trackingState.time
|
|
305
331
|
ScreenTracker.trackScreenDisplayed(
|
|
306
332
|
maxApi: appEnv.maxApi,
|
|
307
333
|
context: appEnv.applicationContext,
|
|
@@ -309,7 +335,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
309
335
|
duration: loadTime,
|
|
310
336
|
state: trackingState
|
|
311
337
|
)
|
|
312
|
-
let interactionTime =
|
|
338
|
+
let interactionTime = ScreenTracker.now() - trackingState.time
|
|
313
339
|
ScreenTracker.trackScreenInteracted(
|
|
314
340
|
maxApi: appEnv.maxApi,
|
|
315
341
|
context: appEnv.applicationContext,
|
|
@@ -326,10 +352,8 @@ public struct StackScreen<Content: View>: View {
|
|
|
326
352
|
@ViewBuilder
|
|
327
353
|
private var headerAndContent: some View {
|
|
328
354
|
if let animatedHeader = animatedHeader {
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
// fades in over the image as the screen scrolls (mirrors Compose's
|
|
332
|
-
// ScreenContent for HeaderType.Animated).
|
|
355
|
+
// The banner is the top of the scroll content and the body overlaps its lower part;
|
|
356
|
+
// the bar fades in over it (Compose's ScreenContent for HeaderType.Animated).
|
|
333
357
|
ZStack(alignment: .top) {
|
|
334
358
|
StackScreenContent(
|
|
335
359
|
scrollable: scrollable,
|
|
@@ -342,7 +366,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
342
366
|
.edgesIgnoringSafeArea(.all)
|
|
343
367
|
|
|
344
368
|
header
|
|
345
|
-
.background(
|
|
369
|
+
.background(alignment: .top) {
|
|
346
370
|
Group {
|
|
347
371
|
if layout.sideBar != nil {
|
|
348
372
|
// glass shows once the banner has scrolled under the header
|
|
@@ -351,29 +375,29 @@ public struct StackScreen<Content: View>: View {
|
|
|
351
375
|
visible: scrollOffset > heroGlassStart(animatedHeader)
|
|
352
376
|
))
|
|
353
377
|
} else {
|
|
354
|
-
|
|
378
|
+
HeaderBarBackground(opacity: opacity)
|
|
355
379
|
}
|
|
356
380
|
}
|
|
357
381
|
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
358
|
-
|
|
382
|
+
}
|
|
359
383
|
.offset(y: -headerHide)
|
|
360
384
|
.zIndex(100)
|
|
361
385
|
}
|
|
362
386
|
} else if headerTransparent && fullScreenContent {
|
|
387
|
+
// Compose's HeaderType.Transparent(isFullScreenContent): content starts under a clear bar
|
|
363
388
|
ZStack(alignment: .top) {
|
|
364
389
|
header
|
|
365
|
-
.background(
|
|
366
|
-
NavigationHeaderBackground(
|
|
367
|
-
headerType: headerType,
|
|
368
|
-
scrollState: scrollOffset,
|
|
369
|
-
headerTransparent: headerTransparent,
|
|
370
|
-
fullScreenContent: fullScreenContent
|
|
371
|
-
)
|
|
372
|
-
)
|
|
373
390
|
.zIndex(100)
|
|
374
391
|
|
|
375
|
-
StackScreenContent(
|
|
376
|
-
|
|
392
|
+
StackScreenContent(
|
|
393
|
+
scrollable: scrollable,
|
|
394
|
+
scrollOffset: $scrollOffset,
|
|
395
|
+
scrollToTopCallback: scrollToTopCallback,
|
|
396
|
+
animatedSearchInset: animatedSearchInset,
|
|
397
|
+
bottomInset: duoTabBarInsets.overlay,
|
|
398
|
+
content: { duoInsetContent }
|
|
399
|
+
)
|
|
400
|
+
.edgesIgnoringSafeArea(.all)
|
|
377
401
|
}
|
|
378
402
|
} else if useDuoGlass {
|
|
379
403
|
ZStack(alignment: .top) {
|
|
@@ -404,17 +428,29 @@ public struct StackScreen<Content: View>: View {
|
|
|
404
428
|
}
|
|
405
429
|
} else {
|
|
406
430
|
header
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
431
|
+
// top-aligned: an extended band runs past the bar, under the content
|
|
432
|
+
.background(alignment: .top) {
|
|
433
|
+
ZStack(alignment: .top) {
|
|
434
|
+
NavigationHeaderBackground(
|
|
435
|
+
headerType: headerType,
|
|
436
|
+
scrollState: scrollOffset,
|
|
437
|
+
headerTransparent: headerTransparent,
|
|
438
|
+
fullScreenContent: fullScreenContent,
|
|
439
|
+
backgroundColor: backgroundColor
|
|
440
|
+
)
|
|
441
|
+
HeaderBarBackground(opacity: opacity, transparent: headerTransparent || headerType == .none)
|
|
442
|
+
}
|
|
414
443
|
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
415
|
-
|
|
444
|
+
}
|
|
416
445
|
|
|
417
|
-
StackScreenContent(
|
|
446
|
+
StackScreenContent(
|
|
447
|
+
scrollable: scrollable,
|
|
448
|
+
scrollOffset: $scrollOffset,
|
|
449
|
+
scrollToTopCallback: scrollToTopCallback,
|
|
450
|
+
animatedSearchInset: animatedSearchInset,
|
|
451
|
+
bottomInset: duoTabBarInsets.overlay,
|
|
452
|
+
content: content
|
|
453
|
+
)
|
|
418
454
|
}
|
|
419
455
|
}
|
|
420
456
|
|
|
@@ -430,9 +466,8 @@ public struct StackScreen<Content: View>: View {
|
|
|
430
466
|
max(layout.width / animatedHeader.aspectRatio.value - layout.headerHeight, 0)
|
|
431
467
|
}
|
|
432
468
|
|
|
433
|
-
///
|
|
434
|
-
///
|
|
435
|
-
/// the lower part of the image (mirrors Compose's image box + padded content).
|
|
469
|
+
/// The banner fills the width at its aspect ratio; the body starts `layoutOffset` below the
|
|
470
|
+
/// header band so it overlaps the banner's lower part.
|
|
436
471
|
@ViewBuilder
|
|
437
472
|
private func animatedHeroBody(_ animatedHeader: AnimatedHeader) -> some View {
|
|
438
473
|
let width = layout.width
|
|
@@ -458,11 +493,10 @@ public struct StackScreen<Content: View>: View {
|
|
|
458
493
|
headerTitle: headerTitle,
|
|
459
494
|
headerRight: headerRight,
|
|
460
495
|
goBack: goBack,
|
|
461
|
-
opacity:
|
|
496
|
+
opacity: opacity,
|
|
462
497
|
animatedHeader: animatedHeader,
|
|
463
498
|
scrollState: scrollOffset,
|
|
464
|
-
//
|
|
465
|
-
// header shows the (fading) title instead of an inline search field.
|
|
499
|
+
// the animated search draws its own field over the (fading) title
|
|
466
500
|
inputSearchProps: inputSearchType == .header ? inputSearchProps : nil,
|
|
467
501
|
tintColor: tintColor,
|
|
468
502
|
titleOpacity: titleOpacity
|
|
@@ -470,18 +504,26 @@ public struct StackScreen<Content: View>: View {
|
|
|
470
504
|
}
|
|
471
505
|
}
|
|
472
506
|
|
|
507
|
+
// MARK: - Bottom-tab root preference
|
|
508
|
+
|
|
509
|
+
/// Set by `BottomTab` so the `StackScreen` hosting it keeps the full width and leaves the side
|
|
510
|
+
/// bar to the tabs (Compose's `Navigation.markAsBottomTabRoot()`).
|
|
511
|
+
public struct IsBottomTabRootKey: PreferenceKey {
|
|
512
|
+
public static var defaultValue: Bool = false
|
|
513
|
+
public static func reduce(value: inout Bool, nextValue: () -> Bool) {
|
|
514
|
+
value = value || nextValue()
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
473
518
|
// MARK: - Screen content (scrollable body)
|
|
474
519
|
|
|
475
|
-
///
|
|
476
|
-
/// when scrollable, tracks the scroll offset so the header/FAB can react to it.
|
|
520
|
+
/// Compose's `MainContent` / `ScreenContent`: the body, tracking the scroll offset when scrollable.
|
|
477
521
|
private struct StackScreenContent<Content: View>: View {
|
|
478
522
|
let scrollable: Bool
|
|
479
523
|
@Binding var scrollOffset: CGFloat
|
|
480
|
-
///
|
|
481
|
-
/// scroll-to-top triggered by a status-bar tap.
|
|
524
|
+
/// Compose's `ScrollData.scrollToTopCallback`, fired with the status-bar tap scroll-to-top.
|
|
482
525
|
let scrollToTopCallback: (() -> Void)?
|
|
483
|
-
///
|
|
484
|
-
/// MainContent animated top spacer). Zero for non-animated screens.
|
|
526
|
+
/// Room for the animated search below the header (Compose's MainContent spacer).
|
|
485
527
|
var animatedSearchInset: CGFloat = 0
|
|
486
528
|
/// Room for a header the content scrolls under (iPhone Duo glass header).
|
|
487
529
|
var topInset: CGFloat = 0
|
|
@@ -497,9 +539,8 @@ private struct StackScreenContent<Content: View>: View {
|
|
|
497
539
|
ScrollViewReader { proxy in
|
|
498
540
|
ScrollView(.vertical, showsIndicators: false) {
|
|
499
541
|
VStack(spacing: 0) {
|
|
500
|
-
// Top probe + scroll-to-top anchor
|
|
501
|
-
//
|
|
502
|
-
// "pixels scrolled" value, independent of the inset below.
|
|
542
|
+
// Top probe + scroll-to-top anchor, measured in the scroll's own space so the
|
|
543
|
+
// offset is a clean 0-based "points scrolled", independent of the insets below.
|
|
503
544
|
Color.clear
|
|
504
545
|
.frame(height: 0)
|
|
505
546
|
.id(topAnchorID)
|
|
@@ -527,8 +568,7 @@ private struct StackScreenContent<Content: View>: View {
|
|
|
527
568
|
}
|
|
528
569
|
}
|
|
529
570
|
.coordinateSpace(name: scrollSpace)
|
|
530
|
-
// Status-bar tap (
|
|
531
|
-
// notifies the consumer, mirroring Compose's scrollToTopCallback.
|
|
571
|
+
// Status-bar tap (MoMoStatusBarTap swizzle) scrolls to top and notifies the consumer.
|
|
532
572
|
.onReceive(NotificationCenter.default.publisher(for: MoMoStatusBarTap.notificationName)) { _ in
|
|
533
573
|
withAnimation {
|
|
534
574
|
proxy.scrollTo(topAnchorID, anchor: .top)
|
|
@@ -546,28 +586,26 @@ private struct StackScreenContent<Content: View>: View {
|
|
|
546
586
|
|
|
547
587
|
// MARK: - Animated search header
|
|
548
588
|
|
|
549
|
-
///
|
|
550
|
-
///
|
|
551
|
-
/// horizontal insets grow to make room for the back button and header-right.
|
|
552
|
-
/// Note: Compose's scroll-end snap and background-color lerp are not ported.
|
|
589
|
+
/// Compose's `SearchAnimated`: the search slides from below the header (scroll 0) up into the bar
|
|
590
|
+
/// while its insets grow to clear the back button and header right.
|
|
553
591
|
private struct SearchAnimatedHeader: View {
|
|
554
592
|
let props: InputSearchProps
|
|
555
593
|
let scrollOffset: CGFloat
|
|
556
594
|
let hiddenBack: Bool
|
|
557
595
|
let hasHeaderRight: Bool
|
|
558
596
|
let headerRightWidth: CGFloat
|
|
559
|
-
let headerHeight: CGFloat
|
|
560
597
|
var useSideBar: Bool = false
|
|
561
598
|
var hasToolkit: Bool = false
|
|
562
599
|
|
|
563
600
|
@Environment(\.screenLayout) private var layout
|
|
601
|
+
@Environment(\.appTheme) private var theme
|
|
564
602
|
|
|
565
603
|
var body: some View {
|
|
566
604
|
let scroll = max(scrollOffset, 0)
|
|
567
605
|
|
|
568
606
|
// Insets are measured from the safe-area top; collapsed, the search is centered in the bar row.
|
|
569
607
|
let rowTop = layout.headerStatusBarHeight - layout.insets.top
|
|
570
|
-
let minTop = rowTop + (layout.headerBarHeight -
|
|
608
|
+
let minTop = rowTop + (layout.headerBarHeight - HEADER_HEIGHT) / 2
|
|
571
609
|
let maxTop = layout.headerHeight - layout.insets.top
|
|
572
610
|
let minSide = Spacing.M
|
|
573
611
|
let maxStart: CGFloat = hiddenBack || useSideBar ? Spacing.M : 48
|
|
@@ -581,8 +619,8 @@ private struct SearchAnimatedHeader: View {
|
|
|
581
619
|
|
|
582
620
|
VStack(spacing: 0) {
|
|
583
621
|
Spacer().frame(height: top)
|
|
584
|
-
InputSearch(props: props)
|
|
585
|
-
.frame(height:
|
|
622
|
+
InputSearch(props: headerSearchProps(props, scroll: scroll, layout: layout, theme: theme))
|
|
623
|
+
.frame(height: HEADER_HEIGHT)
|
|
586
624
|
.padding(.leading, start)
|
|
587
625
|
.padding(.trailing, end)
|
|
588
626
|
Spacer(minLength: 0)
|
|
@@ -593,7 +631,7 @@ private struct SearchAnimatedHeader: View {
|
|
|
593
631
|
|
|
594
632
|
// MARK: - Floating content
|
|
595
633
|
|
|
596
|
-
///
|
|
634
|
+
/// Compose's `FloatingContent`: no FAB props means nothing renders.
|
|
597
635
|
private struct FloatingContent: View {
|
|
598
636
|
let fabProps: FabProps?
|
|
599
637
|
let keyboardSize: CGFloat
|
|
@@ -624,11 +662,106 @@ private struct FloatingContent: View {
|
|
|
624
662
|
}
|
|
625
663
|
}
|
|
626
664
|
|
|
665
|
+
// MARK: - Footer
|
|
666
|
+
|
|
667
|
+
/// Compose's `Footer`: the band pinned under the content — surface background, S/M padding,
|
|
668
|
+
/// the navigation-bar reserve, and a 6pt shadow cast up onto the content.
|
|
669
|
+
/// Named `NavigationFooter` to coexist with the deprecated `Screen`'s footer.
|
|
670
|
+
public struct NavigationFooter: View {
|
|
671
|
+
/// Nothing renders when nil.
|
|
672
|
+
var footerComponent: (() -> AnyView)?
|
|
673
|
+
/// Current keyboard height, measured from the bottom of the screen.
|
|
674
|
+
var keyboardSize: CGFloat
|
|
675
|
+
var transparent: Bool
|
|
676
|
+
var useMargin: Bool
|
|
677
|
+
|
|
678
|
+
public init(
|
|
679
|
+
footerComponent: (() -> AnyView)?,
|
|
680
|
+
keyboardSize: CGFloat = 0,
|
|
681
|
+
transparent: Bool = false,
|
|
682
|
+
useMargin: Bool = true
|
|
683
|
+
) {
|
|
684
|
+
self.footerComponent = footerComponent
|
|
685
|
+
self.keyboardSize = keyboardSize
|
|
686
|
+
self.transparent = transparent
|
|
687
|
+
self.useMargin = useMargin
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
@Environment(\.appTheme) private var theme
|
|
691
|
+
|
|
692
|
+
/// Read from the window, not from an enclosing `GeometryProxy`: the proxy's bottom
|
|
693
|
+
/// inset grows to the keyboard height while the keyboard is up.
|
|
694
|
+
static var safeAreaBottom: CGFloat {
|
|
695
|
+
UIApplication.shared.connectedScenes
|
|
696
|
+
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
|
|
697
|
+
.first ?? 0
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/// Compose's `AppNavigationBar.current` (`getNavigationBarHeight` clamps iOS to 21pt).
|
|
701
|
+
static var navigationBarInset: CGFloat { min(safeAreaBottom, 21) }
|
|
702
|
+
|
|
703
|
+
/// Compose's `bottomPadding + Spacing.S` with the keyboard down. The footer is laid
|
|
704
|
+
/// out inside the safe area, which already covers part of that gap.
|
|
705
|
+
static var bottomPadding: CGFloat {
|
|
706
|
+
max(navigationBarInset + Spacing.S - safeAreaBottom, 0)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/// Deliberately an offset and not padding: growing the footer feeds back into SwiftUI's
|
|
710
|
+
/// keyboard avoidance, so a padding-based lift never settles at the right place.
|
|
711
|
+
/// Negative lifts the band over the keyboard; positive shaves the few points by which
|
|
712
|
+
/// the safe area over-reserves compared to Compose. `FloatingContent` rides the same
|
|
713
|
+
/// amount so the FAB keeps its distance above the band.
|
|
714
|
+
///
|
|
715
|
+
/// Natural position is `safeAreaBottom + bottomPadding` above the bottom of the screen;
|
|
716
|
+
/// Compose puts it at `(AppNavigationBar - keyboard).coerceAtLeast(0) + Spacing.S` on
|
|
717
|
+
/// top of the `imePadding()` lift — i.e. `max(navigationBar, keyboard) + Spacing.S`.
|
|
718
|
+
static func verticalOffset(keyboardSize: CGFloat) -> CGFloat {
|
|
719
|
+
(safeAreaBottom + bottomPadding) - (max(navigationBarInset, keyboardSize) + Spacing.S)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
public var body: some View {
|
|
723
|
+
if let footerComponent = footerComponent {
|
|
724
|
+
let margin: CGFloat = useMargin ? Spacing.S : 0
|
|
725
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
726
|
+
footerComponent()
|
|
727
|
+
}
|
|
728
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
729
|
+
.padding(.top, margin)
|
|
730
|
+
.padding(.horizontal, useMargin ? Spacing.M : 0)
|
|
731
|
+
// without the margin the Spacing.S under the content goes too: negative, so the
|
|
732
|
+
// content sinks into the reserve while the frame keeps its bottom edge
|
|
733
|
+
.padding(.bottom, Self.bottomPadding - (Spacing.S - margin))
|
|
734
|
+
// Compose paints the surface all the way to the bottom of the screen; the
|
|
735
|
+
// footer's own frame stops at the safe area, so only the fill bleeds down.
|
|
736
|
+
.background(
|
|
737
|
+
(transparent ? Color.clear : theme.colors.background.surface)
|
|
738
|
+
.ignoresSafeArea(.container, edges: [.bottom, .horizontal])
|
|
739
|
+
)
|
|
740
|
+
.overlay(alignment: .top) {
|
|
741
|
+
if !transparent { shadow }
|
|
742
|
+
}
|
|
743
|
+
.offset(y: Self.verticalOffset(keyboardSize: keyboardSize))
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/// Compose offsets the shadow box by -6dp, so it lands on the content above the
|
|
748
|
+
/// footer rather than on the footer surface itself.
|
|
749
|
+
private var shadow: some View {
|
|
750
|
+
LinearGradient(
|
|
751
|
+
colors: [Color.clear, Color.black.opacity(0.05)],
|
|
752
|
+
startPoint: .top,
|
|
753
|
+
endPoint: .bottom
|
|
754
|
+
)
|
|
755
|
+
.frame(height: 6)
|
|
756
|
+
.offset(y: -6)
|
|
757
|
+
.allowsHitTesting(false)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
627
761
|
// MARK: - Hide keyboard on tap
|
|
628
762
|
|
|
629
|
-
///
|
|
630
|
-
///
|
|
631
|
-
/// is not consumed — buttons and text fields keep working.
|
|
763
|
+
/// Compose's `Modifier.hideKeyboardOnTap()`. Uses `simultaneousGesture` so the tap is not
|
|
764
|
+
/// consumed — buttons and text fields keep working.
|
|
632
765
|
private struct HideKeyboardOnTap: ViewModifier {
|
|
633
766
|
let enabled: Bool
|
|
634
767
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import Foundation
|
|
10
10
|
|
|
11
11
|
public final class ScreenTrackingState {
|
|
12
|
-
public var time: Double =
|
|
12
|
+
public var time: Double = ScreenTracker.now()
|
|
13
13
|
public var releaseNavigated: Bool = false
|
|
14
14
|
public var releaseLoad: Bool = false
|
|
15
15
|
public var releaseInteraction: Bool = false
|
|
@@ -22,6 +22,12 @@ public final class ScreenTrackingState {
|
|
|
22
22
|
public enum ScreenTracker {
|
|
23
23
|
private static var lastScreenName: String?
|
|
24
24
|
|
|
25
|
+
/// Epoch milliseconds, the clock Compose tracks with.
|
|
26
|
+
public static func now() -> Double { Date().timeIntervalSince1970 * 1000 }
|
|
27
|
+
|
|
28
|
+
/// Compose sends whole milliseconds (`Long`).
|
|
29
|
+
private static func millis(_ value: Double) -> Int64 { Int64(value.rounded()) }
|
|
30
|
+
|
|
25
31
|
public static func trackScreenNavigated(
|
|
26
32
|
maxApi: MaxApi?,
|
|
27
33
|
context: MiniAppContext?,
|
|
@@ -35,7 +41,8 @@ public enum ScreenTracker {
|
|
|
35
41
|
lastScreenName = screenName
|
|
36
42
|
|
|
37
43
|
var params = buildBaseParams(context: context, screenName: screenName)
|
|
38
|
-
|
|
44
|
+
// keeps the key with a null value; a subscript assignment of nil would drop it
|
|
45
|
+
params.updateValue(previousScreen, forKey: "pre_screen_name")
|
|
39
46
|
params["state"] = "navigated"
|
|
40
47
|
params["action"] = action
|
|
41
48
|
maxApi?.track(eventName: "auto_screen_navigated", params: params)
|
|
@@ -55,7 +62,7 @@ public enum ScreenTracker {
|
|
|
55
62
|
|
|
56
63
|
var params = buildBaseParams(context: context, screenName: screenName)
|
|
57
64
|
params["state"] = "load"
|
|
58
|
-
params["duration"] = duration
|
|
65
|
+
params["duration"] = millis(duration)
|
|
59
66
|
maxApi?.track(eventName: "auto_screen_displayed", params: params)
|
|
60
67
|
|
|
61
68
|
state.releaseLoad = true
|
|
@@ -73,8 +80,8 @@ public enum ScreenTracker {
|
|
|
73
80
|
|
|
74
81
|
var params = buildBaseParams(context: context, screenName: screenName)
|
|
75
82
|
params["state"] = "interaction"
|
|
76
|
-
params["duration"] = duration
|
|
77
|
-
params["total_duration"] = totalDuration
|
|
83
|
+
params["duration"] = millis(duration)
|
|
84
|
+
params["total_duration"] = millis(totalDuration)
|
|
78
85
|
maxApi?.track(eventName: "auto_screen_interacted", params: params)
|
|
79
86
|
|
|
80
87
|
state.releaseInteraction = true
|