@momo-kits/native-kits 0.167.1-duo.18 → 0.167.1-duo.19-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 +5 -3
- package/build.gradle.kts +11 -0
- package/compose/build.gradle.kts +204 -0
- package/compose/build.gradle.kts.backup +204 -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 +153 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -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 +916 -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 +81 -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 +357 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +115 -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 +86 -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 +478 -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 +265 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +235 -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 +265 -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 +361 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +308 -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 +330 -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 +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +92 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +560 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -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 +501 -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 +236 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +180 -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 +20 -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 +194 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +283 -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 +309 -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 +338 -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 +137 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +116 -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 +727 -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 +218 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +231 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -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 +447 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +103 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -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 +379 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +39 -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 +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +133 -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 +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -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 +78 -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 +217 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -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/MaxApi.swift +12 -3
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/Layout/GridContext.swift +67 -3
- package/ios/Navigation/BottomSheet.swift +30 -4
- package/ios/Navigation/BottomTab/BottomTab.swift +0 -2
- package/ios/Navigation/Component/SnackBar.swift +0 -1
- package/ios/Navigation/HostBackGestureBridge.swift +27 -6
- package/ios/Navigation/ModalScreen.swift +0 -1
- package/ios/Navigation/Navigation.swift +14 -0
- package/ios/Navigation/NavigationContainer.swift +26 -56
- package/ios/Navigation/Navigator.swift +102 -165
- package/ios/Navigation/StackScreen.swift +19 -2
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios-demo/README.md +120 -0
- package/package.json +1 -1
- package/settings.gradle.kts +64 -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
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# MoMoUIKitsDemo
|
|
2
|
+
|
|
3
|
+
SwiftUI demo browser for `MoMoUIKits`.
|
|
4
|
+
|
|
5
|
+
The package can optionally host a Compose UIKits screen by receiving a
|
|
6
|
+
`UIViewController` factory from the host app. It does not import the KMP
|
|
7
|
+
`shared` module directly.
|
|
8
|
+
|
|
9
|
+
SwiftUI mode reuses the full demo screen set from
|
|
10
|
+
`sample/iosApp/iosApp/Demo`, including the sample `HomeView` registry and demo
|
|
11
|
+
screens for all currently available components.
|
|
12
|
+
|
|
13
|
+
## CocoaPods
|
|
14
|
+
|
|
15
|
+
In the host app `Podfile`, include both local pods:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
pod 'MoMoUIKits', :path => '../momo-native-kits/ios/native-kits.podspec'
|
|
19
|
+
pod 'MoMoUIKitsDemo', :path => '../momo-native-kits/ios-demo'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Adjust the relative paths to match the host app checkout.
|
|
23
|
+
|
|
24
|
+
## Host App Usage
|
|
25
|
+
|
|
26
|
+
```swift
|
|
27
|
+
import MoMoUIKitsDemo
|
|
28
|
+
import shared
|
|
29
|
+
|
|
30
|
+
class UIKitsViewController: BaseComposeAppViewController {
|
|
31
|
+
override func getViewController() -> UIViewController? {
|
|
32
|
+
let params = self.application.data as? [String: Any]
|
|
33
|
+
|
|
34
|
+
return MoMoUIKitsDemoViewControllerFactory.make(
|
|
35
|
+
context: MoMoUIKitsDemoContext(
|
|
36
|
+
appId: params?["appId"] as? String ?? "",
|
|
37
|
+
appCode: params?["code"] as? String ?? "",
|
|
38
|
+
appName: params?["name"] as? String,
|
|
39
|
+
appIcon: params?["icon"] as? String ?? ""
|
|
40
|
+
),
|
|
41
|
+
composeViewController: {
|
|
42
|
+
UIKitsScreenComposeViewControllerKt.UIKitsScreenComposeViewController(
|
|
43
|
+
params: params
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override func viewDidDisappear(_ animated: Bool) {
|
|
50
|
+
super.viewDidDisappear(animated)
|
|
51
|
+
composeViewController = nil
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If the host does not pass `composeViewController`, the demo opens directly in
|
|
57
|
+
SwiftUI mode.
|
|
58
|
+
|
|
59
|
+
## Included SwiftUI Demos
|
|
60
|
+
|
|
61
|
+
The pod includes the sample demo screens for:
|
|
62
|
+
|
|
63
|
+
- Animated Header
|
|
64
|
+
- Avatar
|
|
65
|
+
- Badge
|
|
66
|
+
- Baseline View
|
|
67
|
+
- Bottom Tab
|
|
68
|
+
- Button
|
|
69
|
+
- Carousel
|
|
70
|
+
- Checkbox
|
|
71
|
+
- Chip
|
|
72
|
+
- Collapse
|
|
73
|
+
- DateTimePicker
|
|
74
|
+
- Divider
|
|
75
|
+
- Header User
|
|
76
|
+
- Icon Button
|
|
77
|
+
- Image
|
|
78
|
+
- Information
|
|
79
|
+
- Input
|
|
80
|
+
- Input DropDown
|
|
81
|
+
- Input Money
|
|
82
|
+
- Input OTP
|
|
83
|
+
- Input Phone Number
|
|
84
|
+
- Input Search
|
|
85
|
+
- Input TextArea
|
|
86
|
+
- Loader
|
|
87
|
+
- Navigation
|
|
88
|
+
- Pagination
|
|
89
|
+
- Popup Display
|
|
90
|
+
- Popup Notify
|
|
91
|
+
- Popup Promotion
|
|
92
|
+
- Progress Info
|
|
93
|
+
- Radio
|
|
94
|
+
- Rating
|
|
95
|
+
- Skeleton
|
|
96
|
+
- Slider
|
|
97
|
+
- SnackBar
|
|
98
|
+
- Stepper
|
|
99
|
+
- Steps
|
|
100
|
+
- Suggest Action
|
|
101
|
+
- Swipe
|
|
102
|
+
- Switch
|
|
103
|
+
- TabView
|
|
104
|
+
- Tag
|
|
105
|
+
- Theme
|
|
106
|
+
- Title
|
|
107
|
+
- Tooltip
|
|
108
|
+
- Typography
|
|
109
|
+
- Uploader
|
|
110
|
+
|
|
111
|
+
## Validation Notes
|
|
112
|
+
|
|
113
|
+
Validated locally with:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
pod ipc spec ios/MoMoUIKits.podspec
|
|
117
|
+
pod ipc spec ios-demo/MoMoUIKitsDemo.podspec
|
|
118
|
+
pod lib lint ios/MoMoUIKits.podspec --allow-warnings --skip-import-validation
|
|
119
|
+
pod lib lint ios-demo/MoMoUIKitsDemo.podspec --allow-warnings --skip-import-validation --include-podspecs=ios/MoMoUIKits.podspec
|
|
120
|
+
```
|
package/package.json
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import org.gradle.kotlin.dsl.maven
|
|
2
|
+
|
|
3
|
+
rootProject.name = "momo-native-kits"
|
|
4
|
+
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
|
5
|
+
|
|
6
|
+
pluginManagement {
|
|
7
|
+
repositories {
|
|
8
|
+
google {
|
|
9
|
+
mavenContent {
|
|
10
|
+
includeGroupAndSubgroups("androidx")
|
|
11
|
+
includeGroupAndSubgroups("com.android")
|
|
12
|
+
includeGroupAndSubgroups("com.google")
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
mavenCentral()
|
|
16
|
+
gradlePluginPortal()
|
|
17
|
+
maven {
|
|
18
|
+
url = uri("https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven")
|
|
19
|
+
credentials {username = "download_packages"; password = "gldt-bjDqLpU_sPcHDuXau2ws" }
|
|
20
|
+
}
|
|
21
|
+
maven {
|
|
22
|
+
url = uri("https://artifacts.mservice.com.vn/repository/maven-momo-app/")
|
|
23
|
+
credentials {
|
|
24
|
+
username = "viewer"
|
|
25
|
+
password = "viewer"
|
|
26
|
+
}
|
|
27
|
+
isAllowInsecureProtocol = true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dependencyResolutionManagement {
|
|
33
|
+
repositories {
|
|
34
|
+
google {
|
|
35
|
+
mavenContent {
|
|
36
|
+
includeGroupAndSubgroups("androidx")
|
|
37
|
+
includeGroupAndSubgroups("com.android")
|
|
38
|
+
includeGroupAndSubgroups("com.google")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
mavenCentral()
|
|
42
|
+
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
|
|
43
|
+
maven {
|
|
44
|
+
url = uri("https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven")
|
|
45
|
+
credentials {
|
|
46
|
+
username = "download_packages";
|
|
47
|
+
password = "gldt-bjDqLpU_sPcHDuXau2ws"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
maven {
|
|
51
|
+
url = uri("https://artifacts.mservice.com.vn/repository/maven-momo-app/")
|
|
52
|
+
credentials {
|
|
53
|
+
username = "viewer"
|
|
54
|
+
password = "viewer"
|
|
55
|
+
}
|
|
56
|
+
isAllowInsecureProtocol = true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
include(":compose")
|
|
62
|
+
|
|
63
|
+
include(":sample:androidApp")
|
|
64
|
+
include(":sample:shared")
|