├── .github
└── FUNDING.yml
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcuserdata
│ └── yangxu.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── Demo
├── .DS_Store
├── Demo.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ │ └── Package.resolved
│ │ └── xcuserdata
│ │ │ └── yangxu.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata
│ │ └── yangxu.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── Demo
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── ContentView.swift
│ ├── DemoApp.swift
│ ├── Info.plist
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── Test.swift
├── Image
└── demo.gif
├── LICENSE.md
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
└── SwipeCell
│ ├── ScrollNotification.swift
│ ├── SwipeCellConfiguration.swift
│ ├── SwipeCellViewModifier1.swift
│ ├── SwipeCellViewModifier2.swift
│ ├── SwipeCellViewModifier3.swift
│ └── ViewExtension.swift
└── Tests
├── LinuxMain.swift
└── SwipeCellTests
├── SwipeCellTests.swift
└── XCTestManifests.swift
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: fatbobman
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: fatbobman
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: ['https://afdian.com','https://www.paypal.com/paypalme/fatbobman']
16 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/yangxu.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SwipeCell.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | SwipeCell
16 |
17 | primary
18 |
19 |
20 | SwipeCellTests
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Demo/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatbobman/SwipeCell/0196fcfa7cdc7e763ad26614bd91e15ad125a7bd/Demo/.DS_Store
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 7672254D24DCBF010004593E /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7672254C24DCBF010004593E /* DemoApp.swift */; };
11 | 7672254F24DCBF010004593E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7672254E24DCBF010004593E /* ContentView.swift */; };
12 | 7672255124DCBF020004593E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7672255024DCBF020004593E /* Assets.xcassets */; };
13 | 7672255424DCBF020004593E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7672255324DCBF020004593E /* Preview Assets.xcassets */; };
14 | 7672256024DCBF9B0004593E /* SwipeCell in Frameworks */ = {isa = PBXBuildFile; productRef = 7672255F24DCBF9B0004593E /* SwipeCell */; };
15 | 76F30EE5253064AA0025EB88 /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F30EE4253064AA0025EB88 /* Test.swift */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | 7672254924DCBF010004593E /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | 7672254C24DCBF010004593E /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = ""; };
21 | 7672254E24DCBF010004593E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
22 | 7672255024DCBF020004593E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | 7672255324DCBF020004593E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
24 | 7672255524DCBF020004593E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
25 | 7672255E24DCBF6E0004593E /* SwipeCell */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwipeCell; path = ..; sourceTree = ""; };
26 | 76F30EE4253064AA0025EB88 /* Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test.swift; sourceTree = ""; };
27 | /* End PBXFileReference section */
28 |
29 | /* Begin PBXFrameworksBuildPhase section */
30 | 7672254624DCBF010004593E /* Frameworks */ = {
31 | isa = PBXFrameworksBuildPhase;
32 | buildActionMask = 2147483647;
33 | files = (
34 | 7672256024DCBF9B0004593E /* SwipeCell in Frameworks */,
35 | );
36 | runOnlyForDeploymentPostprocessing = 0;
37 | };
38 | /* End PBXFrameworksBuildPhase section */
39 |
40 | /* Begin PBXGroup section */
41 | 7672254024DCBF010004593E = {
42 | isa = PBXGroup;
43 | children = (
44 | 7672254B24DCBF010004593E /* Demo */,
45 | 7672254A24DCBF010004593E /* Products */,
46 | 7672255B24DCBF190004593E /* Frameworks */,
47 | );
48 | sourceTree = "";
49 | };
50 | 7672254A24DCBF010004593E /* Products */ = {
51 | isa = PBXGroup;
52 | children = (
53 | 7672254924DCBF010004593E /* Demo.app */,
54 | );
55 | name = Products;
56 | sourceTree = "";
57 | };
58 | 7672254B24DCBF010004593E /* Demo */ = {
59 | isa = PBXGroup;
60 | children = (
61 | 7672254C24DCBF010004593E /* DemoApp.swift */,
62 | 7672254E24DCBF010004593E /* ContentView.swift */,
63 | 76F30EE4253064AA0025EB88 /* Test.swift */,
64 | 7672255024DCBF020004593E /* Assets.xcassets */,
65 | 7672255524DCBF020004593E /* Info.plist */,
66 | 7672255224DCBF020004593E /* Preview Content */,
67 | );
68 | path = Demo;
69 | sourceTree = "";
70 | };
71 | 7672255224DCBF020004593E /* Preview Content */ = {
72 | isa = PBXGroup;
73 | children = (
74 | 7672255324DCBF020004593E /* Preview Assets.xcassets */,
75 | );
76 | path = "Preview Content";
77 | sourceTree = "";
78 | };
79 | 7672255B24DCBF190004593E /* Frameworks */ = {
80 | isa = PBXGroup;
81 | children = (
82 | 7672255E24DCBF6E0004593E /* SwipeCell */,
83 | );
84 | name = Frameworks;
85 | sourceTree = "";
86 | };
87 | /* End PBXGroup section */
88 |
89 | /* Begin PBXNativeTarget section */
90 | 7672254824DCBF010004593E /* Demo */ = {
91 | isa = PBXNativeTarget;
92 | buildConfigurationList = 7672255824DCBF020004593E /* Build configuration list for PBXNativeTarget "Demo" */;
93 | buildPhases = (
94 | 7672254524DCBF010004593E /* Sources */,
95 | 7672254624DCBF010004593E /* Frameworks */,
96 | 7672254724DCBF010004593E /* Resources */,
97 | );
98 | buildRules = (
99 | );
100 | dependencies = (
101 | );
102 | name = Demo;
103 | packageProductDependencies = (
104 | 7672255F24DCBF9B0004593E /* SwipeCell */,
105 | );
106 | productName = Demo;
107 | productReference = 7672254924DCBF010004593E /* Demo.app */;
108 | productType = "com.apple.product-type.application";
109 | };
110 | /* End PBXNativeTarget section */
111 |
112 | /* Begin PBXProject section */
113 | 7672254124DCBF010004593E /* Project object */ = {
114 | isa = PBXProject;
115 | attributes = {
116 | LastSwiftUpdateCheck = 1200;
117 | LastUpgradeCheck = 1200;
118 | TargetAttributes = {
119 | 7672254824DCBF010004593E = {
120 | CreatedOnToolsVersion = 12.0;
121 | };
122 | };
123 | };
124 | buildConfigurationList = 7672254424DCBF010004593E /* Build configuration list for PBXProject "Demo" */;
125 | compatibilityVersion = "Xcode 9.3";
126 | developmentRegion = en;
127 | hasScannedForEncodings = 0;
128 | knownRegions = (
129 | en,
130 | Base,
131 | );
132 | mainGroup = 7672254024DCBF010004593E;
133 | productRefGroup = 7672254A24DCBF010004593E /* Products */;
134 | projectDirPath = "";
135 | projectRoot = "";
136 | targets = (
137 | 7672254824DCBF010004593E /* Demo */,
138 | );
139 | };
140 | /* End PBXProject section */
141 |
142 | /* Begin PBXResourcesBuildPhase section */
143 | 7672254724DCBF010004593E /* Resources */ = {
144 | isa = PBXResourcesBuildPhase;
145 | buildActionMask = 2147483647;
146 | files = (
147 | 7672255424DCBF020004593E /* Preview Assets.xcassets in Resources */,
148 | 7672255124DCBF020004593E /* Assets.xcassets in Resources */,
149 | );
150 | runOnlyForDeploymentPostprocessing = 0;
151 | };
152 | /* End PBXResourcesBuildPhase section */
153 |
154 | /* Begin PBXSourcesBuildPhase section */
155 | 7672254524DCBF010004593E /* Sources */ = {
156 | isa = PBXSourcesBuildPhase;
157 | buildActionMask = 2147483647;
158 | files = (
159 | 7672254F24DCBF010004593E /* ContentView.swift in Sources */,
160 | 7672254D24DCBF010004593E /* DemoApp.swift in Sources */,
161 | 76F30EE5253064AA0025EB88 /* Test.swift in Sources */,
162 | );
163 | runOnlyForDeploymentPostprocessing = 0;
164 | };
165 | /* End PBXSourcesBuildPhase section */
166 |
167 | /* Begin XCBuildConfiguration section */
168 | 7672255624DCBF020004593E /* Debug */ = {
169 | isa = XCBuildConfiguration;
170 | buildSettings = {
171 | ALWAYS_SEARCH_USER_PATHS = NO;
172 | CLANG_ANALYZER_NONNULL = YES;
173 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
174 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
175 | CLANG_CXX_LIBRARY = "libc++";
176 | CLANG_ENABLE_MODULES = YES;
177 | CLANG_ENABLE_OBJC_ARC = YES;
178 | CLANG_ENABLE_OBJC_WEAK = YES;
179 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
180 | CLANG_WARN_BOOL_CONVERSION = YES;
181 | CLANG_WARN_COMMA = YES;
182 | CLANG_WARN_CONSTANT_CONVERSION = YES;
183 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
184 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
185 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
186 | CLANG_WARN_EMPTY_BODY = YES;
187 | CLANG_WARN_ENUM_CONVERSION = YES;
188 | CLANG_WARN_INFINITE_RECURSION = YES;
189 | CLANG_WARN_INT_CONVERSION = YES;
190 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
191 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
192 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
193 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
194 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
195 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
196 | CLANG_WARN_STRICT_PROTOTYPES = YES;
197 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
198 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
199 | CLANG_WARN_UNREACHABLE_CODE = YES;
200 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
201 | COPY_PHASE_STRIP = NO;
202 | DEBUG_INFORMATION_FORMAT = dwarf;
203 | ENABLE_STRICT_OBJC_MSGSEND = YES;
204 | ENABLE_TESTABILITY = YES;
205 | GCC_C_LANGUAGE_STANDARD = gnu11;
206 | GCC_DYNAMIC_NO_PIC = NO;
207 | GCC_NO_COMMON_BLOCKS = YES;
208 | GCC_OPTIMIZATION_LEVEL = 0;
209 | GCC_PREPROCESSOR_DEFINITIONS = (
210 | "DEBUG=1",
211 | "$(inherited)",
212 | );
213 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
214 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
215 | GCC_WARN_UNDECLARED_SELECTOR = YES;
216 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
217 | GCC_WARN_UNUSED_FUNCTION = YES;
218 | GCC_WARN_UNUSED_VARIABLE = YES;
219 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
220 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
221 | MTL_FAST_MATH = YES;
222 | ONLY_ACTIVE_ARCH = YES;
223 | SDKROOT = iphoneos;
224 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
225 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
226 | };
227 | name = Debug;
228 | };
229 | 7672255724DCBF020004593E /* Release */ = {
230 | isa = XCBuildConfiguration;
231 | buildSettings = {
232 | ALWAYS_SEARCH_USER_PATHS = NO;
233 | CLANG_ANALYZER_NONNULL = YES;
234 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
235 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
236 | CLANG_CXX_LIBRARY = "libc++";
237 | CLANG_ENABLE_MODULES = YES;
238 | CLANG_ENABLE_OBJC_ARC = YES;
239 | CLANG_ENABLE_OBJC_WEAK = YES;
240 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
241 | CLANG_WARN_BOOL_CONVERSION = YES;
242 | CLANG_WARN_COMMA = YES;
243 | CLANG_WARN_CONSTANT_CONVERSION = YES;
244 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
245 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
246 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
247 | CLANG_WARN_EMPTY_BODY = YES;
248 | CLANG_WARN_ENUM_CONVERSION = YES;
249 | CLANG_WARN_INFINITE_RECURSION = YES;
250 | CLANG_WARN_INT_CONVERSION = YES;
251 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
252 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
253 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
254 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
255 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
256 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
257 | CLANG_WARN_STRICT_PROTOTYPES = YES;
258 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
259 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
260 | CLANG_WARN_UNREACHABLE_CODE = YES;
261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
262 | COPY_PHASE_STRIP = NO;
263 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
264 | ENABLE_NS_ASSERTIONS = NO;
265 | ENABLE_STRICT_OBJC_MSGSEND = YES;
266 | GCC_C_LANGUAGE_STANDARD = gnu11;
267 | GCC_NO_COMMON_BLOCKS = YES;
268 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
269 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
270 | GCC_WARN_UNDECLARED_SELECTOR = YES;
271 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
272 | GCC_WARN_UNUSED_FUNCTION = YES;
273 | GCC_WARN_UNUSED_VARIABLE = YES;
274 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
275 | MTL_ENABLE_DEBUG_INFO = NO;
276 | MTL_FAST_MATH = YES;
277 | SDKROOT = iphoneos;
278 | SWIFT_COMPILATION_MODE = wholemodule;
279 | SWIFT_OPTIMIZATION_LEVEL = "-O";
280 | VALIDATE_PRODUCT = YES;
281 | };
282 | name = Release;
283 | };
284 | 7672255924DCBF020004593E /* Debug */ = {
285 | isa = XCBuildConfiguration;
286 | buildSettings = {
287 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
288 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
289 | CODE_SIGN_STYLE = Automatic;
290 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\"";
291 | DEVELOPMENT_TEAM = VFBLFL665K;
292 | ENABLE_PREVIEWS = YES;
293 | INFOPLIST_FILE = Demo/Info.plist;
294 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
295 | LD_RUNPATH_SEARCH_PATHS = (
296 | "$(inherited)",
297 | "@executable_path/Frameworks",
298 | );
299 | PRODUCT_BUNDLE_IDENTIFIER = com.fatbobman.Demo;
300 | PRODUCT_NAME = "$(TARGET_NAME)";
301 | SWIFT_VERSION = 5.0;
302 | TARGETED_DEVICE_FAMILY = "1,2";
303 | };
304 | name = Debug;
305 | };
306 | 7672255A24DCBF020004593E /* Release */ = {
307 | isa = XCBuildConfiguration;
308 | buildSettings = {
309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
310 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
311 | CODE_SIGN_STYLE = Automatic;
312 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\"";
313 | DEVELOPMENT_TEAM = VFBLFL665K;
314 | ENABLE_PREVIEWS = YES;
315 | INFOPLIST_FILE = Demo/Info.plist;
316 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
317 | LD_RUNPATH_SEARCH_PATHS = (
318 | "$(inherited)",
319 | "@executable_path/Frameworks",
320 | );
321 | PRODUCT_BUNDLE_IDENTIFIER = com.fatbobman.Demo;
322 | PRODUCT_NAME = "$(TARGET_NAME)";
323 | SWIFT_VERSION = 5.0;
324 | TARGETED_DEVICE_FAMILY = "1,2";
325 | };
326 | name = Release;
327 | };
328 | /* End XCBuildConfiguration section */
329 |
330 | /* Begin XCConfigurationList section */
331 | 7672254424DCBF010004593E /* Build configuration list for PBXProject "Demo" */ = {
332 | isa = XCConfigurationList;
333 | buildConfigurations = (
334 | 7672255624DCBF020004593E /* Debug */,
335 | 7672255724DCBF020004593E /* Release */,
336 | );
337 | defaultConfigurationIsVisible = 0;
338 | defaultConfigurationName = Release;
339 | };
340 | 7672255824DCBF020004593E /* Build configuration list for PBXNativeTarget "Demo" */ = {
341 | isa = XCConfigurationList;
342 | buildConfigurations = (
343 | 7672255924DCBF020004593E /* Debug */,
344 | 7672255A24DCBF020004593E /* Release */,
345 | );
346 | defaultConfigurationIsVisible = 0;
347 | defaultConfigurationName = Release;
348 | };
349 | /* End XCConfigurationList section */
350 |
351 | /* Begin XCSwiftPackageProductDependency section */
352 | 7672255F24DCBF9B0004593E /* SwipeCell */ = {
353 | isa = XCSwiftPackageProductDependency;
354 | productName = SwipeCell;
355 | };
356 | /* End XCSwiftPackageProductDependency section */
357 | };
358 | rootObject = 7672254124DCBF010004593E /* Project object */;
359 | }
360 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Introspect",
6 | "repositoryURL": "https://github.com/timbersoftware/SwiftUI-Introspect.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "f2616860a41f9d9932da412a8978fec79c06fe24",
10 | "version": "0.1.4"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/xcuserdata/yangxu.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatbobman/SwipeCell/0196fcfa7cdc7e763ad26614bd91e15ad125a7bd/Demo/Demo.xcodeproj/project.xcworkspace/xcuserdata/yangxu.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/xcuserdata/yangxu.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Demo.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // SwipeCellDemo
4 | //
5 | // Created by Yang Xu on 2020/8/6.
6 | //
7 |
8 | import SwiftUI
9 | import SwipeCell
10 |
11 | struct ContentView: View {
12 | @State private var showSheet = false
13 | @State private var bookmark = false
14 | @State private var unread = false
15 | @State private var showAlert = false
16 |
17 | var body: some View {
18 |
19 | //Configure button
20 | let button1 = SwipeCellButton(
21 | buttonStyle: .titleAndImage,
22 | title: "Mark",
23 | systemImage: "bookmark",
24 | titleColor: .white,
25 | imageColor: .white,
26 | view: nil,
27 | backgroundColor: .green,
28 | action: { bookmark.toggle() },
29 | feedback: true
30 | )
31 | let button2 = SwipeCellButton(
32 | buttonStyle: .titleAndImage,
33 | title: "New",
34 | systemImage: "plus.square",
35 | view: nil,
36 | backgroundColor: .blue,
37 | action: { showSheet.toggle() }
38 | )
39 | let button3 = SwipeCellButton(
40 | buttonStyle: .view,
41 | title: "",
42 | systemImage: "",
43 | view: {
44 | AnyView(
45 | Group {
46 | if unread {
47 | Image(systemName: "envelope.badge")
48 | .foregroundColor(.white)
49 | .font(.title)
50 | }
51 | else {
52 | Image(systemName: "envelope.open")
53 | .foregroundColor(.white)
54 | .font(.title)
55 | }
56 | }
57 | )
58 | },
59 | backgroundColor: .orange,
60 | action: { unread.toggle() },
61 | feedback: false
62 | )
63 | let button4 = SwipeCellButton(
64 | buttonStyle: .titleAndImage,
65 | title: "Chat",
66 | systemImage: "bubble.left.and.bubble.right.fill",
67 | titleColor: .yellow,
68 | imageColor: .yellow,
69 | view: nil,
70 | backgroundColor: .pink,
71 | action: { showSheet.toggle() },
72 | feedback: true
73 | )
74 |
75 | let button5 = SwipeCellButton(
76 | buttonStyle: .titleAndImage,
77 | title: "Delete",
78 | systemImage: "trash",
79 | titleColor: .white,
80 | imageColor: .white,
81 | view: nil,
82 | backgroundColor: .red,
83 | action: { showAlert.toggle() },
84 | feedback: true
85 | )
86 |
87 | //Configure Slot ,Several Buttons can be placed in one Slot.
88 | let slot1 = SwipeCellSlot(slots: [button2, button1])
89 | let slot2 = SwipeCellSlot(slots: [button4], slotStyle: .destructive, buttonWidth: 60)
90 | let slot3 = SwipeCellSlot(slots: [button1, button2, button4], slotStyle: .destructive)
91 | let slot4 = SwipeCellSlot(slots: [button3], slotStyle: .normal, buttonWidth: 60)
92 | let slot5 = SwipeCellSlot(slots: [button2, button5], slotStyle: .destructiveDelay)
93 |
94 | return
95 | NavigationView {
96 | List {
97 | demo1()
98 | .onTapGesture {
99 | print("test")
100 | }
101 | .swipeCell(cellPosition: .right, leftSlot: nil, rightSlot: slot1)
102 | Button(action: { print("button") }) {
103 | demo2()
104 | }
105 | .swipeCell(
106 | cellPosition: .both,
107 | leftSlot: slot1,
108 | rightSlot: slot1,
109 | initalStatus: .showLeftSlot,
110 | initialStatusResetDelay: 2.0
111 | )
112 |
113 | demo3()
114 | .onTapGesture {
115 | print("test")
116 | }
117 | .swipeCell(cellPosition: .right, leftSlot: nil, rightSlot: slot3)
118 |
119 | demo4()
120 | .onTapGesture {
121 | print("test")
122 | }
123 | .swipeCell(cellPosition: .left, leftSlot: slot2, rightSlot: nil)
124 |
125 | demo5()
126 | .onTapGesture {
127 | print("test")
128 | }
129 | .swipeCell(cellPosition: .left, leftSlot: slot4, rightSlot: nil)
130 |
131 | demo6()
132 | .onTapGesture {
133 | print("test")
134 | }
135 | .swipeCell(
136 | cellPosition: .both,
137 | leftSlot: slot1,
138 | rightSlot: slot1,
139 | swipeCellStyle: SwipeCellStyle(
140 | alignment: .leading,
141 | dismissWidth: 20,
142 | appearWidth: 20,
143 | destructiveWidth: 240,
144 | vibrationForButton: .error,
145 | vibrationForDestructive: .heavy,
146 | autoResetTime: 3
147 | )
148 | )
149 | demo7()
150 | .onTapGesture {
151 | print("test")
152 | }
153 | .swipeCell(cellPosition: .right, leftSlot: nil, rightSlot: slot5)
154 | .alert(isPresented: $showAlert) {
155 | Alert(
156 | title: Text("Are you sure"),
157 | message: nil,
158 | primaryButton: .destructive(
159 | Text("Delete"),
160 | action: {
161 | print("deleted")
162 | dismissDestructiveDelayButton()
163 | }
164 | ),
165 | secondaryButton: .cancel({ dismissDestructiveDelayButton() })
166 | )
167 | }
168 | Group{
169 | DemoShowStatus()
170 | DoSomethingWithoutPress()
171 | }
172 |
173 | NavigationLink("ScrollView LazyVStack", destination: demo9())
174 | NavigationLink("ScrollView single Cell", destination: Demo8())
175 | }
176 | .navigationBarTitle("SwipeCell Demo", displayMode: .inline)
177 | .toolbar {
178 | EditButton()
179 | }
180 | }
181 | .dismissSwipeCell()
182 | .sheet(isPresented: $showSheet, content: { Text("Hello world") })
183 |
184 | }
185 |
186 | func demo1() -> some View {
187 | HStack {
188 | Spacer()
189 | Text("← Swipe left")
190 | if bookmark {
191 | Image(systemName: "bookmark.fill")
192 | .font(.largeTitle)
193 | .foregroundColor(.green)
194 | }
195 | else {
196 | Image(systemName: "bookmark")
197 | .font(.largeTitle)
198 | .foregroundColor(.green)
199 | }
200 | Spacer()
201 | }
202 | .frame(height: 100)
203 | }
204 |
205 | func demo2() -> some View {
206 | HStack {
207 | Spacer()
208 | Text("← → Sliding on both sides")
209 | if bookmark {
210 | Image(systemName: "bookmark.fill")
211 | .font(.largeTitle)
212 | .foregroundColor(.green)
213 | }
214 | else {
215 | Image(systemName: "bookmark")
216 | .font(.largeTitle)
217 | .foregroundColor(.green)
218 | }
219 | Spacer()
220 | }
221 | .frame(height: 100)
222 | }
223 |
224 | func demo3() -> some View {
225 | HStack {
226 | Spacer()
227 | VStack {
228 | Text("⇠ Swipe left")
229 | Text("MutliButton with destructive button")
230 | }
231 | Spacer()
232 | }
233 | .frame(height: 100)
234 | }
235 |
236 | func demo4() -> some View {
237 | HStack {
238 | Spacer()
239 | VStack {
240 | Text("⇢ Swipe right")
241 | Text("One destructive button")
242 | }
243 | Spacer()
244 | }
245 | .frame(height: 100)
246 | }
247 |
248 | func demo5() -> some View {
249 | HStack {
250 | Spacer()
251 | VStack {
252 | Text("→ Swipe right")
253 | Text("Dynamic Button")
254 | }
255 | Spacer()
256 | }
257 | .frame(height: 100)
258 | }
259 |
260 | func demo6() -> some View {
261 | HStack {
262 | Spacer()
263 | VStack {
264 | Text("← You can set the auto reset duration ")
265 | Text("please wait 3 sec")
266 | }
267 | Spacer()
268 | }
269 | .frame(height: 100)
270 | }
271 |
272 | func demo7() -> some View {
273 | HStack {
274 | Spacer()
275 | VStack {
276 | Text("← destructiveDelay Button")
277 | Text("click delete")
278 | }
279 | Spacer()
280 | }
281 | .frame(height: 100)
282 | }
283 |
284 | func demo9() -> some View {
285 | let button4 = SwipeCellButton(
286 | buttonStyle: .titleAndImage,
287 | title: "New",
288 | systemImage: "bubble.left.and.bubble.right.fill",
289 | titleColor: .white,
290 | imageColor: .white,
291 | view: nil,
292 | backgroundColor: .blue,
293 | action: {},
294 | feedback: true
295 | )
296 |
297 | let button5 = SwipeCellButton(
298 | buttonStyle: .titleAndImage,
299 | title: "Delete",
300 | systemImage: "trash",
301 | titleColor: .white,
302 | imageColor: .white,
303 | view: nil,
304 | backgroundColor: .red,
305 | action: {},
306 | feedback: true
307 | )
308 | let slot = SwipeCellSlot(slots: [button4, button5])
309 | let lists = (0...100).map { $0 }
310 | return ScrollView {
311 | LazyVStack {
312 | ForEach(lists, id: \.self) { item in
313 | Text("Swipe in scrollView:\(item)")
314 | .frame(height: 80)
315 | .swipeCell(cellPosition: .both, leftSlot: slot, rightSlot: slot)
316 | .dismissSwipeCellForScrollViewForLazyVStack()
317 | }
318 | }
319 | }
320 | }
321 |
322 | }
323 |
324 | struct ContentView_Previews: PreviewProvider {
325 | static var previews: some View {
326 | ContentView()
327 | }
328 | }
329 |
330 | struct Demo8: View {
331 | let button1 = SwipeCellButton(
332 | buttonStyle: .view,
333 | title: "",
334 | systemImage: "",
335 | view: {
336 | AnyView(
337 | Circle()
338 | .fill(Color.blue)
339 | .frame(width: 40, height: 40)
340 | .overlay(
341 | Image(systemName: "arrowshape.turn.up.left.fill")
342 | .font(.headline)
343 | .foregroundColor(.white)
344 | )
345 | )
346 | },
347 | backgroundColor: .clear,
348 | action: {}
349 | )
350 |
351 | let button2 = SwipeCellButton(
352 | buttonStyle: .view,
353 | title: "",
354 | systemImage: "",
355 | view: {
356 | AnyView(
357 | Circle()
358 | .fill(Color.orange)
359 | .frame(width: 40, height: 40)
360 | .overlay(
361 | Image(systemName: "flag.fill")
362 | .font(.headline)
363 | .foregroundColor(.white)
364 | )
365 | )
366 | },
367 | backgroundColor: .clear,
368 | action: {}
369 | )
370 |
371 | let button3 = SwipeCellButton(
372 | buttonStyle: .view,
373 | title: "",
374 | systemImage: "",
375 | view: {
376 | AnyView(
377 | Circle()
378 | .fill(Color.red)
379 | .frame(width: 40, height: 40)
380 | .overlay(
381 | Image(systemName: "trash.fill")
382 | .font(.headline)
383 | .foregroundColor(.white)
384 | )
385 | )
386 | },
387 | backgroundColor: .clear,
388 | action: {}
389 | )
390 |
391 | let button4 = SwipeCellButton(
392 | buttonStyle: .view,
393 | title: "",
394 | systemImage: "",
395 | view: {
396 | AnyView(
397 | Circle()
398 | .fill(Color.blue)
399 | .frame(width: 40, height: 40)
400 | .overlay(
401 | Image(systemName: "envelope.badge.fill")
402 | .font(.headline)
403 | .foregroundColor(.white)
404 | )
405 | )
406 | },
407 | backgroundColor: .clear,
408 | action: {}
409 | )
410 |
411 | var body: some View {
412 | let rightSlot = SwipeCellSlot(slots: [button1, button2, button3], buttonWidth: 50)
413 | let leftSlot = SwipeCellSlot(slots: [button4], buttonWidth: 50)
414 | ScrollView {
415 | VStack {
416 | Text("SwipeCell in ScrollView")
417 | .dismissSwipeCellForScrollView() //目前在ScrollView下注入的方式在iOS14下有点问题,所以必须将dissmissSwipeCellForScrollView放置在ScrollView内部
418 | //dismissSwipeCellForScrollView 只能用于 VStack, 如果是LazyVStack请使用dismissSwipeCellForScrollViewForLazyVStack
419 | ForEach(0..<40) { _ in
420 | Text("mail content....")
421 | }
422 | Text("End")
423 | }
424 | .frame(maxWidth: .infinity, maxHeight: .infinity)
425 | }
426 | .swipeCell(cellPosition: .both, leftSlot: leftSlot, rightSlot: rightSlot, clip: false)
427 | }
428 | }
429 |
430 |
431 | struct DemoShowStatus:View{
432 |
433 | let button = SwipeCellButton(
434 | buttonStyle: .titleAndImage,
435 | title: "Mark",
436 | systemImage: "bookmark",
437 | titleColor: .white,
438 | imageColor: .white,
439 | view: nil,
440 | backgroundColor: .green,
441 | action: { },
442 | feedback: true
443 | )
444 |
445 | var slot:SwipeCellSlot{
446 | SwipeCellSlot(slots: [button])
447 | }
448 |
449 | @State var status:CellStatus = .showCell
450 |
451 | var body: some View{
452 | HStack{
453 | Text("Cell Status:")
454 | Text(status.rawValue)
455 | .foregroundColor(.red)
456 | //get the cell status from Environment
457 | .transformEnvironment(\.cellStatus, transform: { cellStatus in
458 | let temp = cellStatus
459 | DispatchQueue.main.async {
460 | if self.status != temp {
461 | self.status = temp
462 | switch self.status{
463 | case .showRightSlot:
464 | print("do right action")
465 | case .showLeftSlot:
466 | print("do left action")
467 | case .showCell:
468 | break
469 | }
470 | }
471 | }
472 | })
473 | }
474 | .frame(maxWidth:.infinity,alignment: .center)
475 | .frame(height:100)
476 | .swipeCell(cellPosition: .both, leftSlot: slot, rightSlot: slot)
477 | }
478 | }
479 |
480 | struct DoSomethingWithoutPress:View{
481 | let button = SwipeCellButton(
482 | buttonStyle: .titleAndImage,
483 | title: "Mark",
484 | systemImage: "bookmark",
485 | titleColor: .white,
486 | imageColor: .white,
487 | view: nil,
488 | backgroundColor: .green,
489 | action: { },
490 | feedback: true
491 | )
492 |
493 | var slotLeft:SwipeCellSlot{
494 | SwipeCellSlot(slots: [button],showAction: {print("do something Left")})
495 | }
496 |
497 | var slotRight:SwipeCellSlot{
498 | SwipeCellSlot(slots: [button],showAction: {print("do something Right")})
499 | }
500 |
501 |
502 | var body: some View{
503 | HStack{
504 | Text("Do something without press")
505 | }
506 | .frame(maxWidth:.infinity,alignment: .center)
507 | .frame(height:100)
508 | .swipeCell(cellPosition: .both, leftSlot: slotLeft, rightSlot: slotRight)
509 | }
510 | }
511 |
--------------------------------------------------------------------------------
/Demo/Demo/DemoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoApp.swift
3 | // Demo
4 | //
5 | // Created by Yang Xu on 2020/8/7.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct DemoApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Demo/Demo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Test.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Test.swift
3 | // Demo
4 | //
5 | // Created by Yang Xu on 2020/10/9.
6 | //
7 |
8 | import SwiftUI
9 | import SwipeCell
10 |
11 | struct Test: View {
12 | var body: some View {
13 | let button1 = SwipeCellButton(buttonStyle: .titleAndImage, title: "Mark", systemImage: "bookmark", titleColor: .white, imageColor: .white, view: nil, backgroundColor: .green, action: {}, feedback:true)
14 | let button2 = SwipeCellButton(buttonStyle: .titleAndImage, title: "New", systemImage: "plus.square", view:nil, backgroundColor: .blue, action: {})
15 | let slot = SwipeCellSlot(slots: [button2,button1])
16 | return
17 | NavigationView{
18 | ScrollView{
19 | LazyVStack{
20 | ForEach(0..<100){ item in
21 | NavigationLink(destination:Text("Swipe in scrollView:\(item)"),label:linkButton)
22 | .frame(height:80)
23 | .swipeCell(cellPosition: .both, leftSlot:slot, rightSlot: slot)
24 | .dismissSwipeCellForScrollViewForLazyVStack()
25 | }
26 | }
27 | }
28 | }
29 | }
30 |
31 | func linkButton() -> some View{
32 | HStack{
33 | Text("test")
34 | Spacer()
35 | }
36 | .contentShape(Rectangle())
37 | }
38 | }
39 |
40 | struct Test_Previews: PreviewProvider {
41 | static var previews: some View {
42 | Test()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Image/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatbobman/SwipeCell/0196fcfa7cdc7e763ad26614bd91e15ad125a7bd/Image/demo.gif
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | **SwipeCell**
2 |
3 | MIT License
4 |
5 | Copyright (c) 东坡肘子 ( Fatobman )
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Introspect",
6 | "repositoryURL": "https://github.com/timbersoftware/SwiftUI-Introspect.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "de5c32c15ae169cfcb27397ffb2734dcd0e1e6d5",
10 | "version": "0.1.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SwipeCell",
8 | platforms: [
9 | .iOS(.v14),
10 | .macOS(.v10_13),
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, and make them visible to other packages.
14 | .library(
15 | name: "SwipeCell",
16 | targets: ["SwipeCell"]),
17 | ],
18 | dependencies: [
19 | // Dependencies declare other packages that this package depends on.
20 | // .package(url: /* package url */, from: "1.0.0"),
21 | .package(name:"Introspect",url:"https://github.com/timbersoftware/SwiftUI-Introspect.git",from:"0.1.4"),
22 | ],
23 | targets: [
24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
25 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
26 | .target(
27 | name: "SwipeCell",
28 | dependencies: ["Introspect"]),
29 | .testTarget(
30 | name: "SwipeCellTests",
31 | dependencies: ["SwipeCell"]),
32 | ]
33 | )
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwipeCell
2 |
3 | SwipeCell 是一个用Swift 5.3开发的 SwiftUI库.目标是为了实现类似iOS Mail程序实现的左右滑动菜单功能.
4 | SwipeCell 需要 XCode 12 ,iOS 14
5 |
6 | 
7 |
8 | ## 配置Button
9 | ```swift
10 | let button1 = SwipeCellButton(buttonStyle: .titleAndImage,
11 | title: "Mark",
12 | systemImage: "bookmark",
13 | titleColor: .white,
14 | imageColor: .white,
15 | view: nil,
16 | backgroundColor: .green,
17 | action: {bookmark.toggle()},
18 | feedback:true
19 | )
20 | ```
21 |
22 | ```swift
23 | //你可以将按钮设置成任意View从而实现更复杂的设计以及动态效果
24 | let button3 = SwipeCellButton(buttonStyle: .view, title:"",systemImage: "", view: {
25 | AnyView(
26 | Group{
27 | if unread {
28 | Image(systemName: "envelope.badge")
29 | .foregroundColor(.white)
30 | .font(.title)
31 | }
32 | else {
33 | Image(systemName: "envelope.open")
34 | .foregroundColor(.white)
35 | .font(.title)
36 | }
37 | }
38 | )
39 | }, backgroundColor: .orange, action: {unread.toggle()}, feedback: false)
40 | ```
41 |
42 | ## 配置Slot
43 | ```swift
44 | let slot1 = SwipeCellSlot(slots: [button2,button1])
45 | let slot2 = SwipeCellSlot(slots: [button4], slotStyle: .destructive, buttonWidth: 60)
46 | let slot3 = SwipeCellSlot(slots: [button2,button1],slotStyle: .destructiveDelay)
47 | ```
48 |
49 | ## 装配
50 | ```swift
51 | cellView()
52 | .swipeCell(cellPosition: .left, leftSlot: slot4, rightSlot: nil)
53 | ```
54 | *更多的配置选项*
55 | ```swift
56 | cellView()
57 | .swipeCell(cellPosition: .both,
58 | leftSlot: slot1,
59 | rightSlot: slot1 ,
60 | swipeCellStyle: SwipeCellStyle(
61 | alignment: .leading,
62 | dismissWidth: 20,
63 | appearWidth: 20,
64 | destructiveWidth: 240,
65 | vibrationForButton: .error,
66 | vibrationForDestructive: .heavy,
67 | autoResetTime: 3)
68 | )
69 | ```
70 |
71 | ## 滚动列表自动消除
72 | *For List*
73 | ```swift
74 | List{
75 | ```
76 | }
77 | .dismissSwipeCell()
78 | }
79 | ```
80 |
81 | *For single cell in ScrollView*
82 | ```swift
83 | ScrollView{
84 | VStack{
85 | Text("Mail Title")
86 | .dismissSwipeCellForScrollView()
87 | Text("Mail Content")
88 | ....
89 | }
90 | .frame(maxWidth:.infinity,maxHeight: .infinity)
91 | }
92 | .swipeCell(cellPosition: .both, leftSlot: leftSlot, rightSlot: rightSlot,clip: false)
93 | ```
94 |
95 | *For LazyVStack in ScrollView*
96 | ```swift
97 | ScrollView{
98 | LazyVStack{
99 | ForEach(lists,id:\.self){ item in
100 | Text("Swipe in scrollView:\(item)")
101 | .frame(height:80)
102 | .swipeCell(cellPosition: .both, leftSlot:slot, rightSlot: slot)
103 | .dismissSwipeCellForScrollViewForLazyVStack()
104 | }
105 | }
106 | }
107 | ```
108 |
109 | Get Cell Status
110 | ```swift
111 | HStack{
112 | Text("Cell Status:")
113 | Text(status.rawValue)
114 | .foregroundColor(.red)
115 | //get the cell status from Environment
116 | .transformEnvironment(\.cellStatus, transform: { cellStatus in
117 | let temp = cellStatus
118 | DispatchQueue.main.async {
119 | self.status = temp
120 | }
121 | })
122 | }
123 | .frame(maxWidth:.infinity,alignment: .center)
124 | .frame(height:100)
125 | .swipeCell(cellPosition: .both, leftSlot: slot, rightSlot: slot)
126 | ```
127 |
128 |
129 | * dismissSwipeCell 在editmode下支持选择
130 | * dismissSwipeCellForScrollView 用于ScrollView,通常用于只有一个Cell的场景,比如说Mail中的邮件内容显示.参看Demo中的演示
131 | * dismissSwipeCellForScrollViewForLazyVStack 用于ScrollView中使用LazyVStack场景.个别时候会打断滑动菜单出现动画.个人觉得如无特别需要还是使用List代替LazyVStack比较好.
132 |
133 |
134 | 由于SwiftUI没有很好的方案能够获取滚动状态,所以采用了 [Introspect](https://github.com/siteline/SwiftUI-Introspect.git)实现的上述功能.
135 |
136 | destructiveDelay 形式的 button,需要在action中添加 dismissDestructiveDelayButton()已保证在alter执行后,Cell复位
137 |
138 |
139 |
140 | ## 当前问题
141 | * 动画细节仍然不足
142 | * EditMode模式下仍有不足
143 |
144 |
145 | ## 欢迎多提宝贵意见!
146 |
147 | SwipeCell is available under the [MIT license](LICENSE.md).
148 |
149 | You can give your feedback or suggestions by creating Issues. You can also contact me on Twitter [@fatbobman](https://x.com/fatbobman).
150 |
--------------------------------------------------------------------------------
/Sources/SwipeCell/ScrollNotification.swift:
--------------------------------------------------------------------------------
1 | // Created by Yang Xu on 2020/8/4.
2 | //
3 |
4 | import Combine
5 | import Foundation
6 | import Introspect
7 | import SwiftUI
8 | import UIKit
9 |
10 | /*
11 | dismissSwipeCellFast响应及时,不过会产生和SwiftUI List的一些冲突,
12 | 导致删除和选择会有问题.所以屏蔽的删除.如果你不需要选择并自己实现删除,这个版本会给你最快速的滚动后SwipeButton复位动作
13 | 另外,这个dismissSwipeCellFast不支持Button响应,包括NavitionLink.如果你确定要使用,请使用onTapGesture来响应点击.
14 | 总之,如果如果你不很清楚,那么就使用dismissSwipeCell
15 | */
16 | //MARK: dismissList1 not suggest now
17 | extension View {
18 | public func dismissSwipeCellFast() -> some View {
19 | self
20 | .modifier(ScrollNotificationInject(showSelection: false))
21 | }
22 | }
23 |
24 | struct ScrollNotificationInject: ViewModifier {
25 | var showSelection: Bool
26 | @ObservedObject var delegate = Delegate()
27 | func body(content: Content) -> some View {
28 | content
29 | .introspectTableView { list in
30 | list.delegate = delegate
31 | list.allowsSelection = showSelection
32 | }
33 | }
34 | }
35 |
36 | class Delegate: NSObject, UITableViewDelegate, UIScrollViewDelegate, ObservableObject {
37 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
38 | NotificationCenter.default.post(name: .swipeCellReset, object: nil)
39 | }
40 |
41 | func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
42 | NotificationCenter.default.post(name: .swipeCellReset, object: nil)
43 | }
44 |
45 | func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath)
46 | -> UITableViewCell.EditingStyle
47 | {
48 | return UITableViewCell.EditingStyle.none
49 | }
50 | }
51 |
52 | //MARK: dismissList
53 | //这个版本对于SwiftUI的List支持更好一点(可以支持选择),.不过响应稍有延迟.另外,屏幕上的Cell必须要滚动至少一个才能开始dismiss
54 | //如果在ForEach上使用了onDelete,系统会自动在Cell右侧添加删除按钮替代自定义的swipeButton.
55 | struct ScrollNotificationWithoutInject: ViewModifier {
56 | let timeInterval: Double
57 | @State var timer = Timer.publish(every: 0.5, on: .main, in: .common)
58 | @State var cancellable: Set = []
59 | @State var listView = UITableView()
60 | @State var hashValue: Int? = nil
61 |
62 | func body(content: Content) -> some View {
63 | content
64 | .introspectTableView { listView in
65 |
66 | self.listView = listView
67 | }
68 | .onAppear {
69 | timer = Timer.publish(every: timeInterval, on: .main, in: .common)
70 | timer.connect()
71 | .store(in: &cancellable)
72 | }
73 | .onDisappear {
74 | cancellable = []
75 | }
76 | .onReceive(timer) { _ in
77 | if hashValue == nil {
78 | hashValue = listView.visibleCells.first.hashValue
79 | }
80 | if hashValue != listView.visibleCells.first.hashValue {
81 | NotificationCenter.default.post(name: .swipeCellReset, object: nil)
82 | hashValue = listView.visibleCells.first.hashValue
83 | }
84 | }
85 | }
86 | }
87 |
88 | extension View {
89 | public func dismissSwipeCell(timeInterval: Double = 0.5) -> some View {
90 | self
91 | .modifier(ScrollNotificationWithoutInject(timeInterval: timeInterval))
92 | }
93 | }
94 |
95 | //ScrollView使用的dismiss.当前在ios13下使用没有问题,不过Introspect在iOS14的beta下无法获取数据.相信过段时间便能修复.
96 | struct ScrollNotificationForScrollViewInject: ViewModifier {
97 | @State var timer = Timer.publish(every: 0.5, on: .main, in: .common)
98 | @State var cancellable: Set = []
99 | @State var scrollView = UIScrollView()
100 | @State var offset: CGPoint? = nil
101 | func body(content: Content) -> some View {
102 | content
103 | .introspectScrollView { scrollView in
104 | self.scrollView = scrollView
105 | }
106 | .onAppear {
107 | timer = Timer.publish(every: 1, on: .main, in: .common)
108 | timer.connect()
109 | .store(in: &cancellable)
110 | }
111 | .onDisappear {
112 | cancellable = []
113 | }
114 | .onReceive(timer) { _ in
115 | if offset == nil {
116 | offset = scrollView.contentOffset
117 | }
118 | if scrollView.contentOffset != offset {
119 | offset = scrollView.contentOffset
120 | NotificationCenter.default.post(name: .swipeCellReset, object: nil)
121 | }
122 | }
123 | }
124 | }
125 |
126 | extension View {
127 | public func dismissSwipeCellForScrollViewInject() -> some View {
128 | self
129 | .modifier(ScrollNotificationForScrollViewInject())
130 | }
131 | }
132 |
133 | public func dismissDestructiveDelayButton() {
134 | NotificationCenter.default.post(name: .swipeCellReset, object: nil)
135 | }
136 |
137 | //MARK: DismissScrollView for VStack
138 | struct TopLeadingY: Equatable {
139 | let topLeadingY: CGFloat
140 | }
141 |
142 | struct ScrollViewPreferencKey: PreferenceKey {
143 | typealias Value = [TopLeadingY]
144 | static var defaultValue: Value = []
145 | static func reduce(value: inout Value, nextValue: () -> Value) {
146 | value = nextValue()
147 | }
148 | }
149 |
150 | struct DismissSwipeCellForScrollView: ViewModifier {
151 | @State var topleadingY: CGFloat? = nil
152 | func body(content: Content) -> some View {
153 | GeometryReader { proxy in
154 | ZStack {
155 | Color.clear
156 | content
157 | .preference(
158 | key: ScrollViewPreferencKey.self,
159 | value: [TopLeadingY(topLeadingY: proxy.frame(in: .global).minY)]
160 | )
161 | }
162 | }
163 | .onPreferenceChange(ScrollViewPreferencKey.self) { preference in
164 | if topleadingY == nil {
165 | topleadingY = preference.first!.topLeadingY
166 | }
167 | if abs(topleadingY! - preference.first!.topLeadingY) < 10 {
168 | NotificationCenter.default.post(name: .swipeCellReset, object: nil)
169 | }
170 | else {
171 | topleadingY = preference.first!.topLeadingY
172 | }
173 | }
174 | }
175 | }
176 |
177 | extension View {
178 | public func dismissSwipeCellForScrollView() -> some View {
179 | self
180 | .modifier(DismissSwipeCellForScrollView())
181 | }
182 | }
183 |
184 | //MARK: DismissScrollView for LazyVStack
185 | //LazyVStack的实现目前没有太好的方案.个别情况下会打断滑动按钮的出现动画
186 | struct CellInfo: Equatable {
187 | let id: UUID
188 | }
189 |
190 | struct ScrollViewPreferencKeyForLazy: PreferenceKey {
191 | typealias Value = [CellInfo]
192 | static var defaultValue: Value = []
193 | static func reduce(value: inout Value, nextValue: () -> Value) {
194 | value.append(contentsOf: nextValue())
195 | }
196 | }
197 |
198 | struct DismissSwipeCellForScrollViewForLazy: ViewModifier {
199 | @State var cellinfos: [CellInfo] = []
200 | func body(content: Content) -> some View {
201 | content
202 | .background(
203 | GeometryReader { proxy in
204 | Color.clear
205 | .preference(
206 | key: ScrollViewPreferencKeyForLazy.self,
207 | value: [CellInfo(id: UUID())]
208 | )
209 | }
210 | )
211 | .onPreferenceChange(ScrollViewPreferencKeyForLazy.self) { preference in
212 | if cellinfos.count == 0 {
213 | DispatchQueue.main.async {
214 | cellinfos = preference
215 | }
216 | }
217 | if cellinfos != preference {
218 | NotificationCenter.default.post(name: .swipeCellReset, object: nil)
219 | }
220 | else {
221 | DispatchQueue.main.async {
222 | cellinfos = preference
223 | }
224 | }
225 | }
226 | }
227 | }
228 |
229 | extension View {
230 | public func dismissSwipeCellForScrollViewForLazyVStack() -> some View {
231 | self
232 | .modifier(DismissSwipeCellForScrollViewForLazy())
233 | }
234 | }
235 |
236 |
237 |
--------------------------------------------------------------------------------
/Sources/SwipeCell/SwipeCellConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Yang Xu on 2020/8/4.
3 | //
4 |
5 | import AudioToolbox
6 | import Foundation
7 | import SwiftUI
8 |
9 | public enum SwipeCellSlotPosition: Int {
10 | case left, right, both, none
11 | }
12 |
13 | public enum SwipeCellSlotStyle {
14 | case normal, destructive, destructiveDelay, delay
15 | }
16 |
17 | public enum SwipeButtonStyle {
18 | case title, image, titleAndImage, view
19 | }
20 |
21 | public struct SwipeCellButton {
22 | public let buttonStyle: SwipeButtonStyle
23 | public let title: LocalizedStringKey?
24 | public let systemImage: String?
25 | public let titleColor: Color
26 | public let imageColor: Color
27 | public let view: (() -> AnyView)?
28 | public let backgroundColor: Color
29 | public let action: () -> Void
30 | public let feedback: Bool
31 |
32 | public init(
33 | buttonStyle: SwipeButtonStyle,
34 | title: LocalizedStringKey?,
35 | systemImage: String?,
36 | titleColor: Color = .white,
37 | imageColor: Color = .white,
38 | view: (() -> AnyView)?,
39 | backgroundColor: Color,
40 | action: @escaping () -> Void,
41 | feedback: Bool = true
42 | ) {
43 | self.buttonStyle = buttonStyle
44 | self.title = title
45 | self.systemImage = systemImage
46 | self.titleColor = titleColor
47 | self.imageColor = imageColor
48 | self.view = view
49 | self.backgroundColor = backgroundColor
50 | self.action = action
51 | self.feedback = feedback
52 | }
53 | }
54 |
55 | public struct SwipeCellSlot {
56 | public let buttonWidth: CGFloat //按钮宽度
57 | public let slots: [SwipeCellButton] //按钮数据
58 | public let slotStyle: SwipeCellSlotStyle //是否包含销毁按钮,销毁按钮只能是最后一个添加
59 | public let appearAnimation: Animation
60 | public let dismissAnimation: Animation
61 | public let showAction: (() -> Void)?
62 |
63 | public init(
64 | slots: [SwipeCellButton],
65 | slotStyle: SwipeCellSlotStyle = .normal,
66 | buttonWidth: CGFloat = 74,
67 | appearAnimation: Animation = .easeOut(duration: 0.5),
68 | dismissAnimation: Animation = .interactiveSpring(),
69 | showAction: (() -> Void)? = nil
70 | ) {
71 | self.buttonWidth = buttonWidth
72 | self.slots = slots
73 | self.slotStyle = slotStyle
74 | self.appearAnimation = appearAnimation
75 | self.dismissAnimation = dismissAnimation
76 | self.showAction = showAction
77 | }
78 |
79 | }
80 |
81 | public struct SwipeCellStyle {
82 | public let destructiveWidth: CGFloat
83 | public let dismissWidth: CGFloat
84 | public let appearWidth: CGFloat
85 | public let alignment: Alignment
86 | public let vibrationForButton: Vibration
87 | public let vibrationForDestructive: Vibration
88 | public let autoResetTime: TimeInterval?
89 |
90 | public init(
91 | alignment: Alignment,
92 | dismissWidth: CGFloat,
93 | appearWidth: CGFloat,
94 | destructiveWidth: CGFloat = 180,
95 | vibrationForButton: Vibration,
96 | vibrationForDestructive: Vibration,
97 | autoResetTime: TimeInterval? = nil
98 | ) {
99 | self.destructiveWidth = destructiveWidth
100 | self.appearWidth = appearWidth
101 | self.dismissWidth = dismissWidth
102 | self.alignment = alignment
103 | self.vibrationForButton = vibrationForButton
104 | self.vibrationForDestructive = vibrationForDestructive
105 | self.autoResetTime = autoResetTime
106 | }
107 |
108 | public static func defaultStyle() -> SwipeCellStyle {
109 | SwipeCellStyle(
110 | alignment: .leading,
111 | dismissWidth: 30,
112 | appearWidth: 30,
113 | destructiveWidth: 220,
114 | vibrationForButton: .soft,
115 | vibrationForDestructive: .medium,
116 | autoResetTime: nil
117 | )
118 | }
119 | }
120 |
121 | public enum Vibration {
122 | case error
123 | case success
124 | case warning
125 | case light
126 | case medium
127 | case heavy
128 | @available(iOS 13.0, *)
129 | case soft
130 | @available(iOS 13.0, *)
131 | case rigid
132 | case selection
133 | case oldSchool
134 | case mute
135 |
136 | public func vibrate() {
137 | switch self {
138 | case .error:
139 | UINotificationFeedbackGenerator().notificationOccurred(.error)
140 | case .success:
141 | UINotificationFeedbackGenerator().notificationOccurred(.success)
142 | case .warning:
143 | UINotificationFeedbackGenerator().notificationOccurred(.warning)
144 | case .light:
145 | UIImpactFeedbackGenerator(style: .light).impactOccurred()
146 | case .medium:
147 | UIImpactFeedbackGenerator(style: .medium).impactOccurred()
148 | case .heavy:
149 | UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
150 | case .soft:
151 | if #available(iOS 13.0, *) {
152 | UIImpactFeedbackGenerator(style: .soft).impactOccurred()
153 | }
154 | case .rigid:
155 | if #available(iOS 13.0, *) {
156 | UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
157 | }
158 | case .selection:
159 | UISelectionFeedbackGenerator().selectionChanged()
160 | case .oldSchool:
161 | AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
162 | case .mute:
163 | break
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/Sources/SwipeCell/SwipeCellViewModifier1.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | public enum CellStatus: String {
5 | case showCell, showLeftSlot, showRightSlot
6 | }
7 |
8 | enum FeedStatus {
9 | case none, feedOnce, feedAgain
10 | }
11 |
12 | struct SwipeCellModifier: ViewModifier {
13 | @State var cellPosition: SwipeCellSlotPosition
14 | let leftSlot: SwipeCellSlot?
15 | let rightSlot: SwipeCellSlot?
16 | let swipeCellStyle: SwipeCellStyle
17 | let clip: Bool
18 | /// If the status should be reset
19 | @State var shouldResetStatusOnAppear = true
20 | /// The amount of time it should take to reset the status on appear
21 | let initialStatusResetDelay: TimeInterval
22 |
23 | @State var status: CellStatus = .showCell
24 | @State var showDalayButtonWith: CGFloat = 0
25 |
26 | @State var offset: CGFloat = 0.0
27 |
28 | @State var frameWidth: CGFloat = 99999
29 | @State var leftOffset: CGFloat = -10000
30 | @State var rightOffset: CGFloat = 10000
31 | @State var spaceWidth: CGFloat = 0
32 |
33 | let cellID = UUID()
34 |
35 | @State var currentCellID: UUID? = nil
36 | @State var resetNotice = NotificationCenter.default.publisher(for: .swipeCellReset)
37 |
38 | @State var feedStatus: FeedStatus = .none
39 |
40 | var leftSlotWidth: CGFloat {
41 | guard let ls = leftSlot else { return 0 }
42 | return CGFloat(ls.slots.count) * ls.buttonWidth
43 | }
44 |
45 | var rightSlotWidth: CGFloat {
46 | guard let rs = rightSlot else { return 0 }
47 | return CGFloat(rs.slots.count) * rs.buttonWidth
48 | }
49 |
50 | var leftdestructiveWidth: CGFloat {
51 | max(swipeCellStyle.destructiveWidth, leftSlotWidth + 70)
52 | }
53 |
54 | var rightdestructiveWidth: CGFloat {
55 | max(swipeCellStyle.destructiveWidth, rightSlotWidth + 70)
56 | }
57 | @Environment(\.editMode) var editMode
58 |
59 | @State var timer = Timer.publish(every: 1, on: .main, in: .common)
60 | @State var cancellables: Set = []
61 |
62 | init(
63 | cellPosition: SwipeCellSlotPosition,
64 | leftSlot: SwipeCellSlot?,
65 | rightSlot: SwipeCellSlot?,
66 | swipeCellStyle: SwipeCellStyle,
67 | clip: Bool,
68 | initialStatusResetDelay: TimeInterval = 0.0,
69 | initialStatus: CellStatus = .showCell
70 | ) {
71 | switch initialStatus {
72 | case .showLeftSlot:
73 | precondition(cellPosition != .right, "Initial status not supported with a right cell position")
74 | case .showRightSlot:
75 | precondition(cellPosition != .left, "Initial status not support with a left cell position")
76 | default:
77 | break
78 | }
79 | _cellPosition = State(wrappedValue: cellPosition)
80 | self.clip = clip
81 | self.leftSlot = leftSlot
82 | self.rightSlot = rightSlot
83 | self.swipeCellStyle = swipeCellStyle
84 | self._status = State(initialValue: initialStatus)
85 | self.initialStatusResetDelay = initialStatusResetDelay
86 | }
87 |
88 | func emptyView(_ button: SwipeCellButton) -> some View {
89 | Text("nil").foregroundColor(button.titleColor)
90 | }
91 |
92 | @ViewBuilder func buttonView(_ slot: SwipeCellSlot, _ i: Int) -> some View {
93 | let button = slot.slots[i]
94 | let viewStyle = button.buttonStyle
95 | let emptyView = emptyView(button)
96 |
97 | switch viewStyle {
98 | case .image:
99 | if let image = button.systemImage {
100 | Image(systemName: image)
101 | .font(.system(size: 23))
102 | .foregroundColor(button.imageColor)
103 | } else {
104 | emptyView
105 | }
106 | case .title:
107 | if let title = button.title {
108 | Text(title)
109 | .font(.callout)
110 | .bold()
111 | .foregroundColor(button.titleColor)
112 | } else {
113 | emptyView
114 | }
115 | case .titleAndImage:
116 | if let title = button.title, let image = button.systemImage {
117 | VStack(spacing: 5) {
118 | Image(systemName: image)
119 | .font(.system(size: 23))
120 | .foregroundColor(button.imageColor)
121 | Text(title)
122 | .font(.callout)
123 | .bold()
124 | .foregroundColor(button.titleColor)
125 | }
126 | } else {
127 | emptyView
128 | }
129 | case .view:
130 | if let view = button.view {
131 | view()
132 | } else {
133 | emptyView
134 | }
135 | }
136 | }
137 |
138 | func slotView(slot: SwipeCellSlot, i: Int, position: SwipeCellSlotPosition) -> some View {
139 | let buttons = slot.slots
140 |
141 | return Rectangle()
142 | .fill(buttons[i].backgroundColor)
143 | .overlay(
144 | ZStack(alignment: position == .left ? .trailing : .leading) {
145 | Color.clear
146 | buttonView(slot, i)
147 | .contentShape(Rectangle())
148 | .frame(width: slot.buttonWidth)
149 | .offset(x: spaceWidth)
150 | .alignmentGuide(
151 | .trailing,
152 | computeValue: { d in
153 | if slot.slotStyle == .destructive && slot.slots.count == 1
154 | && position == .left
155 | {
156 | var result: CGFloat = 0
157 | if offset > slot.buttonWidth {
158 | result = d[.trailing] + offset - slot.buttonWidth
159 | }
160 | else {
161 | result = d[.trailing]
162 | }
163 | return result
164 | }
165 | else {
166 | return d[.trailing]
167 | }
168 | }
169 | )
170 | .alignmentGuide(
171 | .leading,
172 | computeValue: { d in
173 | if slot.slotStyle == .destructive && slot.slots.count == 1
174 | && position == .right
175 | {
176 | var result: CGFloat = 0
177 | if abs(offset) > slot.buttonWidth {
178 | result = d[.leading] + slot.buttonWidth - abs(offset)
179 | }
180 | else {
181 | result = d[.leading]
182 | }
183 |
184 | return result
185 | }
186 | else {
187 | return d[.leading]
188 | }
189 | }
190 | )
191 |
192 | }
193 | )
194 | .contentShape(Rectangle())
195 | .onTapGesture {
196 | if slot.slotStyle == .destructiveDelay && i == slot.slots.count - 1 {
197 | withAnimation(.easeInOut) {
198 | if position == .left {
199 | offset = frameWidth
200 | showDalayButtonWith = 0.0001 //修改成iOS14的样式
201 |
202 | }
203 | else {
204 | offset = -frameWidth
205 | showDalayButtonWith = -0.0001
206 |
207 | }
208 | }
209 | if buttons[i].feedback {
210 | successFeedBack(swipeCellStyle.vibrationForDestructive)
211 | }
212 | }
213 | else {
214 | if buttons[i].feedback {
215 | successFeedBack(swipeCellStyle.vibrationForButton)
216 | }
217 | }
218 |
219 | if slot.slotStyle == .delay {
220 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
221 | buttons[i].action()
222 | }
223 | }
224 | else {
225 | buttons[i].action()
226 | }
227 |
228 | if !(slot.slotStyle == .destructiveDelay && i == slot.slots.count - 1) {
229 | resetStatus()
230 | }
231 | }
232 | }
233 |
234 | @ViewBuilder func loadButtons(_ slot: SwipeCellSlot, position: SwipeCellSlotPosition, frame: CGRect)
235 | -> some View
236 | {
237 | let buttons = slot.slots
238 |
239 | if slot.slotStyle == .destructive && leftOffset == -10000 && position == .left {
240 | let _ = DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
241 | leftOffset = cellOffset(
242 | i: buttons.count - 1,
243 | count: buttons.count,
244 | position: position,
245 | width: frame.width,
246 | slot: slot
247 | )
248 | }
249 | }
250 |
251 | if slot.slotStyle == .destructive && rightOffset == 10000 && position == .right {
252 | let _ = DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
253 | rightOffset = cellOffset(
254 | i: buttons.count - 1,
255 | count: buttons.count,
256 | position: position,
257 | width: frame.width,
258 | slot: slot
259 | )
260 | }
261 | }
262 |
263 | if slot.slotStyle == .destructive {
264 | destructiveButtons(slot, position: position, frame: frame)
265 | }
266 | else {
267 | ZStack {
268 | ForEach(0.. some View {
303 | let buttons = slot.slots
304 | //单button的销毁按钮
305 | if buttons.count == 1 {
306 | slotView(slot: slot, i: 0, position: position)
307 | .offset(
308 | x: cellOffset(
309 | i: 0,
310 | count: buttons.count,
311 | position: position,
312 | width: frame.width,
313 | slot: slot
314 | )
315 | )
316 | }
317 | else {
318 | ZStack {
319 | ForEach(0.. CGFloat {
414 |
415 | if frameWidth == 99999 {
416 | DispatchQueue.main.async {
417 | frameWidth = width
418 | }
419 | }
420 | var result: CGFloat = 0
421 |
422 | let cellOffset = offset * (CGFloat(count - i) / CGFloat(count))
423 | if position == .left {
424 | result = -width + cellOffset
425 |
426 | }
427 | else {
428 | result = width + cellOffset
429 | }
430 |
431 | return result
432 | }
433 |
434 | func lastButtonOffset(position: SwipeCellSlotPosition, slot: SwipeCellSlot?) {
435 |
436 | let animation = slot?.appearAnimation ?? Animation.easeOut(duration: 0.5)
437 |
438 | guard let slot = slot, slot.slotStyle == .destructive else {
439 | if position == .left {
440 | withAnimation(animation) {
441 | leftOffset = -frameWidth
442 | }
443 | }
444 | else {
445 | withAnimation(animation) {
446 | rightOffset = frameWidth
447 | }
448 | }
449 | return
450 | }
451 |
452 | let count = slot.slots.count
453 |
454 | var result: CGFloat = 0
455 |
456 | let cellOffset = offset * (CGFloat(1) / CGFloat(count))
457 | if position == .left {
458 | result = -frameWidth + cellOffset
459 |
460 | }
461 | else {
462 | result = frameWidth + cellOffset
463 | }
464 |
465 | if feedStatus == .feedOnce {
466 | if position == .left {
467 | result = -frameWidth + offset
468 | withAnimation(animation) {
469 | leftOffset = result
470 | }
471 | }
472 | else {
473 | result = frameWidth + offset
474 | withAnimation(.easeInOut) {
475 | rightOffset = result
476 | }
477 | }
478 | }
479 | else if feedStatus == .feedAgain {
480 | if position == .left {
481 | withAnimation(animation) {
482 | leftOffset = result
483 | }
484 | }
485 | else {
486 | withAnimation(animation) {
487 | rightOffset = result
488 | }
489 | }
490 | }
491 | else {
492 |
493 | if position == .left {
494 | withAnimation(animation) {
495 | leftOffset = result
496 | }
497 | }
498 | else {
499 | withAnimation(animation) {
500 | rightOffset = result
501 | }
502 | }
503 | }
504 | }
505 | }
506 |
--------------------------------------------------------------------------------
/Sources/SwipeCell/SwipeCellViewModifier2.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Yang Xu on 2020/8/6.
3 | //
4 |
5 | import Foundation
6 | import SwiftUI
7 |
8 | extension SwipeCellModifier {
9 |
10 | func body(content: Content) -> some View {
11 | if editMode?.wrappedValue == .active { dismissNotification() }
12 |
13 | return ZStack(alignment: .topLeading) {
14 | Color.clear.zIndex(0)
15 | ZStack {
16 |
17 | //加载左侧按钮
18 | GeometryReader { proxy in
19 | ZStack {
20 | if let lbs = leftSlot {
21 | loadButtons(lbs, position: .left, frame: proxy.frame(in: .local))
22 |
23 | }
24 | }
25 | }.zIndex(1)
26 | //加载右侧按钮
27 | GeometryReader { proxy in
28 | ZStack {
29 | if let rbs = rightSlot {
30 | loadButtons(rbs, position: .right, frame: proxy.frame(in: .local))
31 | }
32 | }
33 | }.zIndex(2)
34 |
35 | //加载Cell内容
36 | ZStack(alignment: swipeCellStyle.alignment) {
37 | Color.clear
38 | content
39 | .environment(\.cellStatus, status)
40 | }
41 | .zIndex(3)
42 | .contentShape(Rectangle())
43 | .highPriorityGesture(
44 | TapGesture(count: 1)
45 | .onEnded {
46 | resetStatus()
47 | dismissNotification()
48 | },
49 | including: currentCellID == nil ? .subviews : .gesture
50 | )
51 | .offset(x: offset)
52 | }
53 | }
54 | .contentShape(Rectangle())
55 | .myGesture(getGesture())
56 | .onAppear {
57 | self.setStatus(status)
58 | switch status {
59 | case .showLeftSlot:
60 | offset = leftSlotWidth
61 | case .showRightSlot:
62 | offset = rightSlotWidth
63 | default:
64 | break
65 | }
66 | DispatchQueue.main.asyncAfter(deadline: .now() + initialStatusResetDelay) {
67 | if shouldResetStatusOnAppear {
68 | resetStatus()
69 | }
70 | }
71 | }
72 | .ifIs(clip) {
73 | $0.clipShape(Rectangle())
74 | }
75 | .onChange(of: status){ status in
76 | switch status {
77 | case .showLeftSlot:
78 | leftSlot?.showAction?()
79 | case .showRightSlot:
80 | rightSlot?.showAction?()
81 | case .showCell:
82 | break
83 | }
84 | }
85 | .onReceive(resetNotice) { notice in
86 | // if status == .showCell {return}
87 | //如果其他的cell发送通知或者list发送通知,则本cell复位
88 | if cellID != notice.object as? UUID {
89 | resetStatus()
90 | currentCellID = notice.object as? UUID ?? nil
91 | }
92 |
93 | }
94 | .onReceive(timer) { _ in
95 | resetStatus()
96 | }
97 | .ifIs(
98 | (leftSlot?.slots.count == 1 && leftSlot?.slotStyle == .destructive)
99 | || (rightSlot?.slots.count == 1 && rightSlot?.slotStyle == .destructive)
100 | ) {
101 | $0.onChange(of: offset) { offset in
102 | //当前向右
103 | if offset > 0 && leftSlot?.slots.count == 1 && leftSlot?.slotStyle == .destructive {
104 | guard let leftSlot = leftSlot else { return }
105 | if leftSlot.slotStyle == .destructive && leftSlot.slots.count == 1 {
106 | if feedStatus == .feedOnce {
107 | withAnimation(.easeInOut) {
108 | spaceWidth = offset - leftSlot.buttonWidth
109 | }
110 | }
111 | if feedStatus == .feedAgain {
112 | withAnimation(.easeInOut) {
113 | spaceWidth = 0
114 | }
115 | }
116 | }
117 | }
118 | //当前向左
119 | if offset < 0 && rightSlot?.slots.count == 1 && rightSlot?.slotStyle == .destructive
120 | {
121 | guard let rightSlot = rightSlot else { return }
122 | if rightSlot.slotStyle == .destructive && rightSlot.slots.count == 1 {
123 | if feedStatus == .feedOnce {
124 | withAnimation(.easeInOut) {
125 | spaceWidth = -(abs(offset) - rightSlot.buttonWidth)
126 | }
127 | }
128 | if feedStatus == .feedAgain {
129 | withAnimation(.easeInOut) {
130 | spaceWidth = 0
131 | }
132 | }
133 | }
134 | }
135 | }
136 | }
137 | .listRowInsets(EdgeInsets())
138 |
139 | }
140 |
141 | func setStatus(_ position: CellStatus) {
142 | status = position
143 | guard let time = swipeCellStyle.autoResetTime else { return }
144 | timer = Timer.publish(every: time, on: .main, in: .common)
145 | timer.connect().store(in: &cancellables)
146 | }
147 |
148 | /// Set the status and associated values to ``CellStatus.showCell``
149 | func resetStatus() {
150 | status = .showCell
151 | withAnimation(.easeInOut) {
152 | offset = 0
153 | leftOffset = -frameWidth
154 | rightOffset = frameWidth
155 | spaceWidth = 0
156 | showDalayButtonWith = 0
157 | }
158 | feedStatus = .none
159 | cancellables.removeAll()
160 | currentCellID = nil
161 | // since we reset, we won't have to do it again
162 | shouldResetStatusOnAppear = false
163 |
164 | }
165 |
166 | func successFeedBack(_ type: Vibration) {
167 | #if os(iOS)
168 | type.vibrate()
169 | #endif
170 | }
171 |
172 | func dismissNotification() {
173 | NotificationCenter.default.post(name: .swipeCellReset, object: nil)
174 | }
175 |
176 | }
177 |
178 | extension View {
179 | @ViewBuilder
180 | func myGesture(_ g:_EndedGesture<_ChangedGesture>) -> some View {
181 | if #available(iOS 18, *) {
182 | #if compiler(>=6.0)
183 | highPriorityGesture(g)
184 | #else
185 | gesture(g)
186 | #endif
187 | } else {
188 | gesture(g)
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/Sources/SwipeCell/SwipeCellViewModifier3.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Yang Xu on 2020/8/6.
3 | //
4 |
5 | import Foundation
6 | import SwiftUI
7 |
8 | extension SwipeCellModifier {
9 | func getGesture() -> _EndedGesture<_ChangedGesture> {
10 | //为了避免editMode切换时的异常动画,所以在进入editmode后仍然继续绘制Slots,只是对手势做了处理,避免了滑动
11 | let nonEditGragMinDistance: CGFloat = {
12 | if #available(iOS 18, *) {
13 | #if compiler(>=6.0)
14 | return 20
15 | #endif
16 | }
17 | return 0
18 | }()
19 | return DragGesture(
20 | minimumDistance: editMode?.wrappedValue == .active ? 10000 : nonEditGragMinDistance,
21 | coordinateSpace: .local
22 | )
23 | .onChanged { value in
24 | var width = value.translation.width
25 | cancellables.removeAll() //只要移动,定时清零
26 |
27 | // A gesture happened so don't reset
28 | self.shouldResetStatusOnAppear = false
29 |
30 | if currentCellID != cellID {
31 | currentCellID = cellID
32 | NotificationCenter.default.post(Notification(name: .swipeCellReset, object: cellID))
33 | }
34 |
35 | switch status {
36 |
37 | //在正常状态下
38 | case .showCell:
39 | if cellPosition == .left { width = max(0, width) }
40 | if cellPosition == .right { width = min(0, width) }
41 |
42 | //向右侧滑动
43 | if width > 0 {
44 | if leftSlot?.slotStyle == .destructive {
45 | //确保只在经过时震动一次,如果未释放,返回时还会震动一次,但并不激发action
46 | if width > leftdestructiveWidth
47 | && (feedStatus == .none || feedStatus == .feedAgain)
48 | {
49 | successFeedBack(swipeCellStyle.vibrationForDestructive)
50 | feedStatus = .feedOnce
51 | }
52 | if width <= leftdestructiveWidth && feedStatus == .feedOnce {
53 | successFeedBack(swipeCellStyle.vibrationForDestructive)
54 | feedStatus = .feedAgain
55 | }
56 | //超过阈值,则移动减速
57 | if width > leftdestructiveWidth {
58 | width = leftdestructiveWidth + (width - leftdestructiveWidth) / 2
59 | }
60 | }
61 | else {
62 | //非销毁按钮,超过阈值移动减速
63 | if width > leftSlotWidth {
64 | width = leftSlotWidth + (width - leftSlotWidth) / 2
65 | }
66 | }
67 | }
68 |
69 | //向左侧滑动
70 | if width < 0 {
71 | if rightSlot?.slotStyle == .destructive {
72 | if width < -rightdestructiveWidth
73 | && (feedStatus == .none || feedStatus == .feedAgain)
74 | {
75 | successFeedBack(swipeCellStyle.vibrationForDestructive)
76 | feedStatus = .feedOnce
77 | }
78 | if width >= -rightdestructiveWidth && feedStatus == .feedOnce {
79 | successFeedBack(swipeCellStyle.vibrationForDestructive)
80 | feedStatus = .feedAgain
81 | }
82 | if width < -rightdestructiveWidth {
83 | let tmp = -(-width - rightdestructiveWidth) / 2
84 | width = -rightdestructiveWidth + tmp
85 | }
86 | }
87 | else {
88 | if width < -rightSlotWidth {
89 | let tmp = -(-width - rightSlotWidth) / 2
90 | width = -rightSlotWidth + tmp
91 | }
92 | }
93 | }
94 |
95 | withAnimation(.easeInOut) {
96 | offset = width
97 | }
98 | lastButtonOffset(position: .left, slot: leftSlot)
99 | lastButtonOffset(position: .right, slot: rightSlot)
100 |
101 | //已处于左侧按钮完全展示状态
102 | case .showLeftSlot:
103 | if leftSlot?.slotStyle == .destructive {
104 | if width > 0 {
105 | if width + leftSlotWidth > leftdestructiveWidth
106 | && (feedStatus == .none || feedStatus == .feedAgain)
107 | {
108 | successFeedBack(swipeCellStyle.vibrationForDestructive)
109 | feedStatus = .feedOnce
110 | }
111 | if width + leftSlotWidth <= leftdestructiveWidth && feedStatus == .feedOnce
112 | {
113 | successFeedBack(swipeCellStyle.vibrationForDestructive)
114 | feedStatus = .feedAgain
115 | }
116 | //超过阈值,则移动减速
117 | if width + leftSlotWidth > leftdestructiveWidth {
118 | withAnimation(.easeInOut) {
119 | offset =
120 | leftdestructiveWidth
121 | + (width + leftSlotWidth - leftdestructiveWidth) / 5
122 | lastButtonOffset(position: .left, slot: leftSlot)
123 | lastButtonOffset(position: .right, slot: rightSlot)
124 | }
125 | }
126 | else {
127 | withAnimation(.easeInOut) {
128 | offset = leftSlotWidth + width
129 | lastButtonOffset(position: .left, slot: leftSlot)
130 | lastButtonOffset(position: .right, slot: rightSlot)
131 | }
132 | }
133 | return
134 | }
135 | else {
136 | withAnimation(.easeInOut) {
137 | offset = leftSlotWidth + width
138 | lastButtonOffset(position: .left, slot: leftSlot)
139 | lastButtonOffset(position: .right, slot: rightSlot)
140 | }
141 | }
142 | return
143 | }
144 | else {
145 | if width > 0 {
146 | withAnimation(.easeInOut) {
147 | offset = leftSlotWidth + width / 10
148 | lastButtonOffset(position: .left, slot: leftSlot)
149 | lastButtonOffset(position: .right, slot: rightSlot)
150 | }
151 | }
152 | else {
153 | withAnimation(.easeInOut) {
154 | offset = leftSlotWidth + width
155 | lastButtonOffset(position: .left, slot: leftSlot)
156 | lastButtonOffset(position: .right, slot: rightSlot)
157 | }
158 | }
159 | return
160 | }
161 |
162 | case .showRightSlot:
163 | if rightSlot?.slotStyle == .destructive {
164 | if width < 0 {
165 | if -width + rightSlotWidth > rightdestructiveWidth
166 | && (feedStatus == .none || feedStatus == .feedAgain)
167 | {
168 | successFeedBack(swipeCellStyle.vibrationForDestructive)
169 | feedStatus = .feedOnce
170 | }
171 | if -width + rightSlotWidth <= rightdestructiveWidth
172 | && feedStatus == .feedOnce
173 | {
174 | successFeedBack(swipeCellStyle.vibrationForDestructive)
175 | feedStatus = .feedAgain
176 | }
177 | //超过阈值,则移动减速
178 | if -width + rightSlotWidth > rightdestructiveWidth {
179 | let tmp = -(-width + rightSlotWidth - rightdestructiveWidth) / 5
180 | withAnimation(.easeInOut) {
181 | offset = -rightdestructiveWidth + tmp
182 | lastButtonOffset(position: .left, slot: leftSlot)
183 | lastButtonOffset(position: .right, slot: rightSlot)
184 | }
185 | }
186 | else {
187 | withAnimation(.easeInOut) {
188 | offset = -rightSlotWidth + width
189 | lastButtonOffset(position: .left, slot: leftSlot)
190 | lastButtonOffset(position: .right, slot: rightSlot)
191 | }
192 | }
193 | return
194 | }
195 | else {
196 | withAnimation(.easeInOut) {
197 | offset = -rightSlotWidth + width
198 | lastButtonOffset(position: .left, slot: leftSlot)
199 | lastButtonOffset(position: .right, slot: rightSlot)
200 | }
201 | }
202 | return
203 | }
204 | else {
205 | if width > 0 {
206 | withAnimation(.easeInOut) {
207 | offset = -rightSlotWidth + width
208 | lastButtonOffset(position: .left, slot: leftSlot)
209 | lastButtonOffset(position: .right, slot: rightSlot)
210 | }
211 | }
212 | else {
213 | withAnimation(.easeInOut) {
214 | offset = -rightSlotWidth + width / 10
215 | lastButtonOffset(position: .left, slot: leftSlot)
216 | lastButtonOffset(position: .right, slot: rightSlot)
217 | }
218 | }
219 | return
220 | }
221 |
222 | }
223 |
224 | }.onEnded { value in
225 | if currentCellID != cellID {
226 | currentCellID = cellID
227 | NotificationCenter.default.post(Notification(name: .swipeCellReset, object: cellID))
228 | }
229 | let width = value.translation.width
230 |
231 | if feedStatus == .feedAgain
232 | && (swipeCellStyle.destructiveWidth - abs(offset)) > swipeCellStyle.dismissWidth
233 | {
234 | resetStatus()
235 | return
236 | }
237 |
238 | switch status {
239 | case .showCell:
240 | if abs(width) < swipeCellStyle.appearWidth {
241 | resetStatus()
242 | return
243 | }
244 |
245 | if leftSlot?.slotStyle != .destructive {
246 | if (cellPosition == .left || cellPosition == .both)
247 | && width >= swipeCellStyle.appearWidth
248 | {
249 | withAnimation(leftSlot?.appearAnimation) {
250 | offset = leftSlotWidth
251 | lastButtonOffset(position: .left, slot: leftSlot)
252 | lastButtonOffset(position: .right, slot: rightSlot)
253 | setStatus(.showLeftSlot)
254 | }
255 | return
256 | }
257 | }
258 | else {
259 | if (cellPosition == .left || cellPosition == .both)
260 | && width >= swipeCellStyle.appearWidth && width <= leftdestructiveWidth
261 | {
262 | withAnimation(leftSlot?.appearAnimation) {
263 | offset = leftSlotWidth
264 | lastButtonOffset(position: .left, slot: leftSlot)
265 | lastButtonOffset(position: .right, slot: rightSlot)
266 | setStatus(.showLeftSlot)
267 | }
268 | return
269 | }
270 |
271 | if (cellPosition == .left || cellPosition == .both)
272 | && width > leftdestructiveWidth
273 | {
274 | resetStatus()
275 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
276 | leftSlot?.slots.last?.action()
277 | }
278 | return
279 | }
280 | }
281 |
282 | if rightSlot?.slotStyle != .destructive {
283 | if (cellPosition == .right || cellPosition == .both)
284 | && width <= swipeCellStyle.appearWidth
285 | {
286 | withAnimation(rightSlot?.appearAnimation) {
287 | offset = -rightSlotWidth
288 | lastButtonOffset(position: .left, slot: leftSlot)
289 | lastButtonOffset(position: .right, slot: rightSlot)
290 | setStatus(.showRightSlot)
291 | }
292 | return
293 | }
294 | }
295 | else {
296 | if (cellPosition == .right || cellPosition == .both)
297 | && width <= swipeCellStyle.appearWidth && width >= -rightdestructiveWidth
298 | {
299 | withAnimation(rightSlot?.appearAnimation) {
300 | offset = -rightSlotWidth
301 | lastButtonOffset(position: .left, slot: leftSlot)
302 | lastButtonOffset(position: .right, slot: rightSlot)
303 | setStatus(.showRightSlot)
304 | }
305 | return
306 | }
307 |
308 | if (cellPosition == .right || cellPosition == .both)
309 | && width < -rightdestructiveWidth
310 | {
311 | resetStatus()
312 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
313 | rightSlot?.slots.last?.action()
314 | }
315 | return
316 | }
317 | }
318 |
319 | case .showLeftSlot:
320 | if abs(width) < swipeCellStyle.dismissWidth
321 | && (width + leftSlotWidth) <= leftdestructiveWidth
322 | {
323 | withAnimation(leftSlot?.appearAnimation) {
324 | offset = leftSlotWidth
325 | lastButtonOffset(position: .left, slot: leftSlot)
326 | lastButtonOffset(position: .right, slot: rightSlot)
327 | setStatus(.showLeftSlot)
328 | }
329 | return
330 | }
331 |
332 | if leftSlot?.slotStyle == .destructive {
333 | if feedStatus == .feedOnce {
334 | resetStatus()
335 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
336 | leftSlot?.slots.last?.action()
337 | }
338 | return
339 | }
340 | }
341 | if width < 0 && width <= -swipeCellStyle.dismissWidth {
342 | resetStatus()
343 | return
344 | }
345 |
346 | withAnimation(leftSlot?.appearAnimation) {
347 | offset = leftSlotWidth
348 | lastButtonOffset(position: .left, slot: leftSlot)
349 | lastButtonOffset(position: .right, slot: rightSlot)
350 | setStatus(.showLeftSlot)
351 | }
352 | return
353 |
354 | case .showRightSlot:
355 | if abs(width) < swipeCellStyle.dismissWidth
356 | && (-width + rightSlotWidth) <= leftdestructiveWidth
357 | {
358 | withAnimation(rightSlot?.appearAnimation) {
359 | offset = -rightSlotWidth
360 | lastButtonOffset(position: .left, slot: leftSlot)
361 | lastButtonOffset(position: .right, slot: rightSlot)
362 | setStatus(.showRightSlot)
363 | }
364 | return
365 | }
366 |
367 | if rightSlot?.slotStyle == .destructive {
368 | if feedStatus == .feedOnce {
369 | resetStatus()
370 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
371 | rightSlot?.slots.last?.action()
372 | }
373 | return
374 | }
375 | }
376 |
377 | if width > 0 && width >= swipeCellStyle.dismissWidth {
378 | resetStatus()
379 | return
380 | }
381 |
382 | withAnimation(rightSlot?.appearAnimation) {
383 | offset = -rightSlotWidth
384 | lastButtonOffset(position: .left, slot: leftSlot)
385 | lastButtonOffset(position: .right, slot: rightSlot)
386 | setStatus(.showRightSlot)
387 | }
388 |
389 | return
390 |
391 | }
392 | }
393 | }
394 | }
395 |
--------------------------------------------------------------------------------
/Sources/SwipeCell/ViewExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Yang Xu on 2020/8/4.
3 | //
4 |
5 | import SwiftUI
6 |
7 | extension View {
8 | /// Add a swipe cell modifier to the current view
9 | /// - Parameters:
10 | /// - cellPosition: <#cellPosition description#>
11 | /// - leftSlot: <#leftSlot description#>
12 | /// - rightSlot: <#rightSlot description#>
13 | /// - swipeCellStyle: <#swipeCellStyle description#>
14 | /// - clip: <#clip description#>
15 | /// - disable: <#disable description#>
16 | /// - initalStatus: The initial status for the swipe cell. This can be used to assist with onboarding
17 | /// - initialStatusResetDelay: The amount of time in seconds from when the view appears to when the initial status is reset
18 | /// - Returns: <#description#>
19 | @ViewBuilder public func swipeCell(
20 | cellPosition: SwipeCellSlotPosition,
21 | leftSlot: SwipeCellSlot?,
22 | rightSlot: SwipeCellSlot?,
23 | swipeCellStyle: SwipeCellStyle = .defaultStyle(),
24 | clip: Bool = true,
25 | disable: Bool = false,
26 | initalStatus: CellStatus = .showCell,
27 | initialStatusResetDelay: TimeInterval = 0.0
28 | ) -> some View {
29 | if cellPosition == .none ? true : disable {
30 | self.listRowInsets(EdgeInsets())
31 | } else {
32 | self
33 | .modifier(
34 | SwipeCellModifier(
35 | cellPosition: cellPosition,
36 | leftSlot: leftSlot,
37 | rightSlot: rightSlot,
38 | swipeCellStyle: swipeCellStyle,
39 | clip: clip,
40 | initialStatusResetDelay: initialStatusResetDelay,
41 | initialStatus: initalStatus
42 | )
43 | )
44 | }
45 | }
46 | }
47 |
48 | extension View {
49 | @ViewBuilder public func _hidden(_ condition: Bool) -> some View {
50 | Group {
51 | if condition {
52 | self
53 | }
54 | else {
55 | EmptyView()
56 | }
57 | }
58 | }
59 |
60 | @ViewBuilder func ifIs(_ condition: Bool, transform: (Self) -> T) -> some View
61 | where T: View {
62 | if condition {
63 | transform(self)
64 | }
65 | else {
66 | self
67 | }
68 | }
69 |
70 | func doSomething(_ action: () -> Void) -> some View {
71 | action()
72 | return self
73 | }
74 | }
75 |
76 | extension Notification.Name {
77 | public static let swipeCellReset = Notification.Name("com.swipeCell.reset")
78 | }
79 |
80 | public struct CellStatusKey: EnvironmentKey {
81 | public static var defaultValue: CellStatus = .showCell
82 | }
83 |
84 | extension EnvironmentValues {
85 | public var cellStatus: CellStatus {
86 | get { self[CellStatusKey.self] }
87 | set {
88 | self[CellStatusKey.self] = newValue
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SwipeCellTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += SwipeCellTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/SwipeCellTests/SwipeCellTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import SwipeCell
3 |
4 | final class SwipeCellTests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | // XCTAssertEqual(SwipeCell().text, "Hello, World!")
10 | }
11 |
12 | static var allTests = [
13 | ("testExample", testExample),
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/SwipeCellTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(SwipeCellTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------