@momo-kits/native-kits 0.300.3-debug → 0.300.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 +51 -7
- package/README.md +2 -2
- package/compose/build.gradle.kts +10 -2
- package/compose/build.gradle.kts.backup +8 -0
- package/compose/compose.podspec +1 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
- package/gradle/libs.versions.toml +3 -14
- package/gradle/wrapper/gradle-wrapper.properties +0 -2
- package/gradle.properties +3 -5
- package/ios/Application/ApplicationEnvironment.swift +24 -6
- package/ios/Application/DesignSystemKits.swift +100 -0
- package/ios/Application/DesignSystemKitsPayload.swift +68 -0
- package/ios/Application/MaxApi.swift +30 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +8 -2
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +171 -148
- package/ios/Input/InputPhoneNumber.swift +10 -9
- package/ios/Input/InputSearch.swift +107 -78
- package/ios/Input/InputTextArea.swift +133 -148
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +32 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +415 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
- package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +154 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +425 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +195 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
- package/ios/Navigation/StackScreen.swift +930 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -135
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +138 -0
- package/ios/Popup/PopupNotify.swift +59 -69
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +7 -3
- 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 +109 -55
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/FontScale.swift +17 -14
- package/ios/Typography/Text.swift +156 -20
- package/ios/Typography/Typography.swift +2 -2
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -3
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/package.json +1 -1
- package/publish.sh +19 -2
- package/scripts/gen-ios-icons.mjs +36 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/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/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- package/ios/Typography/FontScaleStore.swift +0 -58
- package/ios/Typography/KitDesignSystem.swift +0 -17
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
//
|
|
2
|
+
// StackTransition.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/StackTransition.kt` (a 1:1 port of the RN kit's
|
|
6
|
+
// `forStackTransitionWithOverlay`) and of the NavHost it decorates. Every stack entry lives in
|
|
7
|
+
// its own UIHostingController, kept while the entry is on the stack and taken out of the window
|
|
8
|
+
// while covered, as UINavigationController does: a covered screen keeps its state, stops
|
|
9
|
+
// rendering and gets onDisappear / onAppear. The motion itself is drawn in SwiftUI, because an
|
|
10
|
+
// `offset` never moves the safe area the way a UIKit transform would.
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
import SwiftUI
|
|
14
|
+
import UIKit
|
|
15
|
+
|
|
16
|
+
let STACK_TRANSITION_DURATION: TimeInterval = 0.3
|
|
17
|
+
|
|
18
|
+
private let PARALLAX_RATIO: CGFloat = 0.3
|
|
19
|
+
private let SCRIM_MAX_ALPHA: CGFloat = 0.6
|
|
20
|
+
private let EDGE_SHADOW_MAX_ALPHA: CGFloat = 0.3
|
|
21
|
+
private let EDGE_SHADOW_WIDTH: CGFloat = 8
|
|
22
|
+
|
|
23
|
+
// MARK: - Host
|
|
24
|
+
|
|
25
|
+
/// The stack view behind `NavigationContainer`. `screen` builds an entry's content once;
|
|
26
|
+
/// `decorate` re-applies the kit environment on every update.
|
|
27
|
+
struct StackHost: UIViewControllerRepresentable {
|
|
28
|
+
let navigator: Navigator
|
|
29
|
+
let entries: [StackEntry]
|
|
30
|
+
let screen: (StackEntry) -> AnyView
|
|
31
|
+
let decorate: (AnyView) -> AnyView
|
|
32
|
+
|
|
33
|
+
func makeUIViewController(context: Context) -> StackController {
|
|
34
|
+
let controller = StackController()
|
|
35
|
+
navigator.stackController = controller
|
|
36
|
+
return controller
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func updateUIViewController(_ controller: StackController, context: Context) {
|
|
40
|
+
controller.update(
|
|
41
|
+
entries: entries,
|
|
42
|
+
environment: context.environment,
|
|
43
|
+
screen: screen,
|
|
44
|
+
decorate: decorate
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: - Controller
|
|
50
|
+
|
|
51
|
+
final class StackController: UIViewController {
|
|
52
|
+
private final class Card {
|
|
53
|
+
let entry: StackEntry
|
|
54
|
+
let motion = CardMotion()
|
|
55
|
+
var host: UIHostingController<AnyView>!
|
|
56
|
+
|
|
57
|
+
init(entry: StackEntry) {
|
|
58
|
+
self.entry = entry
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// One push or pop between two cards. `coverage` is how far `top` covers `under`: 1 in place,
|
|
63
|
+
/// 0 gone. Push plays 0 → 1, pop 1 → 0; a host gesture sets it straight from the finger.
|
|
64
|
+
private final class Transition {
|
|
65
|
+
enum Kind { case slide, rise }
|
|
66
|
+
|
|
67
|
+
let kind: Kind
|
|
68
|
+
let top: Card
|
|
69
|
+
let under: Card
|
|
70
|
+
let isSeek: Bool
|
|
71
|
+
var coverage: CGFloat
|
|
72
|
+
var from: CGFloat = 0
|
|
73
|
+
var target: CGFloat = 0
|
|
74
|
+
var duration: TimeInterval = 0
|
|
75
|
+
var startTime: CFTimeInterval?
|
|
76
|
+
var isAnimating = false
|
|
77
|
+
|
|
78
|
+
init(kind: Kind, top: Card, under: Card, isSeek: Bool = false, coverage: CGFloat) {
|
|
79
|
+
self.kind = kind
|
|
80
|
+
self.top = top
|
|
81
|
+
self.under = under
|
|
82
|
+
self.isSeek = isSeek
|
|
83
|
+
self.coverage = coverage
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private var entries: [StackEntry] = []
|
|
88
|
+
private var cards: [Int: Card] = [:]
|
|
89
|
+
private var transition: Transition?
|
|
90
|
+
private var displayLink: CADisplayLink?
|
|
91
|
+
private var environment = EnvironmentValues()
|
|
92
|
+
private var screen: (StackEntry) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
93
|
+
private var decorate: (AnyView) -> AnyView = { $0 }
|
|
94
|
+
|
|
95
|
+
override func viewDidLoad() {
|
|
96
|
+
super.viewDidLoad()
|
|
97
|
+
view.backgroundColor = .clear
|
|
98
|
+
view.clipsToBounds = true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override func viewDidLayoutSubviews() {
|
|
102
|
+
super.viewDidLayoutSubviews()
|
|
103
|
+
for subview in view.subviews { subview.frame = view.bounds }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
override func viewDidDisappear(_ animated: Bool) {
|
|
107
|
+
super.viewDidDisappear(animated)
|
|
108
|
+
settle()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func update(
|
|
112
|
+
entries new: [StackEntry],
|
|
113
|
+
environment: EnvironmentValues,
|
|
114
|
+
screen: @escaping (StackEntry) -> AnyView,
|
|
115
|
+
decorate: @escaping (AnyView) -> AnyView
|
|
116
|
+
) {
|
|
117
|
+
self.environment = environment
|
|
118
|
+
self.screen = screen
|
|
119
|
+
self.decorate = decorate
|
|
120
|
+
for card in cards.values { card.host.rootView = rootView(for: card) }
|
|
121
|
+
guard new != entries else { return }
|
|
122
|
+
|
|
123
|
+
settle()
|
|
124
|
+
let old = entries
|
|
125
|
+
let oldTop = old.last.flatMap { cards[$0.id] }
|
|
126
|
+
entries = new
|
|
127
|
+
|
|
128
|
+
let live = Set(new.map(\.id))
|
|
129
|
+
for (id, card) in cards where !live.contains(id) {
|
|
130
|
+
cards[id] = nil
|
|
131
|
+
// the old top may still have to animate out
|
|
132
|
+
if card !== oldTop { tearDown(card) }
|
|
133
|
+
}
|
|
134
|
+
for entry in new where cards[entry.id] == nil {
|
|
135
|
+
cards[entry.id] = makeCard(entry)
|
|
136
|
+
}
|
|
137
|
+
guard let newTop = new.last.flatMap({ cards[$0.id] }) else { return }
|
|
138
|
+
guard let oldTop, oldTop !== newTop else {
|
|
139
|
+
if oldTop == nil { attach(newTop) }
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let isPush = !old.contains(newTop.entry)
|
|
144
|
+
guard view.window != nil, isPush || !live.contains(oldTop.entry.id) else {
|
|
145
|
+
attach(newTop)
|
|
146
|
+
leave(oldTop)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if isPush {
|
|
150
|
+
let kind: Transition.Kind = newTop.entry.isDialog ? .rise : .slide
|
|
151
|
+
play(Transition(kind: kind, top: newTop, under: oldTop, coverage: 0), to: 1)
|
|
152
|
+
} else {
|
|
153
|
+
// closing a presented flow slides it down whole, whatever was pushed inside it
|
|
154
|
+
let closesPresented = old.drop(while: { $0 != newTop.entry }).dropFirst().contains(where: \.isDialog)
|
|
155
|
+
play(Transition(kind: closesPresented ? .rise : .slide, top: oldTop, under: newTop, coverage: 1), to: 0)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// MARK: Host back gesture
|
|
160
|
+
|
|
161
|
+
func beginBackSeek() -> Bool {
|
|
162
|
+
settle()
|
|
163
|
+
guard view.window != nil, entries.count > 1,
|
|
164
|
+
let top = cards[entries[entries.count - 1].id], !top.entry.isDialog,
|
|
165
|
+
let under = cards[entries[entries.count - 2].id] else { return false }
|
|
166
|
+
let transition = Transition(kind: .slide, top: top, under: under, isSeek: true, coverage: 1)
|
|
167
|
+
self.transition = transition
|
|
168
|
+
view.isUserInteractionEnabled = false
|
|
169
|
+
attach(under, below: top)
|
|
170
|
+
apply(transition)
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func seekBack(to progress: CGFloat) {
|
|
175
|
+
guard let transition, transition.isSeek, !transition.isAnimating else { return }
|
|
176
|
+
transition.coverage = 1 - progress
|
|
177
|
+
apply(transition)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Commits the pop here; the navigator drops the same entry right after.
|
|
181
|
+
func finishBackSeek() -> Bool {
|
|
182
|
+
guard let transition, transition.isSeek, !transition.isAnimating else { return false }
|
|
183
|
+
entries.removeLast()
|
|
184
|
+
cards[transition.top.entry.id] = nil
|
|
185
|
+
animate(transition, to: 0)
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func cancelBackSeek() {
|
|
190
|
+
guard let transition, transition.isSeek, !transition.isAnimating else { return }
|
|
191
|
+
animate(transition, to: 1)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// MARK: Transitions
|
|
195
|
+
|
|
196
|
+
private func play(_ transition: Transition, to target: CGFloat) {
|
|
197
|
+
self.transition = transition
|
|
198
|
+
view.isUserInteractionEnabled = false
|
|
199
|
+
if transition.top.host.view.superview !== view { attach(transition.top) }
|
|
200
|
+
attach(transition.under, below: transition.top)
|
|
201
|
+
apply(transition)
|
|
202
|
+
animate(transition, to: target)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// The rest of the way plays at full-transition pace, so a released drag keeps its speed.
|
|
206
|
+
private func animate(_ transition: Transition, to target: CGFloat) {
|
|
207
|
+
transition.from = transition.coverage
|
|
208
|
+
transition.target = target
|
|
209
|
+
transition.duration = STACK_TRANSITION_DURATION * Double(abs(target - transition.coverage))
|
|
210
|
+
transition.startTime = nil
|
|
211
|
+
transition.isAnimating = true
|
|
212
|
+
guard transition.duration > 0 else {
|
|
213
|
+
finish(transition)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
startDisplayLink()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@objc private func tick(_ link: CADisplayLink) {
|
|
220
|
+
guard let transition, transition.isAnimating else {
|
|
221
|
+
stopDisplayLink()
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
// the clock starts on the first frame, so a heavy first layout doesn't eat the motion
|
|
225
|
+
let start = transition.startTime ?? link.targetTimestamp
|
|
226
|
+
transition.startTime = start
|
|
227
|
+
let fraction = min(max((link.targetTimestamp - start) / transition.duration, 0), 1)
|
|
228
|
+
// linear for the stack: the gesture maps the finger straight onto this timeline
|
|
229
|
+
let eased = transition.kind == .rise ? fastOutSlowIn(CGFloat(fraction)) : CGFloat(fraction)
|
|
230
|
+
transition.coverage = fraction >= 1
|
|
231
|
+
? transition.target
|
|
232
|
+
: transition.from + (transition.target - transition.from) * eased
|
|
233
|
+
apply(transition)
|
|
234
|
+
if fraction >= 1 { finish(transition) }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Jumps a running transition to its end; an unreleased drag snaps back.
|
|
238
|
+
private func settle() {
|
|
239
|
+
guard let transition else { return }
|
|
240
|
+
if transition.isSeek, !transition.isAnimating { transition.target = 1 }
|
|
241
|
+
transition.coverage = transition.target
|
|
242
|
+
apply(transition)
|
|
243
|
+
finish(transition)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private func finish(_ transition: Transition) {
|
|
247
|
+
transition.isAnimating = false
|
|
248
|
+
if self.transition === transition { self.transition = nil }
|
|
249
|
+
stopDisplayLink()
|
|
250
|
+
view.isUserInteractionEnabled = true
|
|
251
|
+
transition.top.motion.state = CardMotion.State()
|
|
252
|
+
transition.under.motion.state = CardMotion.State()
|
|
253
|
+
leave(transition.target >= 1 ? transition.under : transition.top)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private func apply(_ transition: Transition) {
|
|
257
|
+
let size = view.bounds.size
|
|
258
|
+
let coverage = transition.coverage
|
|
259
|
+
switch transition.kind {
|
|
260
|
+
case .slide:
|
|
261
|
+
transition.top.motion.state = CardMotion.State(
|
|
262
|
+
offset: CGSize(width: (1 - coverage) * size.width, height: 0),
|
|
263
|
+
shadow: EDGE_SHADOW_MAX_ALPHA * coverage
|
|
264
|
+
)
|
|
265
|
+
transition.under.motion.state = CardMotion.State(
|
|
266
|
+
offset: CGSize(width: -PARALLAX_RATIO * size.width * coverage, height: 0),
|
|
267
|
+
scrim: SCRIM_MAX_ALPHA * coverage
|
|
268
|
+
)
|
|
269
|
+
case .rise:
|
|
270
|
+
transition.top.motion.state = CardMotion.State(offset: CGSize(width: 0, height: (1 - coverage) * size.height))
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private func startDisplayLink() {
|
|
275
|
+
guard displayLink == nil else { return }
|
|
276
|
+
let link = CADisplayLink(target: self, selector: #selector(tick))
|
|
277
|
+
link.preferredFrameRateRange = CAFrameRateRange(minimum: 60, maximum: 120, preferred: 120)
|
|
278
|
+
link.add(to: .main, forMode: .common)
|
|
279
|
+
displayLink = link
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private func stopDisplayLink() {
|
|
283
|
+
displayLink?.invalidate()
|
|
284
|
+
displayLink = nil
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// MARK: Cards
|
|
288
|
+
|
|
289
|
+
private func makeCard(_ entry: StackEntry) -> Card {
|
|
290
|
+
let card = Card(entry: entry)
|
|
291
|
+
let host = UIHostingController(rootView: rootView(for: card))
|
|
292
|
+
host.view.backgroundColor = .clear
|
|
293
|
+
addChild(host)
|
|
294
|
+
host.didMove(toParent: self)
|
|
295
|
+
card.host = host
|
|
296
|
+
return card
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// The card's own environment is the container's (host environment objects included), with
|
|
300
|
+
/// the kit's re-applied on top; the screen itself is built once and never re-run from here.
|
|
301
|
+
private func rootView(for card: Card) -> AnyView {
|
|
302
|
+
let entry = card.entry
|
|
303
|
+
let screen = screen
|
|
304
|
+
let content = StackCard(motion: card.motion) {
|
|
305
|
+
StackCardContent(id: entry.id) { screen(entry) }.equatable()
|
|
306
|
+
}
|
|
307
|
+
return AnyView(decorate(AnyView(content)).environment(\.self, environment))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private func attach(_ card: Card, below other: Card? = nil) {
|
|
311
|
+
let hostView: UIView = card.host.view
|
|
312
|
+
hostView.frame = view.bounds
|
|
313
|
+
if let other, other.host.view.superview === view {
|
|
314
|
+
view.insertSubview(hostView, belowSubview: other.host.view)
|
|
315
|
+
} else {
|
|
316
|
+
view.addSubview(hostView)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// Off the window while covered, released once off the stack.
|
|
321
|
+
private func leave(_ card: Card) {
|
|
322
|
+
if cards[card.entry.id] === card {
|
|
323
|
+
card.host.view.removeFromSuperview()
|
|
324
|
+
} else {
|
|
325
|
+
tearDown(card)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private func tearDown(_ card: Card) {
|
|
330
|
+
card.host.willMove(toParent: nil)
|
|
331
|
+
card.host.view.removeFromSuperview()
|
|
332
|
+
card.host.removeFromParent()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// MARK: - Card
|
|
337
|
+
|
|
338
|
+
final class CardMotion: ObservableObject {
|
|
339
|
+
struct State: Equatable {
|
|
340
|
+
var offset: CGSize = .zero
|
|
341
|
+
var scrim: CGFloat = 0
|
|
342
|
+
var shadow: CGFloat = 0
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@Published var state = State()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/// Compose's `StackTransitionDecor`: the covered-screen scrim and the top card's left-edge shadow.
|
|
349
|
+
private struct StackCard<Content: View>: View {
|
|
350
|
+
@ObservedObject var motion: CardMotion
|
|
351
|
+
let content: Content
|
|
352
|
+
|
|
353
|
+
init(motion: CardMotion, @ViewBuilder content: () -> Content) {
|
|
354
|
+
self.motion = motion
|
|
355
|
+
self.content = content()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
var body: some View {
|
|
359
|
+
let state = motion.state
|
|
360
|
+
content
|
|
361
|
+
.overlay {
|
|
362
|
+
Colors.pink09.opacity(state.scrim)
|
|
363
|
+
.ignoresSafeArea()
|
|
364
|
+
.allowsHitTesting(false)
|
|
365
|
+
}
|
|
366
|
+
.background(alignment: .leading) {
|
|
367
|
+
LinearGradient(colors: [.clear, Colors.black20], startPoint: .leading, endPoint: .trailing)
|
|
368
|
+
.frame(width: EDGE_SHADOW_WIDTH)
|
|
369
|
+
.offset(x: -EDGE_SHADOW_WIDTH)
|
|
370
|
+
.opacity(state.shadow)
|
|
371
|
+
.ignoresSafeArea()
|
|
372
|
+
.allowsHitTesting(false)
|
|
373
|
+
}
|
|
374
|
+
.offset(state.offset)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/// Equal by entry, so a motion frame or a root refresh never re-runs the screen body.
|
|
379
|
+
private struct StackCardContent: View, Equatable {
|
|
380
|
+
let id: Int
|
|
381
|
+
let build: () -> AnyView
|
|
382
|
+
|
|
383
|
+
static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id }
|
|
384
|
+
|
|
385
|
+
var body: some View { build() }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Compose's `FastOutSlowInEasing`, cubic-bezier(0.4, 0, 0.2, 1).
|
|
389
|
+
private func fastOutSlowIn(_ t: CGFloat) -> CGFloat {
|
|
390
|
+
func bezier(_ s: CGFloat, _ p1: CGFloat, _ p2: CGFloat) -> CGFloat {
|
|
391
|
+
3 * (1 - s) * (1 - s) * s * p1 + 3 * (1 - s) * s * s * p2 + s * s * s
|
|
392
|
+
}
|
|
393
|
+
var low: CGFloat = 0
|
|
394
|
+
var high: CGFloat = 1
|
|
395
|
+
for _ in 0..<20 {
|
|
396
|
+
let mid = (low + high) / 2
|
|
397
|
+
if bezier(mid, 0.4, 0.2) < t { low = mid } else { high = mid }
|
|
398
|
+
}
|
|
399
|
+
return bezier((low + high) / 2, 0, 1)
|
|
400
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// SwipeBackHaptic.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/SwipeBackHaptic.kt`: one light tick when the host back
|
|
6
|
+
// gesture has dragged the screen 50pt, re-armed once it goes back under.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import UIKit
|
|
10
|
+
|
|
11
|
+
struct SwipeBackHaptic {
|
|
12
|
+
// Unscaled, so the tick lands at the same physical distance on every device.
|
|
13
|
+
private static let threshold: CGFloat = 50
|
|
14
|
+
|
|
15
|
+
private var fired = false
|
|
16
|
+
|
|
17
|
+
mutating func update(travelled: CGFloat, maxApi: MaxApi?) {
|
|
18
|
+
if travelled >= Self.threshold, !fired {
|
|
19
|
+
fired = true
|
|
20
|
+
if let maxApi {
|
|
21
|
+
maxApi.triggerEventVibration("light")
|
|
22
|
+
} else {
|
|
23
|
+
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
|
24
|
+
}
|
|
25
|
+
} else if travelled < Self.threshold {
|
|
26
|
+
fired = false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
mutating func reset() {
|
|
31
|
+
fired = false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -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
|
|
@@ -65,21 +65,18 @@ public struct OTPKeyboard: View {
|
|
|
65
65
|
HStack {
|
|
66
66
|
KeyboardButton("", action: self.onPressKey).disabled(true).opacity(0)
|
|
67
67
|
KeyboardButton("0", action: self.onPressKey)
|
|
68
|
+
// Frame inside the label so the whole key is tappable, not just the glyph.
|
|
68
69
|
SwiftUI.Button(action: onPressBack) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}.frame(maxWidth: .infinity, minHeight: 48)
|
|
75
|
-
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
70
|
+
Image(systemName: "delete.backward")
|
|
71
|
+
.foregroundColor(Colors.text)
|
|
72
|
+
.frame(maxWidth: .infinity, minHeight: 48)
|
|
73
|
+
.contentShape(Rectangle())
|
|
74
|
+
}
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
77
|
.background(Colors.background)
|
|
79
78
|
.valueChanged(value: value) { otp in
|
|
80
|
-
|
|
81
|
-
self.onChange!(otp)
|
|
82
|
-
}
|
|
79
|
+
onChange?(otp)
|
|
83
80
|
}
|
|
84
81
|
}
|
|
85
82
|
|
|
@@ -128,7 +125,7 @@ private struct ValueView: View {
|
|
|
128
125
|
|
|
129
126
|
public var body: some View {
|
|
130
127
|
return HStack {
|
|
131
|
-
ForEach(0 ..< valueLength, id: \.self) { i in
|
|
128
|
+
ForEach(0 ..< max(valueLength, 0), id: \.self) { i in
|
|
132
129
|
if i < value.count {
|
|
133
130
|
Circle().foregroundColor(Colors.text).frame(width: 8, height: 8)
|
|
134
131
|
} else {
|
|
@@ -14,10 +14,10 @@ import SwiftUI
|
|
|
14
14
|
private let INDICATOR_WIDTH: CGFloat = 24
|
|
15
15
|
private let PROGRESS_WIDTH: CGFloat = 72
|
|
16
16
|
|
|
17
|
-
///
|
|
17
|
+
/// Mirrors Compose's `calculateOffset`; clamped because UIKit bounce overshoots the scroll range.
|
|
18
18
|
private func calculateOffset(currentPosition: CGFloat, maxBound: CGFloat) -> CGFloat {
|
|
19
19
|
guard maxBound > 0 else { return 0 }
|
|
20
|
-
return (PROGRESS_WIDTH - INDICATOR_WIDTH) * (currentPosition / maxBound)
|
|
20
|
+
return (PROGRESS_WIDTH - INDICATOR_WIDTH) * min(max(currentPosition / maxBound, 0), 1)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// MARK: - PaginationScroll
|