├── .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 | ![Demo](Image/demo.gif) 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 | --------------------------------------------------------------------------------