@momo-kits/native-kits 0.152.4-beta.6 → 0.152.4-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CODE_OF_CONDUCT.md +133 -0
  2. package/CONTRIBUTING.md +114 -0
  3. package/LICENSE +20 -0
  4. package/README.md +7 -0
  5. package/build.gradle.kts +32 -0
  6. package/compose/MoMoComposeKits.podspec +54 -0
  7. package/compose/build.gradle.kts +149 -0
  8. package/compose/src/androidMain/AndroidManifest.xml +2 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +105 -0
  10. package/compose/src/commonMain/composeResources/files/lottie_circle_loader.json +1 -0
  11. package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
  12. package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  13. package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  14. package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  15. package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  16. package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  17. package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  18. package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  19. package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  20. package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  21. package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  22. package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  23. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  24. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +57 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +201 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +222 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +48 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +306 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +715 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +214 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +236 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +77 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +27 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +334 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +345 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +90 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +131 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +58 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +143 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +179 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +111 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +384 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +160 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +234 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +223 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +232 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +236 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +228 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +364 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +50 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +34 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +85 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +33 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +338 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +95 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +64 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +89 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +84 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +208 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +172 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +199 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +237 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +191 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +13 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +191 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +258 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +59 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +68 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +49 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +232 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +111 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +94 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +159 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +232 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScaleSizeScope.kt +17 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +459 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +169 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +216 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +180 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +251 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +80 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +306 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +31 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +385 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +38 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +62 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +88 -0
  108. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +144 -0
  109. package/gradle.properties +19 -0
  110. package/gradlew +240 -0
  111. package/gradlew.bat +91 -0
  112. package/ios/Application/ApplicationEnvironment.swift +50 -0
  113. package/ios/Application/Components.swift +263 -0
  114. package/ios/Application/ComposeApi.swift +22 -0
  115. package/ios/Application/FloatingButton.swift +172 -0
  116. package/ios/Application/HeaderRight.swift +271 -0
  117. package/ios/Application/Screen.swift +249 -0
  118. package/ios/Badge/Badge.swift +91 -0
  119. package/ios/Badge/BadgeDot.swift +31 -0
  120. package/ios/Badge/BadgeRibbon.swift +174 -0
  121. package/ios/Button/Button.swift +211 -0
  122. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +126 -0
  123. package/ios/Checkbox/Checkbox.swift +81 -0
  124. package/ios/Chip/Chip.swift +96 -0
  125. package/ios/Colors+Radius+Spacing/Colors.swift +172 -0
  126. package/ios/Colors+Radius+Spacing/Radius.swift +22 -0
  127. package/ios/Colors+Radius+Spacing/Spacing.swift +12 -0
  128. package/ios/Extensions/Color++.swift +25 -0
  129. package/ios/Icon/Icon.swift +51 -0
  130. package/ios/Image/Image.swift +70 -0
  131. package/ios/Input/Input.swift +207 -0
  132. package/ios/Input/InputPhoneNumber.swift +176 -0
  133. package/ios/Input/InputSearch.swift +238 -0
  134. package/ios/Input/InputTextArea.swift +242 -0
  135. package/ios/Lottie/LottieView.swift +86 -0
  136. package/ios/OTPKeyboard/KeyboardButton.swift +41 -0
  137. package/ios/OTPKeyboard/OTPKeyboard.swift +145 -0
  138. package/ios/Popup/PopupDisplay.swift +284 -0
  139. package/ios/Popup/PopupInput.swift +96 -0
  140. package/ios/Popup/PopupPromotion.swift +73 -0
  141. package/ios/PopupView/FullscreenPopup.swift +251 -0
  142. package/ios/PopupView/Modifiers.swift +158 -0
  143. package/ios/PopupView/PopupView.swift +289 -0
  144. package/ios/PopupView/Utils++.swift +281 -0
  145. package/ios/ScrollIndicator/ScrollIndicator.swift +110 -0
  146. package/ios/Swipeable/SwipeCell.swift +278 -0
  147. package/ios/Swipeable/SwipeCellModel.swift +86 -0
  148. package/ios/Switch/Switch.swift +44 -0
  149. package/ios/Template/Logo/Logo.swift +75 -0
  150. package/ios/Template/TrustBanner/TrustBanner.swift +120 -0
  151. package/ios/Theme.md +18 -0
  152. package/ios/Typography/Text.swift +140 -0
  153. package/ios/Typography/Typography.swift +95 -0
  154. package/ios/native-kits.podspec +18 -0
  155. package/package.json +6 -7
  156. package/settings.gradle.kts +25 -0
  157. package/shared/build.gradle.kts +0 -74
@@ -0,0 +1,289 @@
1
+ import SwiftUI
2
+
3
+ public enum DismissSource {
4
+ case binding // set isPresented to false ot item to nil
5
+ case tapInside
6
+ case tapOutside
7
+ case drag
8
+ case autohide
9
+ }
10
+
11
+ public struct Popup<Item: Equatable, PopupContent: View>: ViewModifier {
12
+
13
+ init(isPresented: Binding<Bool>,
14
+ type: PopupType,
15
+ position: Position,
16
+ animation: Animation,
17
+ autohideIn: Double?,
18
+ dragToDismiss: Bool,
19
+ closeOnTapOutside: Bool,
20
+ shouldShowContent: Bool = true,
21
+ showContent: Bool = true,
22
+ dismissCallback: @escaping (DismissSource) -> (),
23
+ dismissSource: Binding<DismissSource?>,
24
+ animationCompletedCallback: @escaping () -> (),
25
+ view: @escaping () -> PopupContent) {
26
+ self._isPresented = isPresented
27
+ self._item = .constant(nil)
28
+ self.type = type
29
+ self.position = position
30
+ self.animation = animation
31
+ self.autohideIn = autohideIn
32
+ self.dragToDismiss = dragToDismiss
33
+ self.closeOnTapOutside = closeOnTapOutside
34
+ self.shouldShowContent = shouldShowContent
35
+ self.showContent = showContent
36
+ self.dismissCallback = dismissCallback
37
+ self._dismissSource = dismissSource
38
+ self.animationCompletedCallback = animationCompletedCallback
39
+ self.view = view
40
+ }
41
+
42
+ init(item: Binding<Item?>,
43
+ type: PopupType,
44
+ position: Position,
45
+ animation: Animation,
46
+ autohideIn: Double?,
47
+ dragToDismiss: Bool,
48
+ closeOnTapOutside: Bool,
49
+ shouldShowContent: Bool = true,
50
+ showContent: Bool = true,
51
+ dismissCallback: @escaping (DismissSource) -> (),
52
+ dismissSource: Binding<DismissSource?>,
53
+ animationCompletedCallback: @escaping () -> (),
54
+ view: @escaping () -> PopupContent) {
55
+ self._isPresented = .constant(false)
56
+ self._item = item
57
+ self.type = type
58
+ self.position = position
59
+ self.animation = animation
60
+ self.autohideIn = autohideIn
61
+ self.dragToDismiss = dragToDismiss
62
+ self.closeOnTapOutside = closeOnTapOutside
63
+ self.shouldShowContent = shouldShowContent
64
+ self.showContent = showContent
65
+ self.dismissCallback = dismissCallback
66
+ self._dismissSource = dismissSource
67
+ self.animationCompletedCallback = animationCompletedCallback
68
+ self.view = view
69
+ }
70
+
71
+ public enum PopupType {
72
+
73
+ case `default`
74
+ case toast
75
+ case floater(verticalPadding: CGFloat = 10, useSafeAreaInset: Bool = true)
76
+
77
+ func shouldBeCentered() -> Bool {
78
+ switch self {
79
+ case .`default`:
80
+ return true
81
+ default:
82
+ return false
83
+ }
84
+ }
85
+ }
86
+
87
+ public enum Position {
88
+ case top
89
+ case bottom
90
+ }
91
+
92
+ private enum DragState {
93
+ case inactive
94
+ case dragging(translation: CGSize)
95
+
96
+ var translation: CGSize {
97
+ switch self {
98
+ case .inactive:
99
+ return .zero
100
+ case .dragging(let translation):
101
+ return translation
102
+ }
103
+ }
104
+
105
+ var isDragging: Bool {
106
+ switch self {
107
+ case .inactive:
108
+ return false
109
+ case .dragging:
110
+ return true
111
+ }
112
+ }
113
+ }
114
+
115
+ // MARK: - Public Properties
116
+ /// Tells if the sheet should be presented or not
117
+ @Binding var isPresented: Bool
118
+ @Binding var item: Item?
119
+
120
+ var type: PopupType
121
+ var position: Position
122
+
123
+ var animation: Animation
124
+
125
+ /// If nil - never hides on its own
126
+ var autohideIn: Double?
127
+
128
+ /// Should allow dismiss by dragging
129
+ var dragToDismiss: Bool
130
+
131
+ /// Should close on tap outside - default is `true`
132
+ var closeOnTapOutside: Bool
133
+
134
+ /// Trigger popup showing/hiding animations and...
135
+ var shouldShowContent: Bool
136
+
137
+ /// ... once hiding animation is finished remove popup from the memory using this flag
138
+ var showContent: Bool
139
+
140
+ /// is called on any close action
141
+ var dismissCallback: (DismissSource) -> ()
142
+
143
+ /// Set dismiss souce to pass to dismiss callback
144
+ @Binding private var dismissSource: DismissSource?
145
+
146
+ /// called on showing/hiding sliding animation completed
147
+ var animationCompletedCallback: () -> ()
148
+
149
+ var view: () -> PopupContent
150
+
151
+ // MARK: - Private Properties
152
+ @Environment(\.safeAreaInsets) private var safeAreaInsets
153
+
154
+ /// The rect and safe area of the hosting controller
155
+ @State private var presenterContentRect: CGRect = .zero
156
+
157
+ /// The rect and safe area of popup content
158
+ @State private var sheetContentRect: CGRect = .zero
159
+
160
+ /// Drag to dismiss gesture state
161
+ @GestureState private var dragState = DragState.inactive
162
+
163
+ /// Last position for drag gesture
164
+ @State private var lastDragPosition: CGFloat = 0
165
+
166
+ /// The offset when the popup is displayed
167
+ private var displayedOffset: CGFloat {
168
+ switch type {
169
+ case .`default`:
170
+ return -presenterContentRect.midY + screenHeight/2
171
+ case .toast:
172
+ if position == .bottom {
173
+ return presenterContentRect.minY + safeAreaInsets.bottom + presenterContentRect.height - presenterContentRect.midY - sheetContentRect.height/2
174
+ } else {
175
+ return presenterContentRect.minY - safeAreaInsets.top - presenterContentRect.midY + sheetContentRect.height/2
176
+ }
177
+ case .floater(let verticalPadding, let useSafeAreaInset):
178
+ if position == .bottom {
179
+ return presenterContentRect.minY + safeAreaInsets.bottom + presenterContentRect.height - presenterContentRect.midY - sheetContentRect.height/2 - verticalPadding + (useSafeAreaInset ? -safeAreaInsets.bottom : 0)
180
+ } else {
181
+ return presenterContentRect.minY - safeAreaInsets.top - presenterContentRect.midY + sheetContentRect.height/2 + verticalPadding + (useSafeAreaInset ? safeAreaInsets.top : 0)
182
+ }
183
+ }
184
+ }
185
+
186
+ /// The offset when the popup is hidden
187
+ private var hiddenOffset: CGFloat {
188
+ if position == .top {
189
+ if presenterContentRect.isEmpty {
190
+ return -1000
191
+ }
192
+ return -presenterContentRect.midY - sheetContentRect.height/2 - 5
193
+ } else {
194
+ if presenterContentRect.isEmpty {
195
+ return 1000
196
+ }
197
+ return screenHeight - presenterContentRect.midY + sheetContentRect.height/2 + 5
198
+ }
199
+ }
200
+
201
+ /// The current offset, based on the **presented** property
202
+ private var currentOffset: CGFloat {
203
+ return shouldShowContent ? displayedOffset : hiddenOffset
204
+ }
205
+
206
+ private var screenSize: CGSize {
207
+ #if os(iOS) || os(tvOS)
208
+ return UIScreen.main.bounds.size
209
+ #elseif os(watchOS)
210
+ return WKInterfaceDevice.current().screenBounds.size
211
+ #else
212
+ return NSScreen.main?.frame.size ?? .zero
213
+ #endif
214
+ }
215
+
216
+ private var screenHeight: CGFloat {
217
+ screenSize.height
218
+ }
219
+
220
+ // MARK: - Content Builders
221
+ public func body(content: Content) -> some View {
222
+ content
223
+ .frameGetter($presenterContentRect)
224
+ .overlay(
225
+ Group {
226
+ if showContent {
227
+ sheet()
228
+ }
229
+ }
230
+ )
231
+ }
232
+
233
+ /// This is the builder for the sheet content
234
+ func sheet() -> some View {
235
+ let sheet = ZStack {
236
+ self.view()
237
+ .frameGetter($sheetContentRect)
238
+ .offset(y: currentOffset)
239
+ .onAnimationCompleted(for: currentOffset) {
240
+ animationCompletedCallback()
241
+ }
242
+ .animation(animation)
243
+ }
244
+
245
+ #if !os(tvOS)
246
+ let drag = DragGesture()
247
+ .updating($dragState) { drag, state, _ in
248
+ state = .dragging(translation: drag.translation)
249
+ }
250
+ .onEnded(onDragEnded)
251
+
252
+ return sheet
253
+ .applyIf(dragToDismiss) {
254
+ $0.offset(y: dragOffset())
255
+ .simultaneousGesture(drag)
256
+ }
257
+ #else
258
+ return sheet
259
+ #endif
260
+ }
261
+
262
+ #if !os(tvOS)
263
+ func dragOffset() -> CGFloat {
264
+ if (position == .bottom && dragState.translation.height > 0) ||
265
+ (position == .top && dragState.translation.height < 0) {
266
+ return dragState.translation.height
267
+ }
268
+ return lastDragPosition
269
+ }
270
+
271
+ private func onDragEnded(drag: DragGesture.Value) {
272
+ let reference = sheetContentRect.height / 3
273
+ if (position == .bottom && drag.translation.height > reference) ||
274
+ (position == .top && drag.translation.height < -reference) {
275
+ lastDragPosition = drag.translation.height
276
+ withAnimation {
277
+ lastDragPosition = 0
278
+ }
279
+ dismissSource = .drag
280
+ dismiss()
281
+ }
282
+ }
283
+ #endif
284
+
285
+ private func dismiss() {
286
+ isPresented = false
287
+ item = nil
288
+ }
289
+ }
@@ -0,0 +1,281 @@
1
+ import SwiftUI
2
+ import Combine
3
+
4
+ final class DispatchWorkHolder {
5
+ var work: DispatchWorkItem?
6
+ }
7
+
8
+ final class ClassReference<T> {
9
+ var value: T
10
+
11
+ init(_ value: T) {
12
+ self.value = value
13
+ }
14
+ }
15
+
16
+ public extension View {
17
+
18
+ @ViewBuilder
19
+ func valueChanged<T: Equatable>(value: T, onChange: @escaping (T) -> Void) -> some View {
20
+ if #available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *) {
21
+ self.onChange(of: value, perform: onChange)
22
+ } else {
23
+ self.onReceive(Just(value)) { value in
24
+ onChange(value)
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ public extension View {
31
+ @ViewBuilder
32
+ func applyIf<T: View>(_ condition: Bool, apply: (Self) -> T) -> some View {
33
+ if condition {
34
+ apply(self)
35
+ } else {
36
+ self
37
+ }
38
+ }
39
+
40
+ @ViewBuilder
41
+ func addTapIf(if condition: Bool, onTap: @escaping ()->()) -> some View {
42
+ if condition {
43
+ self.simultaneousGesture(
44
+ TapGesture().onEnded {
45
+ onTap()
46
+ }
47
+ )
48
+ } else {
49
+ self
50
+ }
51
+ }
52
+ }
53
+
54
+ struct FrameGetter: ViewModifier {
55
+
56
+ @Binding var frame: CGRect
57
+
58
+ func body(content: Content) -> some View {
59
+ content
60
+ .background(
61
+ GeometryReader { proxy -> AnyView in
62
+ DispatchQueue.main.async {
63
+ let rect = proxy.frame(in: .global)
64
+ // This avoids an infinite layout loop
65
+ if rect.integral != self.frame.integral {
66
+ self.frame = rect
67
+ }
68
+ }
69
+ return AnyView(EmptyView())
70
+ }
71
+ )
72
+ }
73
+ }
74
+
75
+ extension View {
76
+ public func frameGetter(_ frame: Binding<CGRect>) -> some View {
77
+ modifier(FrameGetter(frame: frame))
78
+ }
79
+ }
80
+
81
+ struct AnimationCompletionObserverModifier<Value>: AnimatableModifier where Value: VectorArithmetic, Value: Comparable {
82
+
83
+ /// While animating, SwiftUI changes the old input value to the new target value using this property. This value is set to the old value until the animation completes.
84
+ var animatableData: Value {
85
+ didSet {
86
+ notifyCompletionIfFinished()
87
+ }
88
+ }
89
+
90
+ /// The target value for which we're observing. This value is directly set once the animation starts. During animation, `animatableData` will hold the oldValue and is only updated to the target value once the animation completes.
91
+ private var targetValue: Value
92
+
93
+ /// The completion callback which is called once the animation completes.
94
+ private var completion: () -> Void
95
+
96
+ init(observedValue: Value, completion: @escaping () -> Void) {
97
+ self.completion = completion
98
+ self.animatableData = observedValue
99
+ targetValue = observedValue
100
+ }
101
+
102
+ /// Verifies whether the current animation is finished and calls the completion callback if true.
103
+ private func notifyCompletionIfFinished() {
104
+ guard animatableData == targetValue else { return }
105
+
106
+ /// Dispatching is needed to take the next runloop for the completion callback.
107
+ /// This prevents errors like "Modifying state during view update, this will cause undefined behavior."
108
+ DispatchQueue.main.async {
109
+ self.completion()
110
+ }
111
+ }
112
+
113
+ func body(content: Content) -> some View {
114
+ /// We're not really modifying the view so we can directly return the original input value.
115
+ return content
116
+ }
117
+ }
118
+
119
+ struct AnimatableModifierDouble: AnimatableModifier {
120
+
121
+ var targetValue: Double
122
+ static var done = false
123
+
124
+ // SwiftUI gradually varies it from old value to the new value
125
+ var animatableData: Double {
126
+ didSet {
127
+ checkIfFinished()
128
+ }
129
+ }
130
+ var completion: () -> ()
131
+
132
+ // Re-created every time the control argument changes
133
+ init(bindedValue: Double, completion: @escaping () -> ()) {
134
+ self.completion = completion
135
+
136
+ // Set animatableData to the new value. But SwiftUI again directly
137
+ // and gradually varies the value while the body
138
+ // is being called to animate. Following line serves the purpose of
139
+ // associating the extenal argument with the animatableData.
140
+ self.animatableData = bindedValue
141
+ targetValue = bindedValue
142
+ AnimatableModifierDouble.done = false
143
+ }
144
+
145
+ func checkIfFinished() -> () {
146
+ if AnimatableModifierDouble.done { return }
147
+ let delta = 0.1
148
+ if animatableData > targetValue - delta &&
149
+ animatableData < targetValue + delta {
150
+ //print("check", animatableData, targetValue)
151
+ AnimatableModifierDouble.done = true
152
+ DispatchQueue.main.async {
153
+ self.completion()
154
+ }
155
+ }
156
+ }
157
+
158
+ func body(content: Content) -> some View {
159
+ content
160
+ }
161
+ }
162
+
163
+ extension View {
164
+
165
+ func onAnimationCompleted(for value: Double, completion: @escaping () -> Void) -> some View {
166
+ modifier(AnimatableModifierDouble(bindedValue: value, completion: completion))
167
+ }
168
+ }
169
+
170
+ extension UIApplication {
171
+ var keyWindow: UIWindow? {
172
+ connectedScenes
173
+ .compactMap {
174
+ $0 as? UIWindowScene
175
+ }
176
+ .flatMap {
177
+ $0.windows
178
+ }
179
+ .first {
180
+ $0.isKeyWindow
181
+ }
182
+ }
183
+ }
184
+
185
+ private struct SafeAreaInsetsKey: EnvironmentKey {
186
+ static var defaultValue: EdgeInsets {
187
+ UIApplication.shared.keyWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
188
+ }
189
+ }
190
+
191
+ extension EnvironmentValues {
192
+ var safeAreaInsets: EdgeInsets {
193
+ self[SafeAreaInsetsKey.self]
194
+ }
195
+ }
196
+
197
+ private extension UIEdgeInsets {
198
+ var swiftUiInsets: EdgeInsets {
199
+ EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
200
+ }
201
+ }
202
+
203
+ extension View {
204
+
205
+ func transparentNonAnimatingFullScreenCover<Content: View>(isPresented: Binding<Bool>, content: @escaping () -> Content) -> some View {
206
+ modifier(TransparentNonAnimatableFullScreenModifier(isPresented: isPresented, fullScreenContent: content))
207
+ }
208
+ }
209
+
210
+ private struct TransparentNonAnimatableFullScreenModifier<FullScreenContent: View>: ViewModifier {
211
+
212
+ @Binding var isPresented: Bool
213
+ let fullScreenContent: () -> (FullScreenContent)
214
+
215
+ func body(content: Content) -> some View {
216
+ if #available(iOS 14.0, *) {
217
+ content
218
+ .valueChanged(value: isPresented, onChange: { isPresented in
219
+ UIView.setAnimationsEnabled(false)
220
+ })
221
+ .fullScreenCover(isPresented: $isPresented, content: {
222
+ ZStack {
223
+ fullScreenContent()
224
+ }
225
+ .background(FullScreenCoverBackgroundRemovalView())
226
+ .onAppear {
227
+ if !UIView.areAnimationsEnabled {
228
+ UIView.setAnimationsEnabled(true)
229
+ }
230
+ }
231
+ .onDisappear {
232
+ if !UIView.areAnimationsEnabled {
233
+ UIView.setAnimationsEnabled(true)
234
+ }
235
+ }
236
+ })
237
+ } else {
238
+ content
239
+ .valueChanged(value: isPresented, onChange: { isPresented in
240
+ UIView.setAnimationsEnabled(false)
241
+ })
242
+ .sheet(isPresented: $isPresented, content: {
243
+ ZStack {
244
+ fullScreenContent()
245
+ }
246
+ .background(FullScreenCoverBackgroundRemovalView())
247
+ .onAppear {
248
+ if !UIView.areAnimationsEnabled {
249
+ UIView.setAnimationsEnabled(true)
250
+ }
251
+ }
252
+ .onDisappear {
253
+ if !UIView.areAnimationsEnabled {
254
+ UIView.setAnimationsEnabled(true)
255
+ }
256
+ }
257
+ })
258
+ }
259
+ }
260
+
261
+ }
262
+
263
+ private struct FullScreenCoverBackgroundRemovalView: UIViewRepresentable {
264
+
265
+ private class BackgroundRemovalView: UIView {
266
+
267
+ override func didMoveToWindow() {
268
+ super.didMoveToWindow()
269
+
270
+ superview?.superview?.backgroundColor = .clear
271
+ }
272
+
273
+ }
274
+
275
+ func makeUIView(context: Context) -> UIView {
276
+ return BackgroundRemovalView()
277
+ }
278
+
279
+ func updateUIView(_ uiView: UIView, context: Context) {}
280
+
281
+ }
@@ -0,0 +1,110 @@
1
+ import Foundation
2
+ import SwiftUI
3
+
4
+ public struct ScrollViewWithScrollBar<Content: View>: View {
5
+ let INDICATOR_HEIGHT: CGFloat = 4
6
+ @State var position = CGPoint(x: 0, y: 2) //2 is INDICATOR_HEIGHT/2
7
+ @State var contentSize: CGFloat = 0
8
+ @State var scrollViewWidth: CGFloat = 0
9
+ var indicatorWidth: CGFloat
10
+ var indicatorContainerWidth: CGFloat
11
+ var indicatorColor: Color
12
+ var indicatorContainerColor: Color
13
+ var spacing: CGFloat = 4
14
+ @ViewBuilder var content: () -> Content
15
+
16
+ public init(
17
+ indicatorWidth: CGFloat = 12, indicatorContainerWidth: CGFloat = 36,
18
+ indicatorColor: Color = Colors.primary, indicatorContainerColor: Color = Colors.black06,
19
+ spacing: CGFloat = 4,
20
+ @ViewBuilder content: @escaping () -> Content
21
+ ) {
22
+ self.indicatorWidth = indicatorWidth
23
+ self.indicatorContainerWidth = indicatorContainerWidth
24
+ self.indicatorColor = indicatorColor
25
+ self.indicatorContainerColor = indicatorContainerColor
26
+ self.content = content
27
+ self.spacing = spacing
28
+ }
29
+
30
+ // MARK: Public
31
+
32
+ public var body: some View {
33
+ return VStack(spacing: spacing) {
34
+ ScrollView(.horizontal, showsIndicators: false) {
35
+ HStack {
36
+ content()
37
+ }
38
+ .fixedSize(horizontal: true, vertical: false)
39
+ .overlay(
40
+ GeometryReader { proxy in
41
+ Color.clear
42
+ .preference(
43
+ key: ViewOffsetKey.self,
44
+ value: -proxy.frame(in: .named("scroll")).origin.x
45
+ )
46
+ .preference(
47
+ key: ContentSizeKey.self,
48
+ value: proxy.size.width)
49
+ },
50
+ alignment: .topLeading
51
+ )
52
+ .onPreferenceChange(ContentSizeKey.self) { newContentSize in
53
+ if newContentSize > 0 {
54
+ self.contentSize = newContentSize
55
+ if self.position == CGPoint(x: 0, y: 2) {
56
+ self.position = CGPoint(x: indicatorWidth / 2, y: INDICATOR_HEIGHT / 2)
57
+ }
58
+ }
59
+ }
60
+ .onPreferenceChange(ViewOffsetKey.self) { offset in
61
+ let newPosition =
62
+ (offset * ((indicatorContainerWidth - indicatorWidth) / (contentSize - scrollViewWidth)))
63
+ + indicatorWidth / 2
64
+
65
+ if !newPosition.isNaN {
66
+ self.position = CGPoint(x: newPosition, y: INDICATOR_HEIGHT / 2)
67
+ }
68
+ }
69
+ }.background(
70
+ GeometryReader { proxy in
71
+ Color.clear.onAppear {
72
+ self.scrollViewWidth = proxy.size.width
73
+ }
74
+ }
75
+ ).coordinateSpace(name: "scroll")
76
+
77
+ HStack(spacing: 0) {
78
+ HStack(spacing: 0) {}.frame(width: indicatorWidth, height: INDICATOR_HEIGHT)
79
+ .background(indicatorColor)
80
+ .cornerRadius(8)
81
+ .position(self.position)
82
+ }.frame(width: indicatorContainerWidth, height: INDICATOR_HEIGHT, alignment: .leading)
83
+ .background(indicatorContainerColor).cornerRadius(8)
84
+ }
85
+ }
86
+ }
87
+
88
+ // MARK: - ViewOffsetKey
89
+
90
+ struct ViewOffsetKey: PreferenceKey {
91
+ typealias Value = CGFloat
92
+
93
+ static var defaultValue = CGFloat.zero
94
+
95
+ static func reduce(value: inout Value, nextValue: () -> Value) {
96
+ value += nextValue()
97
+ }
98
+ }
99
+
100
+ // MARK: - ContentSizeKey
101
+
102
+ struct ContentSizeKey: PreferenceKey {
103
+ typealias Value = CGFloat
104
+
105
+ static var defaultValue = CGFloat.zero
106
+
107
+ static func reduce(value: inout Value, nextValue: () -> Value) {
108
+ value = nextValue()
109
+ }
110
+ }