@momo-kits/native-kits 0.0.1-beta.2
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/CODE_OF_CONDUCT.md +133 -0
- package/CONTRIBUTING.md +114 -0
- package/LICENSE +20 -0
- package/README.md +6 -0
- package/android/build.gradle +82 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/momoplatform/nativekits/NativeKitsPackage.kt +17 -0
- package/android/src/main/java/com/momoplatform/nativekits/NativeKitsViewManager.kt +20 -0
- package/example/ios/Example/Assets.xcassets/AppIcon.appiconset/Contents.json +63 -0
- package/example/ios/Example/Assets.xcassets/Background.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/Border.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/Card.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/Contents.json +6 -0
- package/example/ios/Example/Assets.xcassets/Error.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/Primary.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/PrimaryDark.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/PrimaryLight.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/Secondary.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/Success.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/Text.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/TextSecondary.colorset/Contents.json +38 -0
- package/example/ios/Example/Assets.xcassets/Warning.colorset/Contents.json +38 -0
- package/example/ios/Example/ComponentDetail.swift +35 -0
- package/example/ios/Example/ComponentItem.swift +24 -0
- package/example/ios/Example/ComponentModel.swift +31 -0
- package/example/ios/Example/ContentView.swift +53 -0
- package/example/ios/Example/Example.entitlements +10 -0
- package/example/ios/Example/Foundation/ButtonDemo.swift +80 -0
- package/example/ios/Example/Foundation/Popup/ActionSheets.swift +136 -0
- package/example/ios/Example/Foundation/Popup/Floats.swift +127 -0
- package/example/ios/Example/Foundation/Popup/Popups.swift +101 -0
- package/example/ios/Example/Foundation/PopupDemo.swift +366 -0
- package/example/ios/Example/Foundation/TypographyDemo.swift +53 -0
- package/example/ios/Example/Foundation.swift +26 -0
- package/example/ios/Example/Info.plist +18 -0
- package/example/ios/Example/Library/SwipeableDemo.swift +105 -0
- package/example/ios/Example/Library.swift +26 -0
- package/example/ios/Example/Main.swift +11 -0
- package/example/ios/Example/Overview.swift +20 -0
- package/example/ios/Example/Preview Content/Preview Assets.xcassets/Contents.json +6 -0
- package/example/ios/Example/Resource/SFProText-Black.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Bold.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Heavy.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Light.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Medium.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Regular.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Semibold.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Thin.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Ultralight.ttf +0 -0
- package/example/ios/Example/Resource/foundation.json +33 -0
- package/example/ios/Example/Resource/library.json +8 -0
- package/example/ios/Example/Utils.swift +30 -0
- package/example/ios/Example/ViewModifier/DarkViewModifier.swift +22 -0
- package/example/ios/Example.xcodeproj/project.pbxproj +565 -0
- package/example/ios/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/example/ios/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/example/ios/Example.xcodeproj/project.xcworkspace/xcuserdata/wem.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/example/ios/Example.xcworkspace/contents.xcworkspacedata +10 -0
- package/example/ios/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/example/ios/Example.xcworkspace/xcuserdata/wem.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcworkspace/xcuserdata/wem.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +6 -0
- package/example/ios/Podfile +6 -0
- package/example/ios/Podfile.lock +16 -0
- package/example/ios/Pods/Local Podspecs/MoMoUIKits.podspec.json +29 -0
- package/example/ios/Pods/Manifest.lock +16 -0
- package/example/ios/Pods/Pods.xcodeproj/project.pbxproj +661 -0
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +58 -0
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/Pods-Example.xcscheme +58 -0
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/xcschememanagement.plist +25 -0
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits-dummy.m +5 -0
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits-prefix.pch +12 -0
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits-umbrella.h +16 -0
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits.debug.xcconfig +12 -0
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits.modulemap +6 -0
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits.release.xcconfig +12 -0
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.markdown +3 -0
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.plist +29 -0
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-dummy.m +5 -0
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-umbrella.h +16 -0
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example.debug.xcconfig +14 -0
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example.modulemap +6 -0
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example.release.xcconfig +14 -0
- package/ios/Button/Button.swift +131 -0
- package/ios/Colors+Spacing/Colors.swift +170 -0
- package/ios/Colors+Spacing/Radius.swift +11 -0
- package/ios/Colors+Spacing/Spacing.swift +12 -0
- package/ios/Extensions/Color++.swift +25 -0
- package/ios/OTPKeyboard/KeyboardButton.swift +33 -0
- package/ios/OTPKeyboard/OTPKeyboard.swift +145 -0
- package/ios/Popup/Modifiers.swift +172 -0
- package/ios/Popup/PopupView.swift +397 -0
- package/ios/Popup/Utils.swift +186 -0
- package/ios/Swipeable/SwipeCell.swift +270 -0
- package/ios/Swipeable/SwipeCellModel.swift +86 -0
- package/ios/Theme.md +18 -0
- package/ios/Typography/Typography.swift +30 -0
- package/ios/native-kits.podspec +15 -0
- package/package.json +8 -0
- package/publish.sh +28 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import SwiftUI
|
|
4
|
+
|
|
5
|
+
public enum DismissSource {
|
|
6
|
+
case binding // set isPresented to false ot item to nil
|
|
7
|
+
case tapInside
|
|
8
|
+
case tapOutside
|
|
9
|
+
case drag
|
|
10
|
+
case autohide
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public struct Popup<Item: Equatable, PopupContent: View>: ViewModifier {
|
|
14
|
+
|
|
15
|
+
init(isPresented: Binding<Bool>,
|
|
16
|
+
type: PopupType,
|
|
17
|
+
position: Position,
|
|
18
|
+
animation: Animation,
|
|
19
|
+
autohideIn: Double?,
|
|
20
|
+
dragToDismiss: Bool,
|
|
21
|
+
closeOnTap: Bool,
|
|
22
|
+
closeOnTapOutside: Bool,
|
|
23
|
+
backgroundColor: Color,
|
|
24
|
+
dismissCallback: @escaping (DismissSource) -> (),
|
|
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.closeOnTap = closeOnTap
|
|
34
|
+
self.closeOnTapOutside = closeOnTapOutside
|
|
35
|
+
self.backgroundColor = backgroundColor
|
|
36
|
+
self.dismissCallback = dismissCallback
|
|
37
|
+
self.view = view
|
|
38
|
+
self.isPresentedRef = ClassReference(self.$isPresented)
|
|
39
|
+
self.itemRef = ClassReference(self.$item)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
init(item: Binding<Item?>,
|
|
43
|
+
type: PopupType,
|
|
44
|
+
position: Position,
|
|
45
|
+
animation: Animation,
|
|
46
|
+
autohideIn: Double?,
|
|
47
|
+
dragToDismiss: Bool,
|
|
48
|
+
closeOnTap: Bool,
|
|
49
|
+
closeOnTapOutside: Bool,
|
|
50
|
+
backgroundColor: Color,
|
|
51
|
+
dismissCallback: @escaping (DismissSource) -> (),
|
|
52
|
+
view: @escaping () -> PopupContent) {
|
|
53
|
+
self._isPresented = .constant(false)
|
|
54
|
+
self._item = item
|
|
55
|
+
self.type = type
|
|
56
|
+
self.position = position
|
|
57
|
+
self.animation = animation
|
|
58
|
+
self.autohideIn = autohideIn
|
|
59
|
+
self.dragToDismiss = dragToDismiss
|
|
60
|
+
self.closeOnTap = closeOnTap
|
|
61
|
+
self.closeOnTapOutside = closeOnTapOutside
|
|
62
|
+
self.backgroundColor = backgroundColor
|
|
63
|
+
self.dismissCallback = dismissCallback
|
|
64
|
+
self.view = view
|
|
65
|
+
self.isPresentedRef = ClassReference(self.$isPresented)
|
|
66
|
+
self.itemRef = ClassReference(self.$item)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public enum PopupType {
|
|
70
|
+
|
|
71
|
+
case `default`
|
|
72
|
+
case toast
|
|
73
|
+
case floater(verticalPadding: CGFloat = 10, useSafeAreaInset: Bool = true)
|
|
74
|
+
|
|
75
|
+
func shouldBeCentered() -> Bool {
|
|
76
|
+
switch self {
|
|
77
|
+
case .`default`:
|
|
78
|
+
return true
|
|
79
|
+
default:
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public enum Position {
|
|
86
|
+
case top
|
|
87
|
+
case bottom
|
|
88
|
+
case center
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private enum DragState {
|
|
92
|
+
case inactive
|
|
93
|
+
case dragging(translation: CGSize)
|
|
94
|
+
|
|
95
|
+
var translation: CGSize {
|
|
96
|
+
switch self {
|
|
97
|
+
case .inactive:
|
|
98
|
+
return .zero
|
|
99
|
+
case .dragging(let translation):
|
|
100
|
+
return translation
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var isDragging: Bool {
|
|
105
|
+
switch self {
|
|
106
|
+
case .inactive:
|
|
107
|
+
return false
|
|
108
|
+
case .dragging:
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: - Public Properties
|
|
115
|
+
|
|
116
|
+
/// Tells if the sheet should be presented or not
|
|
117
|
+
@Binding var isPresented: Bool
|
|
118
|
+
@Binding var item: Item?
|
|
119
|
+
|
|
120
|
+
var sheetPresented: Bool {
|
|
121
|
+
item != nil || isPresented
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var type: PopupType
|
|
125
|
+
var position: Position
|
|
126
|
+
|
|
127
|
+
var animation: Animation
|
|
128
|
+
|
|
129
|
+
/// If nil - never hides on its own
|
|
130
|
+
var autohideIn: Double?
|
|
131
|
+
|
|
132
|
+
/// Should close on tap - default is `true`
|
|
133
|
+
var closeOnTap: Bool
|
|
134
|
+
|
|
135
|
+
/// Should allow dismiss by dragging
|
|
136
|
+
var dragToDismiss: Bool
|
|
137
|
+
|
|
138
|
+
/// Should close on tap outside - default is `true`
|
|
139
|
+
var closeOnTapOutside: Bool
|
|
140
|
+
|
|
141
|
+
/// Background color for outside area - default is `Color.clear`
|
|
142
|
+
var backgroundColor: Color
|
|
143
|
+
|
|
144
|
+
/// is called on any close action
|
|
145
|
+
var dismissCallback: (DismissSource) -> ()
|
|
146
|
+
|
|
147
|
+
var view: () -> PopupContent
|
|
148
|
+
|
|
149
|
+
/// holder for autohiding dispatch work (to be able to cancel it when needed)
|
|
150
|
+
var dispatchWorkHolder = DispatchWorkHolder()
|
|
151
|
+
|
|
152
|
+
// MARK: - Private Properties
|
|
153
|
+
|
|
154
|
+
@Environment(\.safeAreaInsets) private var safeAreaInsets
|
|
155
|
+
|
|
156
|
+
/// Class reference for capturing a weak reference later in dispatch work holder.
|
|
157
|
+
private var isPresentedRef: ClassReference<Binding<Bool>>?
|
|
158
|
+
private var itemRef: ClassReference<Binding<Item?>>?
|
|
159
|
+
|
|
160
|
+
/// The rect and safe area of the hosting controller
|
|
161
|
+
@State private var presenterContentRect: CGRect = .zero
|
|
162
|
+
|
|
163
|
+
/// The rect and safe area of popup content
|
|
164
|
+
@State private var sheetContentRect: CGRect = .zero
|
|
165
|
+
|
|
166
|
+
/// Drag to dismiss gesture state
|
|
167
|
+
@GestureState private var dragState = DragState.inactive
|
|
168
|
+
|
|
169
|
+
/// Last position for drag gesture
|
|
170
|
+
@State private var lastDragPosition: CGFloat = 0
|
|
171
|
+
|
|
172
|
+
/// Trigger popup showing/hiding animations and...
|
|
173
|
+
@State private var shouldShowContent: Bool = false
|
|
174
|
+
|
|
175
|
+
/// ... once hiding animation is finished remove popup from the memory using this flag
|
|
176
|
+
@State private var showContent: Bool = false
|
|
177
|
+
|
|
178
|
+
/// Set dismiss souce to pass to dismiss callback
|
|
179
|
+
@State private var dismissSource: DismissSource?
|
|
180
|
+
|
|
181
|
+
/// The offset when the popup is displayed
|
|
182
|
+
private var displayedOffset: CGFloat {
|
|
183
|
+
switch type {
|
|
184
|
+
case .`default`:
|
|
185
|
+
return -presenterContentRect.midY + screenHeight/2
|
|
186
|
+
case .toast:
|
|
187
|
+
if position == .bottom {
|
|
188
|
+
return presenterContentRect.minY + safeAreaInsets.bottom + presenterContentRect.height - presenterContentRect.midY - sheetContentRect.height/2
|
|
189
|
+
} else {
|
|
190
|
+
return presenterContentRect.minY - safeAreaInsets.top - presenterContentRect.midY + sheetContentRect.height/2
|
|
191
|
+
}
|
|
192
|
+
case .floater(let verticalPadding, let useSafeAreaInset):
|
|
193
|
+
if position == .bottom {
|
|
194
|
+
return presenterContentRect.minY + safeAreaInsets.bottom + presenterContentRect.height - presenterContentRect.midY - sheetContentRect.height/2 - verticalPadding + (useSafeAreaInset ? -safeAreaInsets.bottom : 0)
|
|
195
|
+
} else {
|
|
196
|
+
return presenterContentRect.minY - safeAreaInsets.top - presenterContentRect.midY + sheetContentRect.height/2 + verticalPadding + (useSafeAreaInset ? safeAreaInsets.top : 0)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// The offset when the popup is hidden
|
|
202
|
+
private var hiddenOffset: CGFloat {
|
|
203
|
+
if position == .center {
|
|
204
|
+
return displayedOffset
|
|
205
|
+
} else if position == .top {
|
|
206
|
+
if presenterContentRect.isEmpty {
|
|
207
|
+
return -1000
|
|
208
|
+
}
|
|
209
|
+
return -presenterContentRect.midY - sheetContentRect.height/2 - 5
|
|
210
|
+
} else {
|
|
211
|
+
if presenterContentRect.isEmpty {
|
|
212
|
+
return 1000
|
|
213
|
+
}
|
|
214
|
+
return screenHeight - presenterContentRect.midY + sheetContentRect.height/2 + 5
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// The current offset, based on the **presented** property
|
|
219
|
+
private var currentOffset: CGFloat {
|
|
220
|
+
return shouldShowContent ? displayedOffset : hiddenOffset
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// The current background opacity, based on the **presented** property
|
|
224
|
+
private var currentBackgroundOpacity: Double {
|
|
225
|
+
return shouldShowContent ? 1.0 : 0.0
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private var screenSize: CGSize {
|
|
229
|
+
#if os(iOS) || os(tvOS)
|
|
230
|
+
return UIScreen.main.bounds.size
|
|
231
|
+
#elseif os(watchOS)
|
|
232
|
+
return WKInterfaceDevice.current().screenBounds.size
|
|
233
|
+
#else
|
|
234
|
+
return NSScreen.main?.frame.size ?? .zero
|
|
235
|
+
#endif
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private var screenHeight: CGFloat {
|
|
239
|
+
screenSize.height
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// MARK: - Content Builders
|
|
243
|
+
|
|
244
|
+
public func body(content: Content) -> some View {
|
|
245
|
+
main(content: content)
|
|
246
|
+
.onAppear {
|
|
247
|
+
appearAction(sheetPresented: sheetPresented)
|
|
248
|
+
}
|
|
249
|
+
.valueChanged(value: isPresented) { isPresented in
|
|
250
|
+
appearAction(sheetPresented: isPresented)
|
|
251
|
+
if isPresented {
|
|
252
|
+
dismissSource = .binding
|
|
253
|
+
}
|
|
254
|
+
if !isPresented {
|
|
255
|
+
dismissCallback(dismissSource ?? .autohide)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
.valueChanged(value: item) { item in
|
|
259
|
+
appearAction(sheetPresented: item != nil)
|
|
260
|
+
if item != nil {
|
|
261
|
+
dismissSource = .binding
|
|
262
|
+
}
|
|
263
|
+
if item == nil {
|
|
264
|
+
dismissCallback(dismissSource ?? .autohide)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private func main(content: Content) -> some View {
|
|
270
|
+
ZStack {
|
|
271
|
+
content
|
|
272
|
+
.frameGetter($presenterContentRect)
|
|
273
|
+
|
|
274
|
+
if showContent {
|
|
275
|
+
popupBackground()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
.overlay(
|
|
279
|
+
Group {
|
|
280
|
+
if showContent {
|
|
281
|
+
sheet()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private func popupBackground() -> some View {
|
|
288
|
+
backgroundColor
|
|
289
|
+
.applyIf(closeOnTapOutside) { view in
|
|
290
|
+
view.contentShape(Rectangle())
|
|
291
|
+
}
|
|
292
|
+
.addTapIfNotTV(if: closeOnTapOutside) {
|
|
293
|
+
dismissSource = .tapOutside
|
|
294
|
+
dismiss()
|
|
295
|
+
}
|
|
296
|
+
.edgesIgnoringSafeArea(.all)
|
|
297
|
+
.opacity(currentBackgroundOpacity)
|
|
298
|
+
.animation(.linear)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// This is the builder for the sheet content
|
|
302
|
+
func sheet() -> some View {
|
|
303
|
+
|
|
304
|
+
// if needed, dispatch autohide and cancel previous one
|
|
305
|
+
if let autohideIn = autohideIn {
|
|
306
|
+
dispatchWorkHolder.work?.cancel()
|
|
307
|
+
|
|
308
|
+
// Weak reference to avoid the work item capturing the struct,
|
|
309
|
+
// which would create a retain cycle with the work holder itself.
|
|
310
|
+
|
|
311
|
+
dispatchWorkHolder.work = DispatchWorkItem(block: { [weak isPresentedRef, weak itemRef] in
|
|
312
|
+
isPresentedRef?.value.wrappedValue = false
|
|
313
|
+
itemRef?.value.wrappedValue = nil
|
|
314
|
+
dismissSource = .autohide
|
|
315
|
+
})
|
|
316
|
+
if sheetPresented, let work = dispatchWorkHolder.work {
|
|
317
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + autohideIn, execute: work)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let sheet = ZStack {
|
|
322
|
+
self.view()
|
|
323
|
+
.addTapIfNotTV(if: closeOnTap) {
|
|
324
|
+
dismissSource = .tapInside
|
|
325
|
+
dismiss()
|
|
326
|
+
}
|
|
327
|
+
.frameGetter($sheetContentRect)
|
|
328
|
+
.offset(y: currentOffset)
|
|
329
|
+
.onAnimationCompleted(for: currentOffset) {
|
|
330
|
+
showContent = shouldShowContent
|
|
331
|
+
}
|
|
332
|
+
.animation(animation)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#if !os(tvOS)
|
|
336
|
+
let drag = DragGesture()
|
|
337
|
+
.updating($dragState) { drag, state, _ in
|
|
338
|
+
state = .dragging(translation: drag.translation)
|
|
339
|
+
}
|
|
340
|
+
.onEnded(onDragEnded)
|
|
341
|
+
|
|
342
|
+
return sheet
|
|
343
|
+
.applyIf(dragToDismiss) {
|
|
344
|
+
$0.offset(y: dragOffset())
|
|
345
|
+
.simultaneousGesture(drag)
|
|
346
|
+
}
|
|
347
|
+
#else
|
|
348
|
+
return sheet
|
|
349
|
+
#endif
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#if !os(tvOS)
|
|
353
|
+
func dragOffset() -> CGFloat {
|
|
354
|
+
if (position == .bottom && dragState.translation.height > 0) ||
|
|
355
|
+
(position == .top && dragState.translation.height < 0) {
|
|
356
|
+
return dragState.translation.height
|
|
357
|
+
}
|
|
358
|
+
return lastDragPosition
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private func onDragEnded(drag: DragGesture.Value) {
|
|
362
|
+
let reference = sheetContentRect.height / 3
|
|
363
|
+
if (position == .bottom && drag.translation.height > reference) ||
|
|
364
|
+
(position == .top && drag.translation.height < -reference) {
|
|
365
|
+
lastDragPosition = drag.translation.height
|
|
366
|
+
withAnimation {
|
|
367
|
+
lastDragPosition = 0
|
|
368
|
+
}
|
|
369
|
+
dismissSource = .drag
|
|
370
|
+
dismiss()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
#endif
|
|
374
|
+
|
|
375
|
+
private func appearAction(sheetPresented: Bool) {
|
|
376
|
+
if sheetPresented {
|
|
377
|
+
showContent = true // immediately load popup body
|
|
378
|
+
DispatchQueue.main.async {
|
|
379
|
+
shouldShowContent = true // this will cause currentOffset change thus triggering the showing animation
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
shouldShowContent = false // this will cause currentOffset change thus triggering the hiding animation
|
|
383
|
+
// unload popup body after hiding animation is done (see sheet())
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private func dismiss() {
|
|
388
|
+
dispatchWorkHolder.work?.cancel()
|
|
389
|
+
isPresented = false
|
|
390
|
+
item = nil
|
|
391
|
+
if position == .center {
|
|
392
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
393
|
+
showContent = false
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import SwiftUI
|
|
4
|
+
import Combine
|
|
5
|
+
|
|
6
|
+
final class DispatchWorkHolder {
|
|
7
|
+
var work: DispatchWorkItem?
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
final class ClassReference<T> {
|
|
11
|
+
var value: T
|
|
12
|
+
|
|
13
|
+
init(_ value: T) {
|
|
14
|
+
self.value = value
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
extension View {
|
|
19
|
+
|
|
20
|
+
@ViewBuilder
|
|
21
|
+
func valueChanged<T: Equatable>(value: T, onChange: @escaping (T) -> Void) -> some View {
|
|
22
|
+
if #available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *) {
|
|
23
|
+
self.onChange(of: value, perform: onChange)
|
|
24
|
+
} else {
|
|
25
|
+
self.onReceive(Just(value)) { value in
|
|
26
|
+
onChange(value)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public extension View {
|
|
33
|
+
@ViewBuilder
|
|
34
|
+
func applyIf<T: View>(_ condition: Bool, apply: (Self) -> T) -> some View {
|
|
35
|
+
if condition {
|
|
36
|
+
apply(self)
|
|
37
|
+
} else {
|
|
38
|
+
self
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@ViewBuilder
|
|
43
|
+
func addTapIfNotTV(if condition: Bool, onTap: @escaping ()->()) -> some View {
|
|
44
|
+
#if os(tvOS)
|
|
45
|
+
self
|
|
46
|
+
#else
|
|
47
|
+
if condition {
|
|
48
|
+
self.simultaneousGesture(
|
|
49
|
+
TapGesture().onEnded {
|
|
50
|
+
onTap()
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
} else {
|
|
54
|
+
self
|
|
55
|
+
}
|
|
56
|
+
#endif
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
struct FrameGetter: ViewModifier {
|
|
61
|
+
|
|
62
|
+
@Binding var frame: CGRect
|
|
63
|
+
|
|
64
|
+
func body(content: Content) -> some View {
|
|
65
|
+
content
|
|
66
|
+
.background(
|
|
67
|
+
GeometryReader { proxy -> AnyView in
|
|
68
|
+
DispatchQueue.main.async {
|
|
69
|
+
let rect = proxy.frame(in: .global)
|
|
70
|
+
// This avoids an infinite layout loop
|
|
71
|
+
if rect.integral != self.frame.integral {
|
|
72
|
+
self.frame = rect
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return AnyView(EmptyView())
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
extension View {
|
|
82
|
+
public func frameGetter(_ frame: Binding<CGRect>) -> some View {
|
|
83
|
+
modifier(FrameGetter(frame: frame))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
struct AnimationCompletionObserverModifier<Value>: AnimatableModifier where Value: VectorArithmetic {
|
|
88
|
+
|
|
89
|
+
/// 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.
|
|
90
|
+
var animatableData: Value {
|
|
91
|
+
didSet {
|
|
92
|
+
notifyCompletionIfFinished()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// 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.
|
|
97
|
+
private var targetValue: Value
|
|
98
|
+
|
|
99
|
+
/// The completion callback which is called once the animation completes.
|
|
100
|
+
private var completion: () -> Void
|
|
101
|
+
|
|
102
|
+
init(observedValue: Value, completion: @escaping () -> Void) {
|
|
103
|
+
self.completion = completion
|
|
104
|
+
self.animatableData = observedValue
|
|
105
|
+
targetValue = observedValue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Verifies whether the current animation is finished and calls the completion callback if true.
|
|
109
|
+
private func notifyCompletionIfFinished() {
|
|
110
|
+
guard animatableData == targetValue else { return }
|
|
111
|
+
|
|
112
|
+
/// Dispatching is needed to take the next runloop for the completion callback.
|
|
113
|
+
/// This prevents errors like "Modifying state during view update, this will cause undefined behavior."
|
|
114
|
+
DispatchQueue.main.async {
|
|
115
|
+
self.completion()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func body(content: Content) -> some View {
|
|
120
|
+
/// We're not really modifying the view so we can directly return the original input value.
|
|
121
|
+
return content
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
extension View {
|
|
126
|
+
|
|
127
|
+
/// Calls the completion handler whenever an animation on the given value completes.
|
|
128
|
+
/// - Parameters:
|
|
129
|
+
/// - value: The value to observe for animations.
|
|
130
|
+
/// - completion: The completion callback to call once the animation completes.
|
|
131
|
+
/// - Returns: A modified `View` instance with the observer attached.
|
|
132
|
+
func onAnimationCompleted<Value: VectorArithmetic>(for value: Value, completion: @escaping () -> Void) -> ModifiedContent<Self, AnimationCompletionObserverModifier<Value>> {
|
|
133
|
+
return modifier(AnimationCompletionObserverModifier(observedValue: value, completion: completion))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#if os(iOS)
|
|
138
|
+
|
|
139
|
+
extension UIApplication {
|
|
140
|
+
var keyWindow: UIWindow? {
|
|
141
|
+
connectedScenes
|
|
142
|
+
.compactMap {
|
|
143
|
+
$0 as? UIWindowScene
|
|
144
|
+
}
|
|
145
|
+
.flatMap {
|
|
146
|
+
$0.windows
|
|
147
|
+
}
|
|
148
|
+
.first {
|
|
149
|
+
$0.isKeyWindow
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private struct SafeAreaInsetsKey: EnvironmentKey {
|
|
155
|
+
static var defaultValue: EdgeInsets {
|
|
156
|
+
UIApplication.shared.keyWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
extension EnvironmentValues {
|
|
161
|
+
var safeAreaInsets: EdgeInsets {
|
|
162
|
+
self[SafeAreaInsetsKey.self]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private extension UIEdgeInsets {
|
|
167
|
+
var swiftUiInsets: EdgeInsets {
|
|
168
|
+
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#else
|
|
173
|
+
|
|
174
|
+
private struct SafeAreaInsetsKey: EnvironmentKey {
|
|
175
|
+
static var defaultValue: EdgeInsets {
|
|
176
|
+
EdgeInsets()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
extension EnvironmentValues {
|
|
181
|
+
var safeAreaInsets: EdgeInsets {
|
|
182
|
+
self[SafeAreaInsetsKey.self]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#endif
|