@momo-kits/native-kits 0.167.1-duo.3 → 0.167.1-duo.4-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 +17 -8
- package/build.gradle.kts +11 -0
- package/compose/build.gradle.kts +190 -0
- package/compose/build.gradle.kts.backup +190 -0
- package/compose/compose.podspec +47 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +151 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +831 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +266 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +264 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +334 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +98 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +552 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +157 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +299 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +622 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +175 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +381 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +83 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +105 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +347 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +71 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +213 -0
- package/gradle/libs.versions.toml +69 -0
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/gradle.properties +26 -0
- package/gradlew +252 -0
- package/gradlew.bat +94 -0
- package/ios/Application/ApplicationEnvironment.swift +6 -0
- package/ios/Application/Components.swift +1 -3
- package/ios/Application/HeaderRight.swift +19 -27
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +9 -30
- package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +2 -5
- package/ios/Application/Navigation/HeaderUser.swift +1 -7
- package/ios/Application/Navigation/NavigationContainer.swift +45 -45
- package/ios/Application/Navigation/Navigator.swift +122 -29
- package/ios/Application/Navigation/NavigatorManager.swift +13 -6
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +4 -41
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +1 -5
- package/ios/Application/Navigation/component/Header.swift +4 -12
- package/ios/Application/Navigation/component/HeaderBackground.swift +2 -3
- package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +9 -61
- package/ios/Application/StackScreen.swift +18 -135
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +59 -5
- package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
- package/ios/Extensions/Color++.swift +20 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Icon/Icon.swift +14 -22
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/Image/Image.swift +16 -5
- package/ios/Lottie/LottieView.swift +4 -2
- package/ios/Popup/PopupDisplay.swift +68 -84
- package/ios/Popup/PopupNotify.swift +32 -43
- package/ios/Popup/PopupPromotion.swift +6 -18
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/Template/TrustBanner/TrustBanner.swift +69 -31
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/README.md +120 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +0 -1
- package/package.json +1 -1
- package/scripts/gen-ios-icons.mjs +36 -0
- package/settings.gradle.kts +64 -0
- package/.claude/scheduled_tasks.lock +0 -1
- package/.claude/settings.local.json +0 -185
- package/ios/Application/Layout/ScreenLayout.swift +0 -196
- package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +0 -275
- package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -55
- package/ios/Application/Navigation/component/SideBar.swift +0 -155
- package/ios/Popup/PopupLayout.swift +0 -45
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +0 -76
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +0 -170
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
import SwiftUI
|
|
12
12
|
|
|
13
|
-
@available(iOS 16.0, *)
|
|
14
13
|
public struct HeaderUserView: View {
|
|
15
14
|
private let data: HeaderUserData
|
|
16
15
|
|
|
@@ -20,11 +19,8 @@ public struct HeaderUserView: View {
|
|
|
20
19
|
|
|
21
20
|
private let avatarSize: CGFloat = Spacing.XXL // 32
|
|
22
21
|
|
|
23
|
-
@Environment(\.screenLayout) private var layout
|
|
24
|
-
|
|
25
|
-
// back button and toolkit share the bar unless the iPhone Duo side bar holds them
|
|
26
22
|
private var maxWidth: CGFloat {
|
|
27
|
-
|
|
23
|
+
UIScreen.main.bounds.width - 172
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
public var body: some View {
|
|
@@ -108,7 +104,6 @@ public struct HeaderUserView: View {
|
|
|
108
104
|
|
|
109
105
|
/// Mirrors Compose's `AvatarGroup`: lays out 1..4 avatars in a fixed collage,
|
|
110
106
|
/// falls back to initials when empty, and shows a "+n" overflow tile beyond 4.
|
|
111
|
-
@available(iOS 16.0, *)
|
|
112
107
|
private struct AvatarGroup: View {
|
|
113
108
|
let urls: [String]
|
|
114
109
|
let title: String
|
|
@@ -190,7 +185,6 @@ private struct AvatarGroup: View {
|
|
|
190
185
|
}
|
|
191
186
|
}
|
|
192
187
|
|
|
193
|
-
@available(iOS 16.0, *)
|
|
194
188
|
private struct InitialsAvatar: View {
|
|
195
189
|
let name: String
|
|
196
190
|
let size: CGFloat
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// MoMoUIKits
|
|
4
4
|
//
|
|
5
5
|
// SwiftUI mirror of Compose's NavigationContainer.
|
|
6
|
-
// Wraps NavigationStack, injects Navigator into the environment, and renders
|
|
7
|
-
// every registered screen inside `
|
|
8
|
-
// behavior as Compose's StackScreen.
|
|
6
|
+
// Wraps NavigationStack (iOS 16+), injects Navigator into the environment, and renders
|
|
7
|
+
// every registered screen inside `StackScreen` chrome built from its options.
|
|
9
8
|
//
|
|
10
9
|
|
|
11
10
|
import SwiftUI
|
|
@@ -55,41 +54,23 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
public var body: some View {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.environment(\.applicationEnvironment, applicationEnvironment)
|
|
76
|
-
// AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
|
|
77
|
-
.momoLocalize(localize)
|
|
78
|
-
.momoFontScale(context: applicationEnvironment.applicationContext)
|
|
79
|
-
// AI-GENERATED END: route Localize + font-scale injection through the observing modifiers
|
|
80
|
-
}
|
|
81
|
-
.overlay {
|
|
82
|
-
if let item = navigator.overplay {
|
|
83
|
-
OverplayHost(item: item)
|
|
84
|
-
.environmentObject(navigator)
|
|
85
|
-
.environmentObject(applicationEnvironment)
|
|
86
|
-
.environment(\.applicationEnvironment, applicationEnvironment)
|
|
87
|
-
// AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
|
|
88
|
-
.momoLocalize(localize)
|
|
89
|
-
.momoFontScale(context: applicationEnvironment.applicationContext)
|
|
90
|
-
// AI-GENERATED END: route Localize + font-scale injection through the observing modifiers
|
|
91
|
-
.momoScreenLayout()
|
|
92
|
-
}
|
|
57
|
+
withKitEnvironment(
|
|
58
|
+
stack(path: $navigator.path, root: navigator.rootRoute)
|
|
59
|
+
.overlay { overplayLayer(presentedId: nil) }
|
|
60
|
+
)
|
|
61
|
+
.fullScreenCover(item: Binding(
|
|
62
|
+
get: { navigator.coverRoute },
|
|
63
|
+
// A close that bypasses the navigator (e.g. `dismiss()`) still tears the flow down.
|
|
64
|
+
// SwiftUI also writes nil after a navigator-driven close; by then a new screen may
|
|
65
|
+
// already be queued, so only a cover that is still showing counts.
|
|
66
|
+
set: { if $0 == nil, navigator.coverRoute != nil { navigator.dismiss() } }
|
|
67
|
+
)) { route in
|
|
68
|
+
withKitEnvironment(
|
|
69
|
+
stack(path: $navigator.presentedPath, root: DynamicScreenRoute(id: route.id))
|
|
70
|
+
.overlay { overplayLayer(presentedId: route.id) }
|
|
71
|
+
)
|
|
72
|
+
.onAppear { navigator.coverDidAppear() }
|
|
73
|
+
.onDisappear { navigator.coverDidDisappear() }
|
|
93
74
|
}
|
|
94
75
|
.onAppear {
|
|
95
76
|
if navigator.composeApi == nil {
|
|
@@ -110,10 +91,23 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
110
91
|
// AI-GENERATED END: demote the MaxApi language pull to a seed of last resort
|
|
111
92
|
}
|
|
112
93
|
.onDisappear {
|
|
94
|
+
// Our own cover hides the stack without leaving it; host back must still reach it.
|
|
95
|
+
guard navigator.presented == nil else { return }
|
|
113
96
|
NavigatorManager.shared.remove(navigator)
|
|
114
97
|
}
|
|
115
98
|
}
|
|
116
99
|
|
|
100
|
+
private func stack(path: Binding<[DynamicScreenRoute]>, root: DynamicScreenRoute) -> some View {
|
|
101
|
+
NavigationStack(path: path) {
|
|
102
|
+
screenView(for: root)
|
|
103
|
+
// A reset swaps the root in place; without the id it would inherit the old root's state.
|
|
104
|
+
.id(root.id)
|
|
105
|
+
.navigationDestination(for: DynamicScreenRoute.self) { route in
|
|
106
|
+
screenView(for: route)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
117
111
|
@ViewBuilder
|
|
118
112
|
private func screenView(for route: DynamicScreenRoute) -> some View {
|
|
119
113
|
if let screen = navigator.registry.getScreen(id: route.id) {
|
|
@@ -124,14 +118,22 @@ public struct NavigationContainer<Initial: View>: View {
|
|
|
124
118
|
}
|
|
125
119
|
}
|
|
126
120
|
|
|
121
|
+
/// Each context draws only the overplays shown over it, so one never re-mounts elsewhere.
|
|
127
122
|
@ViewBuilder
|
|
128
|
-
private func
|
|
129
|
-
if let
|
|
130
|
-
|
|
131
|
-
} else {
|
|
132
|
-
EmptyView()
|
|
123
|
+
private func overplayLayer(presentedId: Int?) -> some View {
|
|
124
|
+
if let item = navigator.overplay, item.presentedId == presentedId {
|
|
125
|
+
OverplayHost(item: item)
|
|
133
126
|
}
|
|
134
127
|
}
|
|
128
|
+
|
|
129
|
+
private func withKitEnvironment<Content: View>(_ content: Content) -> some View {
|
|
130
|
+
content
|
|
131
|
+
.environmentObject(navigator)
|
|
132
|
+
.environmentObject(applicationEnvironment)
|
|
133
|
+
.environment(\.applicationEnvironment, applicationEnvironment)
|
|
134
|
+
.momoLocalize(localize)
|
|
135
|
+
.momoFontScale(context: applicationEnvironment.applicationContext)
|
|
136
|
+
}
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
// MARK: - ScreenHost
|
|
@@ -168,8 +170,6 @@ private struct ScreenHost: View {
|
|
|
168
170
|
screenName: screen.name,
|
|
169
171
|
content: { screen.content() }
|
|
170
172
|
)
|
|
171
|
-
// Per screen, not around the NavigationStack: a state-driven reader there stops path pushes rendering.
|
|
172
|
-
.momoScreenLayout()
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// MoMoUIKits
|
|
4
4
|
//
|
|
5
5
|
// SwiftUI mirror of Compose's Navigator + DynamicScreenRegistry.
|
|
6
|
-
// Screens are registered by Int id; the
|
|
7
|
-
// by id and
|
|
6
|
+
// Screens are registered by Int id; the stack paths store routes
|
|
7
|
+
// by id and content is looked up from the registry on render.
|
|
8
8
|
//
|
|
9
9
|
|
|
10
10
|
import SwiftUI
|
|
@@ -83,6 +83,9 @@ public struct OverplayItem: Identifiable {
|
|
|
83
83
|
public var header: BottomSheetHeader? = nil
|
|
84
84
|
// Snack-bar payload (mirrors Compose's SnackBar value).
|
|
85
85
|
public var snackBar: SnackBar? = nil
|
|
86
|
+
/// The presented screen it was shown over, nil for the main stack. Like Compose's overplay
|
|
87
|
+
/// root id, it pins the item to that context instead of following later presents/dismisses.
|
|
88
|
+
public internal(set) var presentedId: Int? = nil
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
// MARK: - Navigator
|
|
@@ -92,11 +95,24 @@ public final class Navigator: ObservableObject {
|
|
|
92
95
|
@Published public var path: [DynamicScreenRoute] = [] {
|
|
93
96
|
// Disposal hangs off the path so every route that leaves the stack — pop,
|
|
94
97
|
// replace, reset — is released the same way, after its exit transition.
|
|
95
|
-
didSet { disposeScreens(droppedFrom: oldValue) }
|
|
98
|
+
didSet { disposeScreens(droppedFrom: oldValue, keeping: path) }
|
|
99
|
+
}
|
|
100
|
+
@Published public var presented: DynamicDialogRoute? {
|
|
101
|
+
didSet { syncCover() }
|
|
102
|
+
}
|
|
103
|
+
/// Screens pushed while a screen is presented. Compose's dialog route is a plain back-stack
|
|
104
|
+
/// entry, so later pushes stack on top of it; the cover hosts its own stack for the same result.
|
|
105
|
+
@Published public var presentedPath: [DynamicScreenRoute] = [] {
|
|
106
|
+
didSet { disposeScreens(droppedFrom: oldValue, keeping: presentedPath) }
|
|
96
107
|
}
|
|
97
|
-
@Published public var presented: DynamicDialogRoute?
|
|
98
108
|
@Published public var overplay: OverplayItem?
|
|
99
109
|
|
|
110
|
+
/// What the cover shows. It trails `presented` while the previous cover is still sliding out:
|
|
111
|
+
/// SwiftUI can drop a presentation issued during another one's dismissal.
|
|
112
|
+
@Published private(set) var coverRoute: DynamicDialogRoute?
|
|
113
|
+
private var coverOnScreen = false
|
|
114
|
+
private var presentationGeneration = 0
|
|
115
|
+
|
|
100
116
|
/// Owned per-Navigator (mirrors Compose's `remember { DynamicScreenRegistry() }` +
|
|
101
117
|
/// disposal): screens registered by this navigator are released via ARC when the
|
|
102
118
|
/// navigator itself deallocates, instead of accumulating in a process-wide singleton.
|
|
@@ -108,7 +124,7 @@ public final class Navigator: ObservableObject {
|
|
|
108
124
|
/// actually removed via `finishOverplayDismiss`.
|
|
109
125
|
private var overplayDismissHandler: (() -> Void)?
|
|
110
126
|
|
|
111
|
-
public private(set) var rootRoute: DynamicScreenRoute
|
|
127
|
+
@Published public private(set) var rootRoute: DynamicScreenRoute
|
|
112
128
|
public var composeApi: KitComposeApi?
|
|
113
129
|
|
|
114
130
|
public init(
|
|
@@ -133,7 +149,7 @@ public final class Navigator: ObservableObject {
|
|
|
133
149
|
content: { AnyView(content()) },
|
|
134
150
|
options: options
|
|
135
151
|
)
|
|
136
|
-
|
|
152
|
+
appendToActiveStack(route)
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
public func push<V: View>(
|
|
@@ -146,12 +162,11 @@ public final class Navigator: ObservableObject {
|
|
|
146
162
|
content: { AnyView(content()) },
|
|
147
163
|
options: options
|
|
148
164
|
)
|
|
149
|
-
|
|
165
|
+
appendToActiveStack(route)
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
public func replace<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
|
|
153
|
-
guard
|
|
154
|
-
path.removeLast()
|
|
169
|
+
guard removeLastFromActiveStack() else { return }
|
|
155
170
|
push(content, options: options)
|
|
156
171
|
}
|
|
157
172
|
|
|
@@ -160,13 +175,37 @@ public final class Navigator: ObservableObject {
|
|
|
160
175
|
@ViewBuilder _ content: @escaping () -> V,
|
|
161
176
|
options: NavigationOptions? = nil
|
|
162
177
|
) {
|
|
163
|
-
guard
|
|
164
|
-
path.removeLast()
|
|
178
|
+
guard removeLastFromActiveStack() else { return }
|
|
165
179
|
push(screenName: screenName, content, options: options)
|
|
166
180
|
}
|
|
167
181
|
|
|
182
|
+
/// The presented flow while one is up, the main stack otherwise.
|
|
183
|
+
private func appendToActiveStack(_ route: DynamicScreenRoute) {
|
|
184
|
+
if presented != nil {
|
|
185
|
+
presentedPath.append(route)
|
|
186
|
+
} else {
|
|
187
|
+
path.append(route)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private func removeLastFromActiveStack() -> Bool {
|
|
192
|
+
if presented != nil {
|
|
193
|
+
// Compose pops the dialog route itself, so the replacement lands on the main stack.
|
|
194
|
+
if presentedPath.isEmpty {
|
|
195
|
+
clearPresented()
|
|
196
|
+
} else {
|
|
197
|
+
presentedPath.removeLast()
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
guard !path.isEmpty else { return false }
|
|
201
|
+
path.removeLast()
|
|
202
|
+
}
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
|
|
168
206
|
public func reset<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
|
|
169
207
|
let previousRoot = rootRoute.id
|
|
208
|
+
clearPresented()
|
|
170
209
|
path.removeAll()
|
|
171
210
|
let route = registry.register(
|
|
172
211
|
screenName: options?.screenName ?? "",
|
|
@@ -183,6 +222,7 @@ public final class Navigator: ObservableObject {
|
|
|
183
222
|
options: NavigationOptions? = nil
|
|
184
223
|
) {
|
|
185
224
|
let previousRoot = rootRoute.id
|
|
225
|
+
clearPresented()
|
|
186
226
|
path.removeAll()
|
|
187
227
|
let route = registry.register(
|
|
188
228
|
screenName: screenName,
|
|
@@ -241,9 +281,11 @@ public final class Navigator: ObservableObject {
|
|
|
241
281
|
@discardableResult
|
|
242
282
|
private func popScreen() -> Bool {
|
|
243
283
|
if presented != nil {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
284
|
+
if presentedPath.isEmpty {
|
|
285
|
+
clearPresented()
|
|
286
|
+
} else {
|
|
287
|
+
presentedPath.removeLast()
|
|
288
|
+
}
|
|
247
289
|
return true
|
|
248
290
|
} else if !path.isEmpty {
|
|
249
291
|
path.removeLast()
|
|
@@ -254,15 +296,20 @@ public final class Navigator: ObservableObject {
|
|
|
254
296
|
}
|
|
255
297
|
}
|
|
256
298
|
|
|
299
|
+
/// Empties both stacks; a presented screen stays up on its own root.
|
|
257
300
|
public func popToRoot() {
|
|
301
|
+
presentedPath.removeAll()
|
|
258
302
|
path.removeAll()
|
|
259
303
|
}
|
|
260
304
|
|
|
261
305
|
/// The entry a back action applies to — mirrors Compose's `currentScreen()`
|
|
262
|
-
/// (`navController.currentBackStackEntry`):
|
|
306
|
+
/// (`navController.currentBackStackEntry`): the presented flow sits on top of
|
|
263
307
|
/// the stack, otherwise it is the last pushed screen, otherwise the root.
|
|
264
308
|
private func currentScreen() -> DynamicScreen? {
|
|
265
|
-
|
|
309
|
+
if let presented {
|
|
310
|
+
return registry.getScreen(id: presentedPath.last?.id ?? presented.id)
|
|
311
|
+
}
|
|
312
|
+
return registry.getScreen(id: path.last?.id ?? rootRoute.id)
|
|
266
313
|
}
|
|
267
314
|
|
|
268
315
|
public func onBackSafe(callback: (() -> Void)? = nil) {
|
|
@@ -318,7 +365,7 @@ public final class Navigator: ObservableObject {
|
|
|
318
365
|
content: { AnyView(content()) },
|
|
319
366
|
options: options
|
|
320
367
|
)
|
|
321
|
-
|
|
368
|
+
presentScreen(route)
|
|
322
369
|
}
|
|
323
370
|
|
|
324
371
|
public func present<V: View>(
|
|
@@ -331,14 +378,54 @@ public final class Navigator: ObservableObject {
|
|
|
331
378
|
content: { AnyView(content()) },
|
|
332
379
|
options: options
|
|
333
380
|
)
|
|
334
|
-
|
|
381
|
+
presentScreen(route)
|
|
335
382
|
}
|
|
336
383
|
|
|
337
384
|
public func dismiss() {
|
|
338
|
-
|
|
339
|
-
|
|
385
|
+
clearPresented()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// One cover per container: presenting from a presented flow stacks onto that flow, the way
|
|
389
|
+
/// Compose stacks a second dialog route.
|
|
390
|
+
private func presentScreen(_ route: DynamicScreenRoute) {
|
|
391
|
+
if presented != nil {
|
|
392
|
+
presentedPath.append(route)
|
|
393
|
+
} else {
|
|
394
|
+
presentationGeneration += 1
|
|
395
|
+
presentedPath = []
|
|
396
|
+
presented = DynamicDialogRoute(id: route.id)
|
|
340
397
|
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/// The cover keeps rendering its stack while it slides out, so the flow is torn down only
|
|
401
|
+
/// once the exit animation is over.
|
|
402
|
+
private func clearPresented() {
|
|
403
|
+
guard let id = presented?.id else { return }
|
|
341
404
|
presented = nil
|
|
405
|
+
let generation = presentationGeneration
|
|
406
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Self.presentedDisposeDelay) { [weak self] in
|
|
407
|
+
guard let self, self.presentationGeneration == generation else { return }
|
|
408
|
+
self.presentedPath.removeAll()
|
|
409
|
+
}
|
|
410
|
+
disposeScreens([id], after: Self.presentedDisposeDelay)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private func syncCover() {
|
|
414
|
+
if presented == nil {
|
|
415
|
+
coverRoute = nil
|
|
416
|
+
} else if !coverOnScreen || coverRoute != nil {
|
|
417
|
+
coverRoute = presented
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/// Reported by the container from the cover's appear / disappear.
|
|
422
|
+
func coverDidAppear() {
|
|
423
|
+
coverOnScreen = true
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
func coverDidDisappear() {
|
|
427
|
+
coverOnScreen = false
|
|
428
|
+
syncCover()
|
|
342
429
|
}
|
|
343
430
|
|
|
344
431
|
// MARK: Overplay
|
|
@@ -353,7 +440,8 @@ public final class Navigator: ObservableObject {
|
|
|
353
440
|
type: .modal,
|
|
354
441
|
content: { AnyView(content()) },
|
|
355
442
|
barrierDismissible: barrierDismissible,
|
|
356
|
-
onDismiss: onDismiss
|
|
443
|
+
onDismiss: onDismiss,
|
|
444
|
+
presentedId: presented?.id
|
|
357
445
|
)
|
|
358
446
|
}
|
|
359
447
|
|
|
@@ -371,7 +459,8 @@ public final class Navigator: ObservableObject {
|
|
|
371
459
|
barrierDismissible: barrierDismissible,
|
|
372
460
|
onDismiss: onDismiss,
|
|
373
461
|
isSurface: isSurface,
|
|
374
|
-
header: header
|
|
462
|
+
header: header,
|
|
463
|
+
presentedId: presented?.id
|
|
375
464
|
)
|
|
376
465
|
}
|
|
377
466
|
|
|
@@ -383,7 +472,8 @@ public final class Navigator: ObservableObject {
|
|
|
383
472
|
content: { AnyView(EmptyView()) },
|
|
384
473
|
barrierDismissible: false,
|
|
385
474
|
onDismiss: onDismiss,
|
|
386
|
-
snackBar: snackBar
|
|
475
|
+
snackBar: snackBar,
|
|
476
|
+
presentedId: presented?.id
|
|
387
477
|
)
|
|
388
478
|
}
|
|
389
479
|
|
|
@@ -429,20 +519,23 @@ public final class Navigator: ObservableObject {
|
|
|
429
519
|
/// after the component has taken itself down.
|
|
430
520
|
private static let overplayDismissDelay: TimeInterval = 0.3
|
|
431
521
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
522
|
+
/// A full-screen cover slides out slower than a stack pop.
|
|
523
|
+
private static let presentedDisposeDelay: TimeInterval = 0.6
|
|
524
|
+
|
|
525
|
+
private func disposeScreens(droppedFrom previous: [DynamicScreenRoute], keeping current: [DynamicScreenRoute]) {
|
|
526
|
+
let kept = Set(current.map(\.id))
|
|
527
|
+
disposeScreens(previous.map(\.id).filter { !kept.contains($0) })
|
|
435
528
|
}
|
|
436
529
|
|
|
437
530
|
/// Screens are released only once the pop transition has played out: the outgoing
|
|
438
531
|
/// view is still on screen while it animates and the registry is what renders it —
|
|
439
532
|
/// dropping it right away blanks the screen mid-transition. Liveness is re-checked
|
|
440
533
|
/// when the timer fires, so a route pushed back before then keeps its screen.
|
|
441
|
-
private func disposeScreens(_ ids: [Int]) {
|
|
534
|
+
private func disposeScreens(_ ids: [Int], after delay: TimeInterval = Navigator.screenDisposeDelay) {
|
|
442
535
|
guard !ids.isEmpty else { return }
|
|
443
|
-
DispatchQueue.main.asyncAfter(deadline: .now() +
|
|
536
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
444
537
|
guard let self else { return }
|
|
445
|
-
var live = Set(self.path.map(\.id))
|
|
538
|
+
var live = Set(self.path.map(\.id)).union(self.presentedPath.map(\.id))
|
|
446
539
|
live.insert(self.rootRoute.id)
|
|
447
540
|
if let presented = self.presented { live.insert(presented.id) }
|
|
448
541
|
for id in ids where !live.contains(id) {
|
|
@@ -5,18 +5,25 @@ public final class NavigatorManager {
|
|
|
5
5
|
public static let shared = NavigatorManager()
|
|
6
6
|
private init() {}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Weak: a container torn down while its cover is up never reports its disappearance.
|
|
9
|
+
private var navigators: [WeakNavigator] = []
|
|
9
10
|
|
|
10
11
|
public func push(_ navigator: Navigator) {
|
|
11
|
-
|
|
12
|
-
navigators.
|
|
12
|
+
navigators.removeAll { $0.value == nil }
|
|
13
|
+
guard !navigators.contains(where: { $0.value === navigator }) else { return }
|
|
14
|
+
navigators.append(WeakNavigator(value: navigator))
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
public func remove(_ navigator: Navigator) {
|
|
16
|
-
navigators.removeAll { $0 === navigator }
|
|
18
|
+
navigators.removeAll { $0.value == nil || $0.value === navigator }
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
public func checkCanPop() -> Bool {
|
|
21
|
+
public func checkCanPop() -> Bool { navigators.contains { $0.value != nil } }
|
|
20
22
|
|
|
21
|
-
public func pop() { navigators.last?.onBackSafe() }
|
|
23
|
+
public func pop() { navigators.last { $0.value != nil }?.value?.onBackSafe() }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@available(iOS 16.0, *)
|
|
27
|
+
private struct WeakNavigator {
|
|
28
|
+
weak var value: Navigator?
|
|
22
29
|
}
|
|
@@ -45,7 +45,6 @@ struct BottomSheetView<Content: View>: View {
|
|
|
45
45
|
@EnvironmentObject private var navigator: Navigator
|
|
46
46
|
@Environment(\.appTheme) private var theme
|
|
47
47
|
@Environment(\.applicationEnvironment) private var appEnv
|
|
48
|
-
@Environment(\.screenLayout) private var layout
|
|
49
48
|
|
|
50
49
|
/// Vertical translation of the sheet; starts off-screen and animates to 0.
|
|
51
50
|
@State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
|
|
@@ -67,36 +66,7 @@ struct BottomSheetView<Content: View>: View {
|
|
|
67
66
|
/// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
|
|
68
67
|
private let maxDynamicAlpha: CGFloat = 0.3
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
private var screenHeight: CGFloat { isDuo ? layout.height : UIScreen.main.bounds.height }
|
|
72
|
-
|
|
73
|
-
private var isDuo: Bool { layout.isDuoDevice }
|
|
74
|
-
// iPhone Duo inner display: center the sheet instead of spanning the width
|
|
75
|
-
private var isDuoCentered: Bool { isDuo && layout.width >= 600 }
|
|
76
|
-
|
|
77
|
-
/// Keeps the portrait width in landscape, like system sheets.
|
|
78
|
-
private var duoCenteredWidth: CGFloat {
|
|
79
|
-
min(
|
|
80
|
-
min(layout.width, layout.height) - Spacing.L * 2,
|
|
81
|
-
layout.width - max(layout.insets.leading, layout.insets.trailing, Spacing.L) * 2
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/// Landscape side buttons span the full height, so the content is inset; in portrait the
|
|
86
|
-
/// side status bar only covers the top corner.
|
|
87
|
-
private var sideInsetLeading: CGFloat {
|
|
88
|
-
isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.leading : 0
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private var sideInsetTrailing: CGFloat {
|
|
92
|
-
isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.trailing : 0
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/// The keyboard filler is part of the sheet, so capping the sheet shrinks the content by it.
|
|
96
|
-
private var duoMaxHeight: CGFloat? {
|
|
97
|
-
guard isDuo else { return nil }
|
|
98
|
-
return layout.height - max(layout.duoTopInset, Spacing.XL)
|
|
99
|
-
}
|
|
69
|
+
private var screenHeight: CGFloat { UIScreen.main.bounds.height }
|
|
100
70
|
private var bottomInset: CGFloat { safeAreaBottomInset() }
|
|
101
71
|
/// While the keyboard is up, it already covers the home-indicator area, so it
|
|
102
72
|
/// replaces (rather than adds to) the usual bottom-inset filler.
|
|
@@ -140,7 +110,7 @@ struct BottomSheetView<Content: View>: View {
|
|
|
140
110
|
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
|
|
141
111
|
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
142
112
|
withAnimation(.easeOut(duration: 0.25)) {
|
|
143
|
-
keyboardHeight =
|
|
113
|
+
keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
|
|
144
114
|
}
|
|
145
115
|
}
|
|
146
116
|
.onAppear {
|
|
@@ -168,8 +138,6 @@ struct BottomSheetView<Content: View>: View {
|
|
|
168
138
|
headerView
|
|
169
139
|
.frame(height: 56)
|
|
170
140
|
.padding(.horizontal, Spacing.M)
|
|
171
|
-
.padding(.leading, sideInsetLeading)
|
|
172
|
-
.padding(.trailing, sideInsetTrailing)
|
|
173
141
|
}
|
|
174
142
|
.frame(maxWidth: .infinity)
|
|
175
143
|
.frame(height: 72)
|
|
@@ -181,17 +149,12 @@ struct BottomSheetView<Content: View>: View {
|
|
|
181
149
|
.frame(height: 1)
|
|
182
150
|
|
|
183
151
|
content()
|
|
184
|
-
.padding(.leading, sideInsetLeading)
|
|
185
|
-
.padding(.trailing, sideInsetTrailing)
|
|
186
|
-
// a tall sheet on iPhone Duo keeps its header and gives the content what is left
|
|
187
|
-
.layoutPriority(-1)
|
|
188
152
|
|
|
189
153
|
backgroundColor
|
|
190
154
|
.frame(height: effectiveBottomInset)
|
|
191
155
|
}
|
|
192
|
-
.frame(maxWidth:
|
|
193
|
-
.frame(
|
|
194
|
-
.clipped()
|
|
156
|
+
.frame(maxWidth: .infinity)
|
|
157
|
+
.frame(alignment: .bottom)
|
|
195
158
|
.background(
|
|
196
159
|
GeometryReader { geo in
|
|
197
160
|
Color.clear.onAppear { sheetHeight = geo.size.height }
|
|
@@ -16,7 +16,6 @@ struct ModalScreen<Content: View>: View {
|
|
|
16
16
|
|
|
17
17
|
@EnvironmentObject private var navigator: Navigator
|
|
18
18
|
@Environment(\.applicationEnvironment) private var appEnv
|
|
19
|
-
@Environment(\.screenLayout) private var layout
|
|
20
19
|
|
|
21
20
|
@State private var scrimOpacity: CGFloat = 0
|
|
22
21
|
@State private var contentScale: CGFloat = 0.8
|
|
@@ -36,11 +35,8 @@ struct ModalScreen<Content: View>: View {
|
|
|
36
35
|
content()
|
|
37
36
|
.scaleEffect(contentScale)
|
|
38
37
|
.opacity(contentOpacity)
|
|
39
|
-
// iPhone Duo: center below the side status bar and above the home indicator
|
|
40
|
-
.padding(.top, layout.isDuoDevice ? layout.duoTopInset : 0)
|
|
41
|
-
.padding(.bottom, layout.isDuoDevice ? layout.insets.bottom : 0)
|
|
42
38
|
}
|
|
43
|
-
//
|
|
39
|
+
// Center on the full screen like Compose; landscape safe areas are asymmetric (camera on one side).
|
|
44
40
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
45
41
|
.ignoresSafeArea(.container)
|
|
46
42
|
.onAppear {
|
|
@@ -60,11 +60,7 @@ public struct NavigationHeader: View {
|
|
|
60
60
|
self.titleOpacity = titleOpacity
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
@Environment(\.screenLayout) private var layout
|
|
64
|
-
|
|
65
63
|
private var isAnimated: Bool { animatedHeader != nil }
|
|
66
|
-
/// iPhone Duo side bar holds back and the toolkit; a custom header right stays in the bar.
|
|
67
|
-
private var useSideBar: Bool { layout.sideBar != nil }
|
|
68
64
|
private var isSurface: Bool { animatedHeader?.isSurface ?? true }
|
|
69
65
|
|
|
70
66
|
private var headerColor: HeaderColor {
|
|
@@ -83,7 +79,7 @@ public struct NavigationHeader: View {
|
|
|
83
79
|
if headerType != .none {
|
|
84
80
|
ZStack {
|
|
85
81
|
HStack(spacing: 0) {
|
|
86
|
-
if let goBack
|
|
82
|
+
if let goBack {
|
|
87
83
|
BackButton(color: headerColor, onBack: goBack)
|
|
88
84
|
Spacer().frame(width: Spacing.S)
|
|
89
85
|
}
|
|
@@ -95,7 +91,7 @@ public struct NavigationHeader: View {
|
|
|
95
91
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
if let headerRight
|
|
94
|
+
if let headerRight {
|
|
99
95
|
AnyView(headerRight())
|
|
100
96
|
.background(
|
|
101
97
|
GeometryReader { geo in
|
|
@@ -111,10 +107,8 @@ public struct NavigationHeader: View {
|
|
|
111
107
|
.padding(.horizontal, Spacing.M)
|
|
112
108
|
}
|
|
113
109
|
}
|
|
114
|
-
.frame(height:
|
|
110
|
+
.frame(height: HEADER_HEIGHT)
|
|
115
111
|
.overlay(alignment: .bottom) { VerticalShadow(opacity: opacity) }
|
|
116
|
-
// status bar off the top edge: the row spans the whole header from the window top
|
|
117
|
-
.padding(.top, min(layout.headerStatusBarHeight - layout.insets.top, 0))
|
|
118
112
|
}
|
|
119
113
|
}
|
|
120
114
|
|
|
@@ -125,9 +119,7 @@ public struct NavigationHeader: View {
|
|
|
125
119
|
if let inputSearchProps {
|
|
126
120
|
InputSearch(props: inputSearchProps)
|
|
127
121
|
} else if case let .user(userData)? = headerTitle {
|
|
128
|
-
|
|
129
|
-
HeaderUserView(userData)
|
|
130
|
-
}
|
|
122
|
+
HeaderUserView(userData)
|
|
131
123
|
} else {
|
|
132
124
|
MomoText(
|
|
133
125
|
headerTitle?.defaultTitle ?? title,
|
|
@@ -43,11 +43,10 @@ public struct NavigationHeaderBackground: View {
|
|
|
43
43
|
)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
@Environment(\.screenLayout) private var layout
|
|
47
|
-
|
|
48
46
|
public var body: some View {
|
|
47
|
+
let statusBarHeight = safeAreaTopInset()
|
|
49
48
|
let opacity = max(0, 1 - scrollState / CGFloat(HEADER_HEIGHT))
|
|
50
|
-
let minHeight = fullScreenContent ? 0 :
|
|
49
|
+
let minHeight = fullScreenContent ? 0 : statusBarHeight + HEADER_HEIGHT
|
|
51
50
|
let maxHeight: CGFloat = 154
|
|
52
51
|
let backgroundColor = headerTransparent ? Color.clear : Colors.black01
|
|
53
52
|
let topColor = opacity == 0 ? backgroundColor : Color(hex: 0xFDCADE)
|
|
@@ -69,7 +69,7 @@ public struct NavigationFooter: View {
|
|
|
69
69
|
.padding(.bottom, Self.bottomPadding)
|
|
70
70
|
// Compose paints the surface all the way to the bottom of the screen; the
|
|
71
71
|
// footer's own frame stops at the safe area, so only the fill bleeds down.
|
|
72
|
-
.background(theme.colors.background.surface.ignoresSafeArea(.container, edges:
|
|
72
|
+
.background(theme.colors.background.surface.ignoresSafeArea(.container, edges: .bottom))
|
|
73
73
|
.overlay(alignment: .top) { shadow }
|
|
74
74
|
.offset(y: Self.verticalOffset(keyboardSize: keyboardSize))
|
|
75
75
|
}
|