@momo-kits/native-kits 0.152.4-beta.6 → 0.152.4-maxapi
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 +7 -0
- package/build.gradle.kts +32 -0
- package/compose/MoMoComposeKits.podspec +54 -0
- package/compose/build.gradle.kts +149 -0
- package/compose/src/androidMain/AndroidManifest.xml +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +105 -0
- package/compose/src/commonMain/composeResources/files/lottie_circle_loader.json +1 -0
- package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
- package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +57 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +201 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +222 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +48 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +715 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +214 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +236 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +77 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +27 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +334 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +345 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +90 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +131 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +143 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +179 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +111 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +384 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +160 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +234 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +223 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +236 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +228 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +364 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +50 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +34 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +85 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +95 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +64 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +89 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +84 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +208 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +172 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +199 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +237 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +191 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +13 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +191 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +258 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +59 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +68 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +49 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +111 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +94 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +159 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScaleSizeScope.kt +17 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +459 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +216 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +180 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +251 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +80 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +31 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +385 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +38 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +88 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +144 -0
- package/gradle.properties +19 -0
- package/gradlew +240 -0
- package/gradlew.bat +91 -0
- package/ios/Application/ApplicationEnvironment.swift +50 -0
- package/ios/Application/Components.swift +263 -0
- package/ios/Application/ComposeApi.swift +22 -0
- package/ios/Application/FloatingButton.swift +172 -0
- package/ios/Application/HeaderRight.swift +271 -0
- package/ios/Application/Screen.swift +249 -0
- package/ios/Badge/BadgeDot.swift +31 -0
- package/ios/Button/Button.swift +211 -0
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +126 -0
- package/ios/Checkbox/Checkbox.swift +81 -0
- package/ios/Chip/Chip.swift +96 -0
- package/ios/Colors+Radius+Spacing/Colors.swift +172 -0
- package/ios/Colors+Radius+Spacing/Radius.swift +22 -0
- package/ios/Colors+Radius+Spacing/Spacing.swift +12 -0
- package/ios/Extensions/Color++.swift +25 -0
- package/ios/Icon/Icon.swift +51 -0
- package/ios/Image/Image.swift +70 -0
- package/ios/Input/Input.swift +207 -0
- package/ios/Input/InputPhoneNumber.swift +176 -0
- package/ios/Input/InputSearch.swift +238 -0
- package/ios/Input/InputTextArea.swift +242 -0
- package/ios/Lottie/LottieView.swift +86 -0
- package/ios/OTPKeyboard/KeyboardButton.swift +41 -0
- package/ios/OTPKeyboard/OTPKeyboard.swift +145 -0
- package/ios/Popup/PopupDisplay.swift +284 -0
- package/ios/Popup/PopupInput.swift +96 -0
- package/ios/Popup/PopupPromotion.swift +73 -0
- package/ios/PopupView/FullscreenPopup.swift +251 -0
- package/ios/PopupView/Modifiers.swift +158 -0
- package/ios/PopupView/PopupView.swift +289 -0
- package/ios/PopupView/Utils++.swift +281 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +110 -0
- package/ios/Swipeable/SwipeCell.swift +278 -0
- package/ios/Swipeable/SwipeCellModel.swift +86 -0
- package/ios/Switch/Switch.swift +44 -0
- package/ios/Template/Logo/Logo.swift +75 -0
- package/ios/Template/TrustBanner/TrustBanner.swift +120 -0
- package/ios/Theme.md +18 -0
- package/ios/Typography/Text.swift +140 -0
- package/ios/Typography/Typography.swift +95 -0
- package/ios/native-kits.podspec +18 -0
- package/package.json +6 -7
- package/settings.gradle.kts +25 -0
- package/shared/build.gradle.kts +0 -74
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
// MARK: - SwipeCellModifier
|
|
5
|
+
|
|
6
|
+
public struct SwipeCellModifier: ViewModifier {
|
|
7
|
+
var id: String
|
|
8
|
+
var cellWidth: CGFloat = .infinity
|
|
9
|
+
var leadingSideGroup: [SwipeCellActionItem] = []
|
|
10
|
+
var trailingSideGroup: [SwipeCellActionItem] = []
|
|
11
|
+
@Binding var currentUserInteractionCellID: String?
|
|
12
|
+
var settings: SwipeCellSettings = .init()
|
|
13
|
+
|
|
14
|
+
@State private var offsetX: CGFloat = 0
|
|
15
|
+
|
|
16
|
+
let generator = UINotificationFeedbackGenerator()
|
|
17
|
+
@State private var hapticFeedbackOccurred: Bool = false
|
|
18
|
+
@State private var openSideLock: SwipeGroupSide?
|
|
19
|
+
|
|
20
|
+
public func body(content: Content)->some View {
|
|
21
|
+
ZStack {
|
|
22
|
+
if self.leadingSideGroup.isEmpty == false && self.offsetX != 0 {
|
|
23
|
+
self.swipeToRevealArea(swipeItemGroup: self.leadingSideGroup, side: .leading)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if self.trailingSideGroup.isEmpty == false && self.offsetX != 0 {
|
|
27
|
+
self.swipeToRevealArea(swipeItemGroup: self.trailingSideGroup, side: .trailing)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
content
|
|
31
|
+
.offset(x: self.offsetX)
|
|
32
|
+
.simultaneousGesture(
|
|
33
|
+
DragGesture(minimumDistance: 30, coordinateSpace: .local)
|
|
34
|
+
.onChanged { value in
|
|
35
|
+
self.dragOnChanged(value: value)
|
|
36
|
+
}
|
|
37
|
+
.onEnded { value in
|
|
38
|
+
self.dragOnEnded(value: value)
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
}.frame(width: cellWidth)
|
|
43
|
+
.edgesIgnoringSafeArea(.horizontal)
|
|
44
|
+
.clipped()
|
|
45
|
+
.valueChanged(value: self.currentUserInteractionCellID) { _ in
|
|
46
|
+
if let currentDragCellID = self.currentUserInteractionCellID, currentDragCellID != self.id && self.openSideLock != nil {
|
|
47
|
+
// if this cell has an open side area and is not the cell being dragged, close the cell
|
|
48
|
+
self.setOffsetX(value: 0)
|
|
49
|
+
// reset the drag cell id to nil
|
|
50
|
+
self.currentUserInteractionCellID = nil
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
internal func swipeToRevealArea(swipeItemGroup: [SwipeCellActionItem], side: SwipeGroupSide)->some View {
|
|
56
|
+
HStack {
|
|
57
|
+
if side == .trailing {
|
|
58
|
+
Spacer()
|
|
59
|
+
}
|
|
60
|
+
ZStack {
|
|
61
|
+
HStack(spacing: 0) {
|
|
62
|
+
ForEach(swipeItemGroup) { item in
|
|
63
|
+
SwiftUI.Button {
|
|
64
|
+
self.setOffsetX(value: 0)
|
|
65
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
66
|
+
item.actionCallback()
|
|
67
|
+
}
|
|
68
|
+
} label: {
|
|
69
|
+
self.buttonContentView(item: item, group: swipeItemGroup, side: side)
|
|
70
|
+
}.buttonStyle(BorderlessButtonStyle())
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}.opacity(self.swipeRevealAreaOpacity(side: side))
|
|
74
|
+
|
|
75
|
+
if side == .leading {
|
|
76
|
+
Spacer()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
internal func buttonContentView(item: SwipeCellActionItem, group: [SwipeCellActionItem], side: SwipeGroupSide)->some View {
|
|
82
|
+
ZStack {
|
|
83
|
+
item.backgroundColor
|
|
84
|
+
|
|
85
|
+
HStack {
|
|
86
|
+
if self.warnSwipeOutCondition(side: side, hasSwipeOut: item.swipeOutAction) && item.swipeOutButtonView != nil {
|
|
87
|
+
item.swipeOutButtonView!()
|
|
88
|
+
} else {
|
|
89
|
+
item.buttonView()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}.frame(width: self.itemButtonWidth(item: item, itemGroup: group, side: side))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
internal func menuWidth(side: SwipeGroupSide)->CGFloat {
|
|
97
|
+
switch side {
|
|
98
|
+
case .leading:
|
|
99
|
+
return self.leadingSideGroup.map { $0.buttonWidth }.reduce(0, +)
|
|
100
|
+
|
|
101
|
+
case .trailing:
|
|
102
|
+
return self.trailingSideGroup.map { $0.buttonWidth }.reduce(0, +)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// MARK: drag gesture
|
|
107
|
+
|
|
108
|
+
internal func dragOnChanged(value: DragGesture.Value) {
|
|
109
|
+
let horizontalTranslation = value.translation.width
|
|
110
|
+
if self.nonDraggableCondition(horizontalTranslation: horizontalTranslation) {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if self.openSideLock != nil {
|
|
115
|
+
// if one side is open, we need to add the menu width!
|
|
116
|
+
let menuWidth = self.openSideLock == .leading ? self.menuWidth(side: .leading) : self.menuWidth(side: .trailing)
|
|
117
|
+
self.offsetX = menuWidth * openSideLock!.sideFactor + horizontalTranslation
|
|
118
|
+
self.triggerHapticFeedbackIfNeeded(horizontalTranslation: horizontalTranslation)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
self.triggerHapticFeedbackIfNeeded(horizontalTranslation: horizontalTranslation)
|
|
123
|
+
|
|
124
|
+
if horizontalTranslation > 8 || horizontalTranslation < -8 { // makes sure the swipe cell doesn't open too easily
|
|
125
|
+
self.currentUserInteractionCellID = self.id
|
|
126
|
+
self.offsetX = horizontalTranslation
|
|
127
|
+
} else {
|
|
128
|
+
self.offsetX = 0
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
internal func nonDraggableCondition(horizontalTranslation: CGFloat)->Bool {
|
|
133
|
+
return self.offsetX == 0 && (self.leadingSideGroup.isEmpty && horizontalTranslation > 0 || self.trailingSideGroup.isEmpty && horizontalTranslation < 0)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
internal func dragOnEnded(value: DragGesture.Value) {
|
|
137
|
+
let swipeOutTriggerValue = self.cellWidth * self.settings.swipeOutTriggerRatio
|
|
138
|
+
|
|
139
|
+
if self.offsetX == 0 {
|
|
140
|
+
self.openSideLock = nil
|
|
141
|
+
} else if self.offsetX > 0 {
|
|
142
|
+
if self.leadingSideGroup.isEmpty == false {
|
|
143
|
+
if self.offsetX < settings.openTriggerValue || (self.openSideLock == .leading && self.offsetX < self.menuWidth(side: .leading) * 0.8) {
|
|
144
|
+
self.setOffsetX(value: 0)
|
|
145
|
+
} else if let leftItem = self.leadingSideGroup.filter({ $0.swipeOutAction == true }).first, self.offsetX.magnitude > swipeOutTriggerValue {
|
|
146
|
+
self.swipeOutAction(item: leftItem, sideFactor: 1)
|
|
147
|
+
} else {
|
|
148
|
+
self.lockSideMenu(side: .leading)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
} else {
|
|
152
|
+
// leading group emtpy
|
|
153
|
+
self.setOffsetX(value: 0)
|
|
154
|
+
}
|
|
155
|
+
} else if self.offsetX < 0 {
|
|
156
|
+
if self.trailingSideGroup.isEmpty == false {
|
|
157
|
+
if self.offsetX.magnitude < settings.openTriggerValue || (self.openSideLock == .trailing && self.offsetX > -self.menuWidth(side: .trailing) * 0.8) {
|
|
158
|
+
self.setOffsetX(value: 0)
|
|
159
|
+
} else if let rightItem = self.trailingSideGroup.filter({ $0.swipeOutAction == true }).first, self.offsetX.magnitude > swipeOutTriggerValue {
|
|
160
|
+
self.swipeOutAction(item: rightItem, sideFactor: -1)
|
|
161
|
+
} else {
|
|
162
|
+
self.lockSideMenu(side: .trailing)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
} else {
|
|
166
|
+
// trailing group emtpy
|
|
167
|
+
self.setOffsetX(value: 0)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
internal func triggerHapticFeedbackIfNeeded(horizontalTranslation: CGFloat) {
|
|
173
|
+
let side: SwipeGroupSide = horizontalTranslation > 0 ? .leading : .trailing
|
|
174
|
+
let group = side == .leading ? self.leadingSideGroup : self.trailingSideGroup
|
|
175
|
+
let swipeOutActionCondition = self.warnSwipeOutCondition(side: side, hasSwipeOut: true)
|
|
176
|
+
if let item = self.swipeOutItemWithHapticFeedback(group: group), self.hapticFeedbackOccurred == false, swipeOutActionCondition == true {
|
|
177
|
+
self.generator.notificationOccurred(item.swipeOutHapticFeedbackType!)
|
|
178
|
+
self.hapticFeedbackOccurred = true
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
internal func swipeOutItemWithHapticFeedback(group: [SwipeCellActionItem])->SwipeCellActionItem? {
|
|
183
|
+
if let item = group.filter({ $0.swipeOutAction == true }).first {
|
|
184
|
+
if item.swipeOutHapticFeedbackType != nil {
|
|
185
|
+
return item
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return nil
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
internal func swipeOutAction(item: SwipeCellActionItem, sideFactor: CGFloat) {
|
|
192
|
+
if item.swipeOutIsDestructive {
|
|
193
|
+
let swipeOutWidth = cellWidth + 10
|
|
194
|
+
self.setOffsetX(value: swipeOutWidth * sideFactor)
|
|
195
|
+
self.openSideLock = nil
|
|
196
|
+
} else {
|
|
197
|
+
self.setOffsetX(value: 0) // open side lock set in function!
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
201
|
+
item.actionCallback()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
internal func lockSideMenu(side: SwipeGroupSide) {
|
|
206
|
+
self.setOffsetX(value: side.sideFactor * self.menuWidth(side: side))
|
|
207
|
+
self.openSideLock = side
|
|
208
|
+
self.hapticFeedbackOccurred = false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
internal func setOffsetX(value: CGFloat) {
|
|
212
|
+
withAnimation(.spring()) {
|
|
213
|
+
self.offsetX = value
|
|
214
|
+
}
|
|
215
|
+
if self.offsetX == 0 {
|
|
216
|
+
self.openSideLock = nil
|
|
217
|
+
self.hapticFeedbackOccurred = false
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
internal func itemButtonWidth(item: SwipeCellActionItem, itemGroup: [SwipeCellActionItem], side: SwipeGroupSide)->CGFloat {
|
|
222
|
+
let dynamicButtonWidth = self.dynamicButtonWidth(item: item, itemCount: itemGroup.count, side: side)
|
|
223
|
+
let triggerValue = self.cellWidth * settings.swipeOutTriggerRatio
|
|
224
|
+
let swipeOutActionCondition = side == .leading ? self.offsetX > triggerValue : self.offsetX < -triggerValue
|
|
225
|
+
|
|
226
|
+
if item.swipeOutAction && swipeOutActionCondition {
|
|
227
|
+
return self.offsetX.magnitude + settings.addWidthMargin
|
|
228
|
+
} else if swipeOutActionCondition && item.swipeOutAction == false && itemGroup.contains(where: { $0.swipeOutAction == true }) {
|
|
229
|
+
return 0
|
|
230
|
+
} else {
|
|
231
|
+
return dynamicButtonWidth
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
internal func dynamicButtonWidth(item: SwipeCellActionItem, itemCount: Int, side: SwipeGroupSide)->CGFloat {
|
|
236
|
+
let menuWidth = self.menuWidth(side: side)
|
|
237
|
+
return (self.offsetX.magnitude + settings.addWidthMargin) * (item.buttonWidth / menuWidth)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
internal func warnSwipeOutCondition(side: SwipeGroupSide, hasSwipeOut: Bool)->Bool {
|
|
241
|
+
if hasSwipeOut == false {
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
let triggerValue = self.cellWidth * settings.swipeOutTriggerRatio
|
|
245
|
+
return (side == .trailing && self.offsetX < -triggerValue) || (side == .leading && self.offsetX > triggerValue)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
internal func swipeRevealAreaOpacity(side: SwipeGroupSide)->Double {
|
|
249
|
+
switch side {
|
|
250
|
+
case .leading:
|
|
251
|
+
|
|
252
|
+
return self.offsetX > 5 ? 1 : 0
|
|
253
|
+
case .trailing:
|
|
254
|
+
return self.offsetX < -5 ? 1 : 0
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public extension View {
|
|
260
|
+
/// swipe cell modifier
|
|
261
|
+
/// - Parameters:
|
|
262
|
+
/// - id: the string id of this cell. The default value is a uuid string. If you want to set the currentUserInteractionCellID yourself, e.g. for tap to close functionality, you need to override this id value with your own cell id.
|
|
263
|
+
/// - cellWidth: the width of the content view - typically a cell or row in a list under which the swipe to reveal menu should appear.
|
|
264
|
+
/// - leadingSideGroup: the button group on the leading side that shall appear when the user swipes the cell to the right
|
|
265
|
+
/// - trailingSideGroup: the button group on the trailing side that shall appear when the user swipes the cell to the left
|
|
266
|
+
/// - currentUserInteractionCellID: a Binding of an optional UUID that should be set either in the view model of the parent view in which the cells appear or as a State variable into the parent view itself. Don't assign it a value!
|
|
267
|
+
/// - settings: settings. can be omitted in which case the settings struct default values apply.
|
|
268
|
+
/// - Returns: the modified view of the view that can be swiped.
|
|
269
|
+
func swipeCell(id: String = UUID().uuidString, cellWidth: CGFloat = .infinity, leadingSideGroup: [SwipeCellActionItem], trailingSideGroup: [SwipeCellActionItem], currentUserInteractionCellID: Binding<String?> = .constant(""), settings: SwipeCellSettings = SwipeCellSettings())->some View {
|
|
270
|
+
self.modifier(SwipeCellModifier(id: id, cellWidth: cellWidth, leadingSideGroup: leadingSideGroup, trailingSideGroup: trailingSideGroup, currentUserInteractionCellID: currentUserInteractionCellID, settings: settings))
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public extension View {
|
|
275
|
+
func castToAnyView()->AnyView {
|
|
276
|
+
return AnyView(self)
|
|
277
|
+
}
|
|
278
|
+
}
|