@momo-kits/native-kits 0.152.4-beta.2 → 0.152.4-beta.3

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 (155) 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 +69 -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/BadgeDot.swift +31 -0
  119. package/ios/Button/Button.swift +211 -0
  120. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +126 -0
  121. package/ios/Checkbox/Checkbox.swift +81 -0
  122. package/ios/Chip/Chip.swift +96 -0
  123. package/ios/Colors+Radius+Spacing/Colors.swift +172 -0
  124. package/ios/Colors+Radius+Spacing/Radius.swift +22 -0
  125. package/ios/Colors+Radius+Spacing/Spacing.swift +12 -0
  126. package/ios/Extensions/Color++.swift +25 -0
  127. package/ios/Icon/Icon.swift +51 -0
  128. package/ios/Image/Image.swift +70 -0
  129. package/ios/Input/Input.swift +207 -0
  130. package/ios/Input/InputPhoneNumber.swift +176 -0
  131. package/ios/Input/InputSearch.swift +238 -0
  132. package/ios/Input/InputTextArea.swift +242 -0
  133. package/ios/Lottie/LottieView.swift +86 -0
  134. package/ios/OTPKeyboard/KeyboardButton.swift +41 -0
  135. package/ios/OTPKeyboard/OTPKeyboard.swift +145 -0
  136. package/ios/Popup/PopupDisplay.swift +284 -0
  137. package/ios/Popup/PopupInput.swift +96 -0
  138. package/ios/Popup/PopupPromotion.swift +73 -0
  139. package/ios/PopupView/FullscreenPopup.swift +251 -0
  140. package/ios/PopupView/Modifiers.swift +158 -0
  141. package/ios/PopupView/PopupView.swift +289 -0
  142. package/ios/PopupView/Utils++.swift +281 -0
  143. package/ios/ScrollIndicator/ScrollIndicator.swift +110 -0
  144. package/ios/Swipeable/SwipeCell.swift +278 -0
  145. package/ios/Swipeable/SwipeCellModel.swift +86 -0
  146. package/ios/Switch/Switch.swift +44 -0
  147. package/ios/Template/Logo/Logo.swift +75 -0
  148. package/ios/Template/TrustBanner/TrustBanner.swift +120 -0
  149. package/ios/Theme.md +18 -0
  150. package/ios/Typography/Text.swift +140 -0
  151. package/ios/Typography/Typography.swift +95 -0
  152. package/ios/native-kits.podspec +18 -0
  153. package/package.json +6 -7
  154. package/settings.gradle.kts +25 -0
  155. package/shared/build.gradle.kts +0 -74
@@ -0,0 +1,251 @@
1
+
2
+ import Foundation
3
+ import SwiftUI
4
+
5
+ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier {
6
+
7
+ // MARK: - Presentaion
8
+ @Binding var isPresented: Bool
9
+ @Binding var item: Item?
10
+
11
+ var isBoolMode: Bool
12
+
13
+ var sheetPresented: Bool {
14
+ item != nil || isPresented
15
+ }
16
+
17
+ // MARK: - Parameters
18
+ var type: Popup<Item, PopupContent>.PopupType
19
+ var position: Popup<Item, PopupContent>.Position
20
+
21
+ var animation: Animation
22
+
23
+ /// If nil - never hides on its own
24
+ var autohideIn: Double?
25
+
26
+ /// Should allow dismiss by dragging
27
+ var dragToDismiss: Bool
28
+
29
+ /// Should close on tap outside - default is `true`
30
+ var closeOnTapOutside: Bool
31
+
32
+ /// Background color for outside area - default is `Color.clear`
33
+ var backgroundColor: Color
34
+
35
+ /// is called on any close action
36
+ var dismissCallback: (DismissSource) -> ()
37
+
38
+ var view: () -> PopupContent
39
+
40
+ // MARK: - Presentation animation
41
+ /// Trigger popup showing/hiding animations and...
42
+ @State private var shouldShowContent = false
43
+
44
+ /// ... once hiding animation is finished remove popup from the memory using this flag
45
+ @State private var showContent = false
46
+
47
+ /// show transparentNonAnimatingFullScreenCover
48
+ @State private var showSheet = false
49
+
50
+ /// opacity of background color
51
+ @State private var opacity = 0.0
52
+
53
+ // MARK: - Autohide
54
+ /// Class reference for capturing a weak reference later in dispatch work holder.
55
+ private var isPresentedRef: ClassReference<Binding<Bool>>?
56
+ private var itemRef: ClassReference<Binding<Item?>>?
57
+
58
+ /// holder for autohiding dispatch work (to be able to cancel it when needed)
59
+ private var dispatchWorkHolder = DispatchWorkHolder()
60
+
61
+ // MARK: - Internal
62
+ /// Set dismiss souce to pass to dismiss callback
63
+ @State private var dismissSource: DismissSource?
64
+
65
+ init(isPresented: Binding<Bool>,
66
+ type: Popup<Item, PopupContent>.PopupType = .`default`,
67
+ position: Popup<Item, PopupContent>.Position = .bottom,
68
+ animation: Animation,
69
+ autohideIn: Double?,
70
+ dragToDismiss: Bool,
71
+ closeOnTapOutside: Bool,
72
+ backgroundColor: Color,
73
+ dismissCallback: @escaping (DismissSource) -> (),
74
+ view: @escaping () -> PopupContent) {
75
+ self._isPresented = isPresented
76
+ self._item = .constant(nil)
77
+ self.isBoolMode = true
78
+
79
+ self.type = type
80
+ self.position = position
81
+ self.animation = animation
82
+ self.autohideIn = autohideIn
83
+ self.dragToDismiss = dragToDismiss
84
+ self.closeOnTapOutside = closeOnTapOutside
85
+ self.backgroundColor = backgroundColor
86
+ self.dismissCallback = dismissCallback
87
+ self.view = view
88
+
89
+ self.isPresentedRef = ClassReference(self.$isPresented)
90
+ self.itemRef = ClassReference(self.$item)
91
+ }
92
+
93
+ init(item: Binding<Item?>,
94
+ type: Popup<Item, PopupContent>.PopupType = .`default`,
95
+ position: Popup<Item, PopupContent>.Position = .bottom,
96
+ animation: Animation,
97
+ autohideIn: Double?,
98
+ dragToDismiss: Bool,
99
+ closeOnTapOutside: Bool,
100
+ backgroundColor: Color,
101
+ dismissCallback: @escaping (DismissSource) -> (),
102
+ view: @escaping () -> PopupContent) {
103
+ self._isPresented = .constant(false)
104
+ self._item = item
105
+ self.isBoolMode = false
106
+
107
+ self.type = type
108
+ self.position = position
109
+ self.animation = animation
110
+ self.autohideIn = autohideIn
111
+ self.dragToDismiss = dragToDismiss
112
+ self.closeOnTapOutside = closeOnTapOutside
113
+ self.backgroundColor = backgroundColor
114
+ self.dismissCallback = dismissCallback
115
+ self.view = view
116
+
117
+ self.isPresentedRef = ClassReference(self.$isPresented)
118
+ self.itemRef = ClassReference(self.$item)
119
+ }
120
+
121
+ public func body(content: Content) -> some View {
122
+ if isBoolMode {
123
+ boolBody(content: content)
124
+ } else {
125
+ itemBody(content: content)
126
+ }
127
+ }
128
+
129
+ func backgroundColorView() -> some View {
130
+ backgroundColor.opacity(opacity)
131
+ .applyIf(closeOnTapOutside) { view in
132
+ view.contentShape(Rectangle())
133
+ }
134
+ .addTapIf(if: closeOnTapOutside) {
135
+ dismissSource = .tapOutside
136
+ isPresented = false
137
+ item = nil
138
+ }
139
+ .edgesIgnoringSafeArea(.all)
140
+ .animation(.linear(duration: 0.2), value: opacity)
141
+ }
142
+
143
+ public func boolBody(content: Content) -> some View {
144
+ content
145
+ .transparentNonAnimatingFullScreenCover(isPresented: $showSheet) {
146
+ backgroundColorView()
147
+ .modifier(
148
+ Popup(
149
+ isPresented: $isPresented,
150
+ type: type,
151
+ position: position,
152
+ animation: animation,
153
+ autohideIn: autohideIn,
154
+ dragToDismiss: dragToDismiss,
155
+ closeOnTapOutside: closeOnTapOutside,
156
+ shouldShowContent: shouldShowContent,
157
+ showContent: showContent,
158
+ dismissCallback: { _ in },
159
+ dismissSource: $dismissSource,
160
+ animationCompletedCallback: onAnimationCompleted,
161
+ view: view)
162
+ )
163
+ }
164
+ .valueChanged(value: isPresented, onChange: { newValue in
165
+ appearAction(sheetPresented: newValue)
166
+ })
167
+ }
168
+
169
+ public func itemBody(content: Content) -> some View {
170
+ content
171
+ .transparentNonAnimatingFullScreenCover(isPresented: $showSheet) {
172
+ backgroundColorView()
173
+ .modifier(
174
+ Popup(
175
+ item: $item,
176
+ type: type,
177
+ position: position,
178
+ animation: animation,
179
+ autohideIn: autohideIn,
180
+ dragToDismiss: dragToDismiss,
181
+ closeOnTapOutside: closeOnTapOutside,
182
+ shouldShowContent: shouldShowContent,
183
+ showContent: showContent,
184
+ dismissCallback: { _ in },
185
+ dismissSource: $dismissSource,
186
+ animationCompletedCallback: onAnimationCompleted,
187
+ view: view)
188
+ )
189
+ }
190
+ .valueChanged(value: item, onChange: { newValue in
191
+ appearAction(sheetPresented: newValue != nil)
192
+ })
193
+ }
194
+
195
+ func appearAction(sheetPresented: Bool) {
196
+ if sheetPresented {
197
+ dismissSource = nil
198
+ showSheet = true // show transparent fullscreen sheet
199
+ showContent = true // immediately load popup body
200
+ performWithDelay(0.01) {
201
+ shouldShowContent = true // this will cause currentOffset change thus triggering the sliding showing animation
202
+ opacity = 1 // this will cause cross disolving animation for background color
203
+ setupAutohide()
204
+ }
205
+ } else {
206
+ dispatchWorkHolder.work?.cancel()
207
+ shouldShowContent = false // this will cause currentOffset change thus triggering the sliding hiding animation
208
+ opacity = 0
209
+ // do the rest once the animation is finished (see onAnimationCompleted())
210
+ performWithDelay(0.3) { // TEMP: imitate onAnimationCompleted for now
211
+ onAnimationCompleted()
212
+ }
213
+ }
214
+ }
215
+
216
+ func onAnimationCompleted() -> () {
217
+ if shouldShowContent { // return if this was called on showing animation, only proceed if called on hiding
218
+ return
219
+ }
220
+ showContent = false // unload popup body after hiding animation is done
221
+ performWithDelay(0.01) {
222
+ showSheet = false
223
+ }
224
+ dismissCallback(dismissSource ?? .binding)
225
+ }
226
+
227
+ func setupAutohide() {
228
+ // if needed, dispatch autohide and cancel previous one
229
+ if let autohideIn = autohideIn {
230
+ dispatchWorkHolder.work?.cancel()
231
+
232
+ // Weak reference to avoid the work item capturing the struct,
233
+ // which would create a retain cycle with the work holder itself.
234
+ dispatchWorkHolder.work = DispatchWorkItem(block: { [weak isPresentedRef, weak itemRef] in
235
+ isPresentedRef?.value.wrappedValue = false
236
+ itemRef?.value.wrappedValue = nil
237
+ dismissSource = .autohide
238
+ })
239
+ if sheetPresented, let work = dispatchWorkHolder.work {
240
+ DispatchQueue.main.asyncAfter(deadline: .now() + autohideIn, execute: work)
241
+ }
242
+ }
243
+ }
244
+
245
+ func performWithDelay(_ delay: Double, block: @escaping ()->()) {
246
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
247
+ block()
248
+ }
249
+ }
250
+
251
+ }
@@ -0,0 +1,158 @@
1
+ import SwiftUI
2
+
3
+ extension View {
4
+
5
+ public func popup<Item: Equatable, PopupContent: View>(
6
+ item: Binding<Item?>,
7
+ type: Popup<Item, PopupContent>.PopupType = .`default`,
8
+ position: Popup<Item, PopupContent>.Position = .bottom,
9
+ animation: Animation = Animation.easeOut(duration: 0.3),
10
+ autohideIn: Double? = nil,
11
+ dragToDismiss: Bool = true,
12
+ closeOnTapOutside: Bool = false,
13
+ backgroundColor: Color = Color.clear,
14
+ @ViewBuilder view: @escaping () -> PopupContent) -> some View {
15
+ self.modifier(
16
+ FullscreenPopup(
17
+ item: item,
18
+ type: type,
19
+ position: position,
20
+ animation: animation,
21
+ autohideIn: autohideIn,
22
+ dragToDismiss: dragToDismiss,
23
+ closeOnTapOutside: closeOnTapOutside,
24
+ backgroundColor: backgroundColor,
25
+ dismissCallback: { _ in },
26
+ view: view)
27
+ )
28
+ }
29
+
30
+ public func popup<PopupContent: View>(
31
+ isPresented: Binding<Bool>,
32
+ type: Popup<Int, PopupContent>.PopupType = .`default`,
33
+ position: Popup<Int, PopupContent>.Position = .bottom,
34
+ animation: Animation = Animation.easeOut(duration: 0.3),
35
+ autohideIn: Double? = nil,
36
+ dragToDismiss: Bool = true,
37
+ closeOnTapOutside: Bool = false,
38
+ backgroundColor: Color = Color.clear,
39
+ @ViewBuilder view: @escaping () -> PopupContent) -> some View {
40
+ self.modifier(
41
+ FullscreenPopup<Int, PopupContent>(
42
+ isPresented: isPresented,
43
+ type: type,
44
+ position: position,
45
+ animation: animation,
46
+ autohideIn: autohideIn,
47
+ dragToDismiss: dragToDismiss,
48
+ closeOnTapOutside: closeOnTapOutside,
49
+ backgroundColor: backgroundColor,
50
+ dismissCallback: { _ in },
51
+ view: view)
52
+ )
53
+ }
54
+
55
+ public func popup<Item: Equatable, PopupContent: View>(
56
+ item: Binding<Item?>,
57
+ type: Popup<Item, PopupContent>.PopupType = .`default`,
58
+ position: Popup<Item, PopupContent>.Position = .bottom,
59
+ animation: Animation = Animation.easeOut(duration: 0.3),
60
+ autohideIn: Double? = nil,
61
+ dragToDismiss: Bool = true,
62
+ closeOnTapOutside: Bool = false,
63
+ backgroundColor: Color = Color.clear,
64
+ dismissCallback: @escaping () -> (),
65
+ @ViewBuilder view: @escaping () -> PopupContent) -> some View {
66
+ self.modifier(
67
+ FullscreenPopup(
68
+ item: item,
69
+ type: type,
70
+ position: position,
71
+ animation: animation,
72
+ autohideIn: autohideIn,
73
+ dragToDismiss: dragToDismiss,
74
+ closeOnTapOutside: closeOnTapOutside,
75
+ backgroundColor: backgroundColor,
76
+ dismissCallback: { _ in dismissCallback() },
77
+ view: view)
78
+ )
79
+ }
80
+
81
+ public func popup<PopupContent: View>(
82
+ isPresented: Binding<Bool>,
83
+ type: Popup<Int, PopupContent>.PopupType = .`default`,
84
+ position: Popup<Int, PopupContent>.Position = .bottom,
85
+ animation: Animation = Animation.easeOut(duration: 0.3),
86
+ autohideIn: Double? = nil,
87
+ dragToDismiss: Bool = true,
88
+ closeOnTapOutside: Bool = false,
89
+ backgroundColor: Color = Color.clear,
90
+ dismissCallback: @escaping () -> (),
91
+ @ViewBuilder view: @escaping () -> PopupContent) -> some View {
92
+ self.modifier(
93
+ FullscreenPopup<Int, PopupContent>(
94
+ isPresented: isPresented,
95
+ type: type,
96
+ position: position,
97
+ animation: animation,
98
+ autohideIn: autohideIn,
99
+ dragToDismiss: dragToDismiss,
100
+ closeOnTapOutside: closeOnTapOutside,
101
+ backgroundColor: backgroundColor,
102
+ dismissCallback: { _ in dismissCallback() },
103
+ view: view)
104
+ )
105
+ }
106
+
107
+ public func popup<Item: Equatable, PopupContent: View>(
108
+ item: Binding<Item?>,
109
+ type: Popup<Item, PopupContent>.PopupType = .`default`,
110
+ position: Popup<Item, PopupContent>.Position = .bottom,
111
+ animation: Animation = Animation.easeOut(duration: 0.3),
112
+ autohideIn: Double? = nil,
113
+ dragToDismiss: Bool = true,
114
+ closeOnTapOutside: Bool = false,
115
+ backgroundColor: Color = Color.clear,
116
+ dismissSourceCallback: @escaping (DismissSource) -> (),
117
+ @ViewBuilder view: @escaping () -> PopupContent) -> some View {
118
+ self.modifier(
119
+ FullscreenPopup(
120
+ item: item,
121
+ type: type,
122
+ position: position,
123
+ animation: animation,
124
+ autohideIn: autohideIn,
125
+ dragToDismiss: dragToDismiss,
126
+ closeOnTapOutside: closeOnTapOutside,
127
+ backgroundColor: backgroundColor,
128
+ dismissCallback: dismissSourceCallback,
129
+ view: view)
130
+ )
131
+ }
132
+
133
+ public func popup<PopupContent: View>(
134
+ isPresented: Binding<Bool>,
135
+ type: Popup<Int, PopupContent>.PopupType = .`default`,
136
+ position: Popup<Int, PopupContent>.Position = .bottom,
137
+ animation: Animation = Animation.easeOut(duration: 0.3),
138
+ autohideIn: Double? = nil,
139
+ dragToDismiss: Bool = true,
140
+ closeOnTapOutside: Bool = false,
141
+ backgroundColor: Color = Color.clear,
142
+ dismissSourceCallback: @escaping (DismissSource) -> (),
143
+ @ViewBuilder view: @escaping () -> PopupContent) -> some View {
144
+ self.modifier(
145
+ FullscreenPopup<Int, PopupContent>(
146
+ isPresented: isPresented,
147
+ type: type,
148
+ position: position,
149
+ animation: animation,
150
+ autohideIn: autohideIn,
151
+ dragToDismiss: dragToDismiss,
152
+ closeOnTapOutside: closeOnTapOutside,
153
+ backgroundColor: backgroundColor,
154
+ dismissCallback: dismissSourceCallback,
155
+ view: view)
156
+ )
157
+ }
158
+ }
@@ -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
+ }