├── .gitignore ├── DragCardContainer.podspec ├── DragCardContainer ├── DragCardContainer.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── DragCardContainer.xcscheme ├── DragCardContainer │ └── DragCardContainer.h └── Sources │ ├── BasicInfo.swift │ ├── CGFloat+Extension.swift │ ├── CGPoint+Extension.swift │ ├── CGVector+Extension.swift │ ├── CardEngine.swift │ ├── CardModel.swift │ ├── Default.swift │ ├── Direction.swift │ ├── DragCardContainer.swift │ ├── DragCardDataSource.swift │ ├── DragCardDelegate.swift │ ├── DragCardView.swift │ ├── Gesture.swift │ ├── Log.swift │ ├── Metrics.swift │ ├── Mode.swift │ └── ScaleMode.swift ├── GIF └── example.gif ├── LICENSE ├── README.md └── iOS Example ├── Podfile ├── Podfile.lock ├── iOS Example.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── iOS Example.xcscheme ├── iOS Example.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── iOS Example ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json ├── Contents.json ├── heart.imageset │ ├── Contents.json │ └── heart.png ├── lightning.imageset │ ├── Contents.json │ └── lightning.png ├── pass.imageset │ ├── Contents.json │ └── pass.png └── refresh.imageset │ ├── Contents.json │ └── undo.png ├── Base.lproj ├── LaunchScreen.storyboard └── Main.storyboard ├── Demo ├── BottomView.swift ├── CardOverlayView.swift ├── CardView.swift ├── DetailViewController.swift ├── OverlayLabelView.swift └── ViewController.swift └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | # Mac OS X Finder and whatnot 6 | .DS_Store 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xcuserstate 26 | *.xccheckout 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | #CocoaPods 35 | iOS Example/Pods 36 | -------------------------------------------------------------------------------- /DragCardContainer.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = 'DragCardContainer' 4 | s.version = '1.2.0' 5 | s.summary = 'A multi-directional card swiping library inspired by Tinder and TanTan.' 6 | s.homepage = 'https://github.com/liujunliuhong/DragCardContainer' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { 'liujunliuhong' => '1035841713@qq.com' } 9 | s.source = { :git => 'https://github.com/liujunliuhong/DragCardContainer.git', :tag => s.version.to_s } 10 | s.module_name = 'DragCardContainer' 11 | s.swift_version = '5.0' 12 | s.platform = :ios, '11.0' 13 | s.ios.deployment_target = '11.0' 14 | s.requires_arc = true 15 | s.source_files = 'DragCardContainer/Sources/*.swift' 16 | end 17 | -------------------------------------------------------------------------------- /DragCardContainer/DragCardContainer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8559819329AFA79300365130 /* CGFloat+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8559819229AFA79300365130 /* CGFloat+Extension.swift */; }; 11 | D107964A29B19689003CC604 /* Mode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107964929B19689003CC604 /* Mode.swift */; }; 12 | D107964E29B19C9F003CC604 /* ScaleMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107964D29B19C9F003CC604 /* ScaleMode.swift */; }; 13 | D107965029B1B670003CC604 /* CardEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107964F29B1B670003CC604 /* CardEngine.swift */; }; 14 | D107965A29B1D6BF003CC604 /* DragCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107965929B1D6BF003CC604 /* DragCardView.swift */; }; 15 | D145811529F11BFF00F63F21 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = D145811429F11BFF00F63F21 /* Log.swift */; }; 16 | D15CD68A29A30A1000086A1B /* DragCardContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = D15CD68929A30A1000086A1B /* DragCardContainer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | D15CD69F29A30A8000086A1B /* DragCardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15CD69329A30A8000086A1B /* DragCardDelegate.swift */; }; 18 | D15CD6A229A30A8000086A1B /* DragCardContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15CD69629A30A8000086A1B /* DragCardContainer.swift */; }; 19 | D15CD6A629A30A8000086A1B /* DragCardDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15CD69A29A30A8000086A1B /* DragCardDataSource.swift */; }; 20 | D16A363429E8E8290038FE0E /* Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16A363329E8E8290038FE0E /* Gesture.swift */; }; 21 | D1740C5D29B9D537002DBAD8 /* CGVector+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1740C5C29B9D537002DBAD8 /* CGVector+Extension.swift */; }; 22 | D185877829AEFFC4001FCBB5 /* CardModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D185877729AEFFC4001FCBB5 /* CardModel.swift */; }; 23 | D185877A29AF01E8001FCBB5 /* BasicInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D185877929AF01E8001FCBB5 /* BasicInfo.swift */; }; 24 | D185877C29AF0239001FCBB5 /* Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = D185877B29AF0239001FCBB5 /* Default.swift */; }; 25 | D185878429AF39D0001FCBB5 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D185878329AF39D0001FCBB5 /* Metrics.swift */; }; 26 | D185878A29AF547E001FCBB5 /* CGPoint+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D185878929AF547E001FCBB5 /* CGPoint+Extension.swift */; }; 27 | D185878C29AF5508001FCBB5 /* Direction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D185878B29AF5508001FCBB5 /* Direction.swift */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 8559819229AFA79300365130 /* CGFloat+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Extension.swift"; sourceTree = ""; }; 32 | D107964929B19689003CC604 /* Mode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mode.swift; sourceTree = ""; }; 33 | D107964D29B19C9F003CC604 /* ScaleMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleMode.swift; sourceTree = ""; }; 34 | D107964F29B1B670003CC604 /* CardEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardEngine.swift; sourceTree = ""; }; 35 | D107965929B1D6BF003CC604 /* DragCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragCardView.swift; sourceTree = ""; }; 36 | D145811429F11BFF00F63F21 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 37 | D15CD68629A30A1000086A1B /* DragCardContainer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DragCardContainer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | D15CD68929A30A1000086A1B /* DragCardContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DragCardContainer.h; sourceTree = ""; }; 39 | D15CD69329A30A8000086A1B /* DragCardDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragCardDelegate.swift; sourceTree = ""; }; 40 | D15CD69629A30A8000086A1B /* DragCardContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragCardContainer.swift; sourceTree = ""; }; 41 | D15CD69A29A30A8000086A1B /* DragCardDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragCardDataSource.swift; sourceTree = ""; }; 42 | D16A363329E8E8290038FE0E /* Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gesture.swift; sourceTree = ""; }; 43 | D1740C5C29B9D537002DBAD8 /* CGVector+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGVector+Extension.swift"; sourceTree = ""; }; 44 | D185877729AEFFC4001FCBB5 /* CardModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardModel.swift; sourceTree = ""; }; 45 | D185877929AF01E8001FCBB5 /* BasicInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicInfo.swift; sourceTree = ""; }; 46 | D185877B29AF0239001FCBB5 /* Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Default.swift; sourceTree = ""; }; 47 | D185878329AF39D0001FCBB5 /* Metrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metrics.swift; sourceTree = ""; }; 48 | D185878929AF547E001FCBB5 /* CGPoint+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Extension.swift"; sourceTree = ""; }; 49 | D185878B29AF5508001FCBB5 /* Direction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Direction.swift; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | D15CD68329A30A1000086A1B /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXFrameworksBuildPhase section */ 61 | 62 | /* Begin PBXGroup section */ 63 | D15CD67C29A30A1000086A1B = { 64 | isa = PBXGroup; 65 | children = ( 66 | D15CD69029A30A8000086A1B /* Sources */, 67 | D15CD68829A30A1000086A1B /* DragCardContainer */, 68 | D15CD68729A30A1000086A1B /* Products */, 69 | ); 70 | sourceTree = ""; 71 | }; 72 | D15CD68729A30A1000086A1B /* Products */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | D15CD68629A30A1000086A1B /* DragCardContainer.framework */, 76 | ); 77 | name = Products; 78 | sourceTree = ""; 79 | }; 80 | D15CD68829A30A1000086A1B /* DragCardContainer */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | D15CD68929A30A1000086A1B /* DragCardContainer.h */, 84 | ); 85 | path = DragCardContainer; 86 | sourceTree = ""; 87 | }; 88 | D15CD69029A30A8000086A1B /* Sources */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | D15CD69629A30A8000086A1B /* DragCardContainer.swift */, 92 | D107964F29B1B670003CC604 /* CardEngine.swift */, 93 | D107965929B1D6BF003CC604 /* DragCardView.swift */, 94 | D15CD69A29A30A8000086A1B /* DragCardDataSource.swift */, 95 | D15CD69329A30A8000086A1B /* DragCardDelegate.swift */, 96 | D185878329AF39D0001FCBB5 /* Metrics.swift */, 97 | D185878B29AF5508001FCBB5 /* Direction.swift */, 98 | D185877729AEFFC4001FCBB5 /* CardModel.swift */, 99 | D185877929AF01E8001FCBB5 /* BasicInfo.swift */, 100 | D107964929B19689003CC604 /* Mode.swift */, 101 | D107964D29B19C9F003CC604 /* ScaleMode.swift */, 102 | D1740C5C29B9D537002DBAD8 /* CGVector+Extension.swift */, 103 | D185878929AF547E001FCBB5 /* CGPoint+Extension.swift */, 104 | 8559819229AFA79300365130 /* CGFloat+Extension.swift */, 105 | D185877B29AF0239001FCBB5 /* Default.swift */, 106 | D16A363329E8E8290038FE0E /* Gesture.swift */, 107 | D145811429F11BFF00F63F21 /* Log.swift */, 108 | ); 109 | path = Sources; 110 | sourceTree = ""; 111 | }; 112 | /* End PBXGroup section */ 113 | 114 | /* Begin PBXHeadersBuildPhase section */ 115 | D15CD68129A30A1000086A1B /* Headers */ = { 116 | isa = PBXHeadersBuildPhase; 117 | buildActionMask = 2147483647; 118 | files = ( 119 | D15CD68A29A30A1000086A1B /* DragCardContainer.h in Headers */, 120 | ); 121 | runOnlyForDeploymentPostprocessing = 0; 122 | }; 123 | /* End PBXHeadersBuildPhase section */ 124 | 125 | /* Begin PBXNativeTarget section */ 126 | D15CD68529A30A1000086A1B /* DragCardContainer */ = { 127 | isa = PBXNativeTarget; 128 | buildConfigurationList = D15CD68D29A30A1000086A1B /* Build configuration list for PBXNativeTarget "DragCardContainer" */; 129 | buildPhases = ( 130 | D15CD68129A30A1000086A1B /* Headers */, 131 | D15CD68229A30A1000086A1B /* Sources */, 132 | D15CD68329A30A1000086A1B /* Frameworks */, 133 | D15CD68429A30A1000086A1B /* Resources */, 134 | ); 135 | buildRules = ( 136 | ); 137 | dependencies = ( 138 | ); 139 | name = DragCardContainer; 140 | productName = DragCardContainer; 141 | productReference = D15CD68629A30A1000086A1B /* DragCardContainer.framework */; 142 | productType = "com.apple.product-type.framework"; 143 | }; 144 | /* End PBXNativeTarget section */ 145 | 146 | /* Begin PBXProject section */ 147 | D15CD67D29A30A1000086A1B /* Project object */ = { 148 | isa = PBXProject; 149 | attributes = { 150 | BuildIndependentTargetsInParallel = 1; 151 | LastUpgradeCheck = 1410; 152 | TargetAttributes = { 153 | D15CD68529A30A1000086A1B = { 154 | CreatedOnToolsVersion = 14.1; 155 | }; 156 | }; 157 | }; 158 | buildConfigurationList = D15CD68029A30A1000086A1B /* Build configuration list for PBXProject "DragCardContainer" */; 159 | compatibilityVersion = "Xcode 14.0"; 160 | developmentRegion = en; 161 | hasScannedForEncodings = 0; 162 | knownRegions = ( 163 | en, 164 | Base, 165 | ); 166 | mainGroup = D15CD67C29A30A1000086A1B; 167 | productRefGroup = D15CD68729A30A1000086A1B /* Products */; 168 | projectDirPath = ""; 169 | projectRoot = ""; 170 | targets = ( 171 | D15CD68529A30A1000086A1B /* DragCardContainer */, 172 | ); 173 | }; 174 | /* End PBXProject section */ 175 | 176 | /* Begin PBXResourcesBuildPhase section */ 177 | D15CD68429A30A1000086A1B /* Resources */ = { 178 | isa = PBXResourcesBuildPhase; 179 | buildActionMask = 2147483647; 180 | files = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXResourcesBuildPhase section */ 185 | 186 | /* Begin PBXSourcesBuildPhase section */ 187 | D15CD68229A30A1000086A1B /* Sources */ = { 188 | isa = PBXSourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | D16A363429E8E8290038FE0E /* Gesture.swift in Sources */, 192 | D185877A29AF01E8001FCBB5 /* BasicInfo.swift in Sources */, 193 | D145811529F11BFF00F63F21 /* Log.swift in Sources */, 194 | D1740C5D29B9D537002DBAD8 /* CGVector+Extension.swift in Sources */, 195 | D15CD6A629A30A8000086A1B /* DragCardDataSource.swift in Sources */, 196 | D185878429AF39D0001FCBB5 /* Metrics.swift in Sources */, 197 | 8559819329AFA79300365130 /* CGFloat+Extension.swift in Sources */, 198 | D185877829AEFFC4001FCBB5 /* CardModel.swift in Sources */, 199 | D15CD6A229A30A8000086A1B /* DragCardContainer.swift in Sources */, 200 | D185878C29AF5508001FCBB5 /* Direction.swift in Sources */, 201 | D107964E29B19C9F003CC604 /* ScaleMode.swift in Sources */, 202 | D107964A29B19689003CC604 /* Mode.swift in Sources */, 203 | D15CD69F29A30A8000086A1B /* DragCardDelegate.swift in Sources */, 204 | D107965A29B1D6BF003CC604 /* DragCardView.swift in Sources */, 205 | D185878A29AF547E001FCBB5 /* CGPoint+Extension.swift in Sources */, 206 | D185877C29AF0239001FCBB5 /* Default.swift in Sources */, 207 | D107965029B1B670003CC604 /* CardEngine.swift in Sources */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | /* End PBXSourcesBuildPhase section */ 212 | 213 | /* Begin XCBuildConfiguration section */ 214 | D15CD68B29A30A1000086A1B /* Debug */ = { 215 | isa = XCBuildConfiguration; 216 | buildSettings = { 217 | ALWAYS_SEARCH_USER_PATHS = NO; 218 | CLANG_ANALYZER_NONNULL = YES; 219 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 220 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 221 | CLANG_ENABLE_MODULES = YES; 222 | CLANG_ENABLE_OBJC_ARC = YES; 223 | CLANG_ENABLE_OBJC_WEAK = YES; 224 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 225 | CLANG_WARN_BOOL_CONVERSION = YES; 226 | CLANG_WARN_COMMA = YES; 227 | CLANG_WARN_CONSTANT_CONVERSION = YES; 228 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 229 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 230 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 231 | CLANG_WARN_EMPTY_BODY = YES; 232 | CLANG_WARN_ENUM_CONVERSION = YES; 233 | CLANG_WARN_INFINITE_RECURSION = YES; 234 | CLANG_WARN_INT_CONVERSION = YES; 235 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 236 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 237 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 238 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 239 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 240 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 241 | CLANG_WARN_STRICT_PROTOTYPES = YES; 242 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 243 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 244 | CLANG_WARN_UNREACHABLE_CODE = YES; 245 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 246 | COPY_PHASE_STRIP = NO; 247 | CURRENT_PROJECT_VERSION = 1; 248 | DEBUG_INFORMATION_FORMAT = dwarf; 249 | ENABLE_STRICT_OBJC_MSGSEND = YES; 250 | ENABLE_TESTABILITY = YES; 251 | GCC_C_LANGUAGE_STANDARD = gnu11; 252 | GCC_DYNAMIC_NO_PIC = NO; 253 | GCC_NO_COMMON_BLOCKS = YES; 254 | GCC_OPTIMIZATION_LEVEL = 0; 255 | GCC_PREPROCESSOR_DEFINITIONS = ( 256 | "DEBUG=1", 257 | "$(inherited)", 258 | ); 259 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 260 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 261 | GCC_WARN_UNDECLARED_SELECTOR = YES; 262 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 263 | GCC_WARN_UNUSED_FUNCTION = YES; 264 | GCC_WARN_UNUSED_VARIABLE = YES; 265 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 266 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 267 | MTL_FAST_MATH = YES; 268 | ONLY_ACTIVE_ARCH = YES; 269 | SDKROOT = iphoneos; 270 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 271 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 272 | VERSIONING_SYSTEM = "apple-generic"; 273 | VERSION_INFO_PREFIX = ""; 274 | }; 275 | name = Debug; 276 | }; 277 | D15CD68C29A30A1000086A1B /* Release */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ALWAYS_SEARCH_USER_PATHS = NO; 281 | CLANG_ANALYZER_NONNULL = YES; 282 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 284 | CLANG_ENABLE_MODULES = YES; 285 | CLANG_ENABLE_OBJC_ARC = YES; 286 | CLANG_ENABLE_OBJC_WEAK = YES; 287 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 288 | CLANG_WARN_BOOL_CONVERSION = YES; 289 | CLANG_WARN_COMMA = YES; 290 | CLANG_WARN_CONSTANT_CONVERSION = YES; 291 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 292 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 293 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INFINITE_RECURSION = YES; 297 | CLANG_WARN_INT_CONVERSION = YES; 298 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 299 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 300 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 302 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 303 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 304 | CLANG_WARN_STRICT_PROTOTYPES = YES; 305 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 306 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 307 | CLANG_WARN_UNREACHABLE_CODE = YES; 308 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 309 | COPY_PHASE_STRIP = NO; 310 | CURRENT_PROJECT_VERSION = 1; 311 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 312 | ENABLE_NS_ASSERTIONS = NO; 313 | ENABLE_STRICT_OBJC_MSGSEND = YES; 314 | GCC_C_LANGUAGE_STANDARD = gnu11; 315 | GCC_NO_COMMON_BLOCKS = YES; 316 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 317 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 318 | GCC_WARN_UNDECLARED_SELECTOR = YES; 319 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 320 | GCC_WARN_UNUSED_FUNCTION = YES; 321 | GCC_WARN_UNUSED_VARIABLE = YES; 322 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 323 | MTL_ENABLE_DEBUG_INFO = NO; 324 | MTL_FAST_MATH = YES; 325 | SDKROOT = iphoneos; 326 | SWIFT_COMPILATION_MODE = wholemodule; 327 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 328 | VALIDATE_PRODUCT = YES; 329 | VERSIONING_SYSTEM = "apple-generic"; 330 | VERSION_INFO_PREFIX = ""; 331 | }; 332 | name = Release; 333 | }; 334 | D15CD68E29A30A1000086A1B /* Debug */ = { 335 | isa = XCBuildConfiguration; 336 | buildSettings = { 337 | CODE_SIGN_STYLE = Automatic; 338 | CURRENT_PROJECT_VERSION = 1; 339 | DEFINES_MODULE = YES; 340 | DYLIB_COMPATIBILITY_VERSION = 1; 341 | DYLIB_CURRENT_VERSION = 1; 342 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 343 | GENERATE_INFOPLIST_FILE = YES; 344 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 345 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 346 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 347 | LD_RUNPATH_SEARCH_PATHS = ( 348 | "$(inherited)", 349 | "@executable_path/Frameworks", 350 | "@loader_path/Frameworks", 351 | ); 352 | MARKETING_VERSION = 1.0; 353 | PRODUCT_BUNDLE_IDENTIFIER = com.galaxy.DragCardContainer; 354 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 355 | SKIP_INSTALL = YES; 356 | SWIFT_EMIT_LOC_STRINGS = YES; 357 | SWIFT_VERSION = 5.0; 358 | TARGETED_DEVICE_FAMILY = "1,2"; 359 | }; 360 | name = Debug; 361 | }; 362 | D15CD68F29A30A1000086A1B /* Release */ = { 363 | isa = XCBuildConfiguration; 364 | buildSettings = { 365 | CODE_SIGN_STYLE = Automatic; 366 | CURRENT_PROJECT_VERSION = 1; 367 | DEFINES_MODULE = YES; 368 | DYLIB_COMPATIBILITY_VERSION = 1; 369 | DYLIB_CURRENT_VERSION = 1; 370 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 371 | GENERATE_INFOPLIST_FILE = YES; 372 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 373 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 374 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 375 | LD_RUNPATH_SEARCH_PATHS = ( 376 | "$(inherited)", 377 | "@executable_path/Frameworks", 378 | "@loader_path/Frameworks", 379 | ); 380 | MARKETING_VERSION = 1.0; 381 | PRODUCT_BUNDLE_IDENTIFIER = com.galaxy.DragCardContainer; 382 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 383 | SKIP_INSTALL = YES; 384 | SWIFT_EMIT_LOC_STRINGS = YES; 385 | SWIFT_VERSION = 5.0; 386 | TARGETED_DEVICE_FAMILY = "1,2"; 387 | }; 388 | name = Release; 389 | }; 390 | /* End XCBuildConfiguration section */ 391 | 392 | /* Begin XCConfigurationList section */ 393 | D15CD68029A30A1000086A1B /* Build configuration list for PBXProject "DragCardContainer" */ = { 394 | isa = XCConfigurationList; 395 | buildConfigurations = ( 396 | D15CD68B29A30A1000086A1B /* Debug */, 397 | D15CD68C29A30A1000086A1B /* Release */, 398 | ); 399 | defaultConfigurationIsVisible = 0; 400 | defaultConfigurationName = Release; 401 | }; 402 | D15CD68D29A30A1000086A1B /* Build configuration list for PBXNativeTarget "DragCardContainer" */ = { 403 | isa = XCConfigurationList; 404 | buildConfigurations = ( 405 | D15CD68E29A30A1000086A1B /* Debug */, 406 | D15CD68F29A30A1000086A1B /* Release */, 407 | ); 408 | defaultConfigurationIsVisible = 0; 409 | defaultConfigurationName = Release; 410 | }; 411 | /* End XCConfigurationList section */ 412 | }; 413 | rootObject = D15CD67D29A30A1000086A1B /* Project object */; 414 | } 415 | -------------------------------------------------------------------------------- /DragCardContainer/DragCardContainer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DragCardContainer/DragCardContainer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DragCardContainer/DragCardContainer.xcodeproj/xcshareddata/xcschemes/DragCardContainer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /DragCardContainer/DragCardContainer/DragCardContainer.h: -------------------------------------------------------------------------------- 1 | // 2 | // DragCardContainer.h 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/2/20. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for DragCardContainer. 11 | FOUNDATION_EXPORT double DragCardContainerVersionNumber; 12 | 13 | //! Project version string for DragCardContainer. 14 | FOUNDATION_EXPORT const unsigned char DragCardContainerVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/BasicInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicInfo.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/1. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import QuartzCore 49 | 50 | public struct BasicInfo { 51 | public let translation: CGPoint 52 | public let scale: CGFloat 53 | public let alpha: CGFloat 54 | public let rotationAngle: CGFloat 55 | 56 | public var transform: CGAffineTransform { 57 | let transform = CGAffineTransform(translationX: translation.x, y: translation.y) 58 | return transform.rotated(by: rotationAngle).scaledBy(x: scale, y: scale) 59 | 60 | // let t1 = CGAffineTransform(translationX: translation.x, y: translation.y) 61 | // let t2 = CGAffineTransform(scaleX: scale, y: scale) 62 | // let t3 = CGAffineTransform(rotationAngle: rotationAngle) 63 | // return t1.concatenating(t2).concatenating(t3) 64 | } 65 | 66 | public init(translation: CGPoint, scale: CGFloat, alpha: CGFloat, rotationAngle: CGFloat) { 67 | self.translation = translation 68 | self.scale = scale 69 | self.alpha = alpha 70 | self.rotationAngle = rotationAngle 71 | } 72 | 73 | public static let `default` = BasicInfo(translation: .zero, 74 | scale: 1, 75 | alpha: 1.0, 76 | rotationAngle: 0) 77 | } 78 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/CGFloat+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloat+Extension.swift 3 | // DragCardContainer 4 | // 5 | // Created by galaxy on 2023/3/1. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | 49 | extension CGFloat { 50 | internal var radius: CGFloat { 51 | return self / 180.0 * CGFloat(Double.pi) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/CGPoint+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+Extension.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/1. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | 49 | extension CGPoint { 50 | internal init(_ vector: CGVector) { 51 | self = CGPoint(x: vector.dx, y: vector.dy) 52 | } 53 | } 54 | 55 | extension CGPoint { 56 | internal static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 57 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 58 | } 59 | 60 | internal static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 61 | return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) 62 | } 63 | 64 | static func * (point: CGPoint, scalar: CGFloat) -> CGPoint { 65 | return CGPoint(x: point.x * scalar, y: point.y * scalar) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/CGVector+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGVector+Extension.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/9. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | 49 | extension CGVector { 50 | internal init(from origin: CGPoint = .zero, to target: CGPoint) { 51 | self = CGVector(dx: target.x - origin.x, 52 | dy: target.y - origin.y) 53 | } 54 | 55 | internal init(_ size: CGSize) { 56 | self = CGVector(dx: size.width, dy: size.height) 57 | } 58 | } 59 | 60 | extension CGVector { 61 | internal static func * (lhs: CGVector, rhs: CGVector) -> CGFloat { 62 | return lhs.dx * rhs.dx + lhs.dy * rhs.dy 63 | } 64 | 65 | internal static func / (vector: CGVector, scalar: CGFloat) -> CGVector { 66 | return CGVector(dx: vector.dx / scalar, dy: vector.dy / scalar) 67 | } 68 | 69 | internal var length: CGFloat { 70 | return hypot(dx, dy) 71 | } 72 | 73 | internal var normalized: CGVector { 74 | return self / length 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/CardEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardEngine.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/3. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import UIKit 49 | import QuartzCore 50 | 51 | internal final class CardEngine { 52 | 53 | deinit { 54 | if Log.enableLog { 55 | print("\(self) deinit") 56 | } 57 | flush() 58 | } 59 | 60 | private weak var weakCardContainer: DragCardContainer? 61 | 62 | private var displayCardIndex: Int = 0 63 | 64 | private var cardModels: [CardModel] = [] 65 | 66 | private var currentResetCardModel: CardModel? 67 | private var willOutModels: [CardModel] = [] 68 | 69 | private var metrics: Metrics = .default 70 | 71 | private var _topIndex: Int? 72 | internal var topIndex: Int? { 73 | get { 74 | _topIndex = cardModels.last?.index 75 | return _topIndex 76 | } 77 | set { 78 | if let newValue = newValue, newValue >= 0 && newValue <= metrics.numberOfCards - 1 { 79 | _topIndex = newValue 80 | } else { 81 | _topIndex = 0 82 | } 83 | start(forceReset: false, animation: false) 84 | } 85 | } 86 | 87 | internal var cardContainer: DragCardContainer { 88 | guard let cardContainer = weakCardContainer else { 89 | fatalError("`cardContainer` is `nil`") 90 | } 91 | return cardContainer 92 | } 93 | 94 | internal var cardDataSource: DragCardDataSource { 95 | guard let cardDataSource = cardContainer.dataSource else { 96 | fatalError("`cardDataSource` is `nil`") 97 | } 98 | return cardDataSource 99 | } 100 | 101 | internal init(cardContainer: DragCardContainer) { 102 | self.weakCardContainer = cardContainer 103 | setup() 104 | } 105 | } 106 | 107 | extension CardEngine { 108 | private func setup() { 109 | 110 | } 111 | } 112 | 113 | extension CardEngine { 114 | internal func start(forceReset: Bool, animation: Bool) { 115 | metrics = Metrics(engine: self) 116 | // 117 | if forceReset { 118 | displayCardIndex = 0 119 | } else { 120 | displayCardIndex = _topIndex ?? 0 121 | } 122 | // 123 | flush() 124 | // 125 | let displayCount = metrics.visibleCount 126 | for i in 0.. metrics.numberOfCards - 1 { 254 | return 255 | } 256 | 257 | let cardView = cardDataSource.dragCard(cardContainer, viewForCard: index) 258 | 259 | cardView.layer.anchorPoint = metrics.cardAnchorPoint 260 | cardView.frame = metrics.cardFrame 261 | cardView.transform = .identity 262 | cardView.alpha = 1.0 263 | cardView.delegate = self 264 | cardContainer.containerView.addSubview(cardView) 265 | 266 | cardView.rewind(from: from) 267 | 268 | let cardModel = CardModel(cardView: cardView, index: index) 269 | cardModel.currentBasicInfo = metrics.maximumBasicInfo 270 | 271 | currentResetCardModel = cardModel 272 | 273 | cardView.initialInfo = currentResetCardModel?.currentBasicInfo ?? .default 274 | 275 | restoreAllCards() 276 | } 277 | } 278 | 279 | extension CardEngine { 280 | internal func setUserInteractionForTopCard(_ isEnabled: Bool) { 281 | guard let topModel = cardModels.last else { return } 282 | topModel.cardView.isUserInteractionEnabled = isEnabled 283 | } 284 | 285 | internal func addPanGestureForTopCard() { 286 | guard let topModel = cardModels.last else { return } 287 | addPanGesture(topModel.cardView) 288 | } 289 | 290 | internal func addTapGestureForTopCard() { 291 | guard let topModel = cardModels.last else { return } 292 | addTapGesture(topModel.cardView) 293 | } 294 | 295 | internal func removePanGestureForTopCard() { 296 | guard let topModel = cardModels.last else { return } 297 | removePanGesture(topModel.cardView) 298 | } 299 | 300 | internal func removeTapGestureForTopCard() { 301 | guard let topModel = cardModels.last else { return } 302 | removeTapGesture(topModel.cardView) 303 | } 304 | 305 | private func removePanGesture(_ cardView: DragCardView) { 306 | cardView.removePanGesture() 307 | } 308 | 309 | private func removeTapGesture(_ cardView: DragCardView) { 310 | cardView.removeTapGesture() 311 | } 312 | 313 | private func addPanGesture(_ cardView: DragCardView) { 314 | cardView.removePanGesture() 315 | cardView.addPanGesture() 316 | } 317 | 318 | private func addTapGesture(_ cardView: DragCardView) { 319 | cardView.removeTapGesture() 320 | cardView.addTapGesture() 321 | } 322 | } 323 | 324 | extension CardEngine { 325 | private func flush() { 326 | for model in cardModels { 327 | model.cardView.removeAllAnimations() 328 | model.cardView.removeFromSuperview() 329 | } 330 | cardModels.removeAll() 331 | // 332 | if let currentResetCardModel = currentResetCardModel { 333 | currentResetCardModel.cardView.isOld = true 334 | } 335 | currentResetCardModel = nil 336 | // 337 | willOutModels.forEach { model in 338 | model.cardView.isOld = true 339 | } 340 | willOutModels.removeAll() 341 | } 342 | 343 | private func installNextCard() { 344 | if !canInstallNextCard() { 345 | return 346 | } 347 | 348 | let index = displayCardIndex 349 | 350 | let cardView = cardDataSource.dragCard(cardContainer, viewForCard: index) 351 | 352 | cardView.layer.anchorPoint = metrics.cardAnchorPoint 353 | cardView.frame = metrics.cardFrame 354 | cardView.transform = metrics.minimumBasicInfo.transform 355 | cardView.alpha = metrics.minimumBasicInfo.alpha 356 | cardView.delegate = self 357 | cardView.isUserInteractionEnabled = false 358 | cardContainer.containerView.insertSubview(cardView, at: 0) 359 | 360 | let cardModel = CardModel(cardView: cardView, index: index) 361 | cardModel.currentBasicInfo = metrics.minimumBasicInfo 362 | cardModels.insert(cardModel, at: 0) 363 | } 364 | 365 | private func restoreAllCards() { 366 | guard let currentResetCardModel = currentResetCardModel else { 367 | return 368 | } 369 | if !canRestoreCard() { 370 | return 371 | } 372 | 373 | let duration = currentResetCardModel.cardView.totalRewindDuration / 2.0 374 | 375 | removePanGestureForTopCard() 376 | removeTapGestureForTopCard() 377 | setUserInteractionForTopCard(false) 378 | 379 | updateAllCardModelsCurrentBasicInfo() 380 | 381 | cardModels.append(currentResetCardModel) 382 | 383 | updateAllCardModelsTargetBasicInfo() 384 | 385 | restoreDisplayIndex() 386 | 387 | for model in cardModels { 388 | if model != currentResetCardModel { 389 | model.cardView.removeAllAnimations() 390 | } 391 | } 392 | 393 | UIView.animate(withDuration: duration, 394 | delay: 0, 395 | options: [.curveLinear, .allowUserInteraction]) { 396 | for model in self.cardModels { 397 | if model != currentResetCardModel { 398 | model.cardView.transform = model.targetBasicInfo.transform 399 | model.cardView.alpha = model.targetBasicInfo.alpha 400 | } 401 | } 402 | } completion: { finished in 403 | if !finished { return } 404 | if self.cardModels.count > self.metrics.visibleCount { 405 | 406 | let tmpModels = Array(self.cardModels.suffix(self.metrics.visibleCount)) 407 | 408 | let willRemoveModels = Array(self.cardModels.prefix(self.cardModels.count - self.metrics.visibleCount)) 409 | for model in willRemoveModels { 410 | model.cardView.removeFromSuperview() 411 | } 412 | 413 | self.cardModels = tmpModels 414 | } 415 | self.currentResetCardModel = nil 416 | } 417 | 418 | if cardContainer.disableTopCardDrag { 419 | removePanGestureForTopCard() 420 | } else { 421 | addPanGestureForTopCard() 422 | } 423 | if cardContainer.disableTopCardClick { 424 | removeTapGestureForTopCard() 425 | } else { 426 | addTapGestureForTopCard() 427 | } 428 | 429 | setUserInteractionForTopCard(true) 430 | 431 | delegateForDisplayTopCard() 432 | } 433 | 434 | private func updateAllCardModelsCurrentBasicInfo() { 435 | for model in cardModels { 436 | model.currentBasicInfo = metrics.minimumBasicInfo 437 | } 438 | 439 | let needModifyModels = cardModels.suffix(metrics.visibleCount).reversed() 440 | for (i, model) in needModifyModels.enumerated() { 441 | model.currentBasicInfo = metrics.basicInfos[i] 442 | } 443 | } 444 | 445 | private func updateAllCardModelsTargetBasicInfo() { 446 | for model in cardModels { 447 | model.targetBasicInfo = metrics.minimumBasicInfo 448 | } 449 | 450 | let needModifyModels = cardModels.suffix(metrics.visibleCount).reversed() 451 | for (i, model) in needModifyModels.enumerated() { 452 | model.targetBasicInfo = metrics.basicInfos[i] 453 | } 454 | } 455 | 456 | private func swipeDelay(for card: DragCardView, forced: Bool) -> TimeInterval { 457 | let duration = card.totalSwipeDuration 458 | let relativeOverlayDuration = card.relativeSwipeOverlayFadeDuration 459 | let delay = duration * TimeInterval(relativeOverlayDuration) 460 | return forced ? delay : 0 461 | } 462 | 463 | private func swipeDuration(for card: DragCardView, direction: Direction, forced: Bool) -> TimeInterval { 464 | return card.finalDuration(direction, forced: forced) / 2.0 465 | } 466 | } 467 | 468 | extension CardEngine { 469 | private func canInstallNextCard() -> Bool { 470 | if metrics.infiniteLoop { 471 | return true 472 | } 473 | if displayCardIndex >= 0 && displayCardIndex <= metrics.numberOfCards - 1 { 474 | return true 475 | } 476 | return false 477 | } 478 | 479 | private func incrementDisplayIndex() { 480 | if !canInstallNextCard() { 481 | return 482 | } 483 | if metrics.infiniteLoop { 484 | if displayCardIndex >= metrics.numberOfCards - 1 { 485 | displayCardIndex = 0 486 | } else { 487 | displayCardIndex = displayCardIndex + 1 488 | } 489 | } else { 490 | displayCardIndex = displayCardIndex + 1 491 | } 492 | } 493 | 494 | private func canRestoreCard() -> Bool { 495 | if metrics.infiniteLoop { 496 | return true 497 | } else { 498 | if displayCardIndex > 0 { 499 | return true 500 | } 501 | return false 502 | } 503 | } 504 | 505 | private func restoreDisplayIndex() { 506 | if !canRestoreCard() { 507 | return 508 | } 509 | if metrics.infiniteLoop { 510 | if displayCardIndex <= 0 { 511 | displayCardIndex = metrics.numberOfCards - 1 512 | } else { 513 | displayCardIndex = displayCardIndex - 1 514 | } 515 | } else { 516 | if displayCardIndex > 0, let bottomModel = cardModels.first, let topModel = cardModels.last { 517 | if bottomModel.index - topModel.index >= metrics.visibleCount - 1 { 518 | displayCardIndex = displayCardIndex - 1 519 | } 520 | } 521 | } 522 | } 523 | } 524 | 525 | extension CardEngine: CardDelegate { 526 | internal func cardDidBeginSwipe(_ card: DragCardView) { 527 | if cardModels.isEmpty { return } 528 | 529 | if card.isOld { return } 530 | 531 | updateAllCardModelsCurrentBasicInfo() 532 | 533 | currentResetCardModel = cardModels.last 534 | 535 | card.initialInfo = currentResetCardModel?.currentBasicInfo ?? .default 536 | 537 | cardModels.removeLast() // remove last 538 | 539 | incrementDisplayIndex() 540 | installNextCard() 541 | 542 | updateAllCardModelsTargetBasicInfo() 543 | 544 | for model in cardModels { 545 | model.cardView.removeAllAnimations() 546 | } 547 | } 548 | 549 | internal func cardDidContinueSwipe(_ card: DragCardView) { 550 | if card.isOld { return } 551 | 552 | let panTranslation = card.panGestureRecognizer?.translation(in: card.superview) ?? .zero 553 | let minimumSideLength = min(cardContainer.bounds.width, cardContainer.bounds.height) 554 | let percentage = min(CGVector(to: panTranslation).length / minimumSideLength, 1) 555 | 556 | for model in cardModels { 557 | let scale = model.currentBasicInfo.scale + (model.targetBasicInfo.scale - model.currentBasicInfo.scale) * percentage 558 | let alpha = model.currentBasicInfo.alpha + (model.targetBasicInfo.alpha - model.currentBasicInfo.alpha) * percentage 559 | let translation = model.currentBasicInfo.translation + (model.targetBasicInfo.translation - model.currentBasicInfo.translation) * percentage 560 | let rotationAngle = model.currentBasicInfo.rotationAngle + (model.targetBasicInfo.rotationAngle - model.currentBasicInfo.rotationAngle) * percentage 561 | 562 | let transform = CGAffineTransform(translationX: translation.x, y: translation.y) 563 | model.cardView.transform = transform.rotated(by: rotationAngle).scaledBy(x: scale, y: scale) 564 | model.cardView.alpha = alpha 565 | // let t1 = CGAffineTransform(translationX: translation.x, y: translation.y) 566 | // let t2 = CGAffineTransform(scaleX: scale, y: scale) 567 | // let t3 = CGAffineTransform(rotationAngle: rotationAngle) 568 | // model.cardView.transform = t1.concatenating(t2).concatenating(t3) 569 | } 570 | } 571 | 572 | internal func cardDidCancelSwipe(_ card: DragCardView) { 573 | if card.isOld { return } 574 | 575 | restoreAllCards() 576 | } 577 | 578 | internal func cardDidSwipe(_ card: DragCardView, withDirection direction: Direction) { 579 | if card.isOld { return } 580 | 581 | let duration = swipeDuration(for: card, direction: direction, forced: false) 582 | 583 | for model in cardModels { 584 | model.cardView.removeAllAnimations() 585 | } 586 | 587 | UIView.animate(withDuration: duration, 588 | delay: 0, 589 | options: [.curveLinear, .allowUserInteraction]) { 590 | for model in self.cardModels { 591 | model.cardView.transform = model.targetBasicInfo.transform 592 | model.cardView.alpha = model.targetBasicInfo.alpha 593 | } 594 | } 595 | 596 | delegateForDisplayTopCard() 597 | 598 | if let currentResetCardModel = currentResetCardModel { 599 | delegateForRemoveTopCard(model: currentResetCardModel, direction: direction) 600 | delegateForFinishRemoveLastCard(model: currentResetCardModel) 601 | 602 | willOutModels.append(currentResetCardModel) 603 | } 604 | currentResetCardModel = nil 605 | 606 | if cardContainer.disableTopCardDrag { 607 | removePanGestureForTopCard() 608 | } else { 609 | addPanGestureForTopCard() 610 | } 611 | if cardContainer.disableTopCardClick { 612 | removeTapGestureForTopCard() 613 | } else { 614 | addTapGestureForTopCard() 615 | } 616 | 617 | setUserInteractionForTopCard(true) 618 | } 619 | 620 | internal func cardDidDragout(_ card: DragCardView) { 621 | for (index, model) in willOutModels.enumerated() { 622 | if model.cardView.identifier == card.identifier { 623 | card.removeFromSuperview() 624 | willOutModels.remove(at: index) 625 | break 626 | } 627 | } 628 | } 629 | 630 | internal func cardDidTap(_ card: DragCardView) { 631 | if card.isOld { return } 632 | delegateForTapTopCard() 633 | } 634 | } 635 | 636 | extension CardEngine { 637 | private func delegateForTapTopCard() { 638 | guard let topModel = cardModels.last else { return } 639 | cardContainer.delegate?.dragCard(cardContainer, 640 | didSelectTopCardAt: topModel.index, 641 | with: topModel.cardView) 642 | } 643 | 644 | private func delegateForDisplayTopCard() { 645 | guard let topModel = cardModels.last else { return } 646 | _topIndex = topModel.index 647 | cardContainer.delegate?.dragCard(cardContainer, 648 | displayTopCardAt: topModel.index, 649 | with: topModel.cardView) 650 | } 651 | 652 | private func delegateForFinishRemoveLastCard(model: CardModel) { 653 | if model.index == metrics.numberOfCards - 1 && !metrics.infiniteLoop { 654 | cardContainer.delegate?.dragCard(cardContainer, 655 | didRemovedLast: model.cardView) 656 | } 657 | } 658 | 659 | private func delegateForRemoveTopCard(model: CardModel, direction: Direction) { 660 | cardContainer.delegate?.dragCard(cardContainer, 661 | didRemovedTopCardAt: model.index, 662 | direction: direction, 663 | with: model.cardView) 664 | } 665 | } 666 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/CardModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardModel.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/1. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import UIKit 49 | 50 | internal final class CardModel { 51 | internal let identifier: String = UUID().uuidString 52 | 53 | internal let cardView: DragCardView 54 | internal let index: Int 55 | 56 | internal var currentBasicInfo: BasicInfo = .default 57 | internal var targetBasicInfo: BasicInfo = .default 58 | 59 | internal var resetTag: Bool = false 60 | 61 | internal init(cardView: DragCardView, index: Int) { 62 | self.cardView = cardView 63 | self.index = index 64 | } 65 | } 66 | 67 | extension CardModel: Equatable { 68 | internal static func == (lhs: CardModel, rhs: CardModel) -> Bool { 69 | return lhs.identifier == rhs.identifier 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/Default.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Default.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/1. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | 49 | internal struct Default { 50 | internal static let infiniteLoop: Bool = false 51 | internal static let mode: Mode = ScaleMode.default 52 | internal static let visibleCount: Int = 3 53 | internal static let cardAnchorPoint: CGPoint = CGPoint(x: 0.5, y: 0.5) 54 | internal static let cardFrame: CGRect = .zero 55 | } 56 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/Direction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Direction.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/1. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | 49 | public enum Direction: Int, CaseIterable { 50 | case left 51 | case right 52 | case up 53 | case down 54 | 55 | public static let horizontal: [Direction] = [.left, .right] 56 | public static let vertical: [Direction] = [.up, .down] 57 | public static let all: [Direction] = Direction.allCases 58 | 59 | public var vector: CGVector { 60 | switch self { 61 | case .left: 62 | return CGVector(dx: -1, dy: 0) 63 | case .right: 64 | return CGVector(dx: 1, dy: 0) 65 | case .up: 66 | return CGVector(dx: 0, dy: -1) 67 | case .down: 68 | return CGVector(dx: 0, dy: 1) 69 | } 70 | } 71 | } 72 | 73 | extension Direction: CustomStringConvertible { 74 | public var description: String { 75 | switch self { 76 | case .left: 77 | return "Left" 78 | case .right: 79 | return "Right" 80 | case .up: 81 | return "Up" 82 | case .down: 83 | return "Down" 84 | } 85 | } 86 | } 87 | 88 | extension Direction { 89 | public static func fromPoint(_ point: CGPoint) -> Direction { 90 | switch (point.x, point.y) { 91 | case let (x, y) where abs(x) >= abs(y) && x > 0: 92 | return .right 93 | case let (x, y) where abs(x) >= abs(y) && x < 0: 94 | return .left 95 | case let (x, y) where abs(x) < abs(y) && y < 0: 96 | return .up 97 | case let (x, y) where abs(x) < abs(y) && y > 0: 98 | return .down 99 | case (_, _): 100 | return .right 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/DragCardContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragCardContainer.swift 3 | // YHDragContainer 4 | // 5 | // Created by jun on 2021/10/18. 6 | // Copyright © 2021 yinhe. All rights reserved. 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import UIKit 49 | 50 | public class DragCardContainer: UIView { 51 | 52 | deinit { 53 | if Log.enableLog { 54 | print("\(self.classForCoder) deinit") 55 | } 56 | } 57 | 58 | /// DataSource. 59 | public weak var dataSource: DragCardDataSource? { 60 | didSet { 61 | reloadDataIfNeeded() 62 | } 63 | } 64 | 65 | /// Delegate. 66 | public weak var delegate: DragCardDelegate? 67 | 68 | /// Visible card count. 69 | public var visibleCount: Int = Default.visibleCount { 70 | didSet { 71 | reloadDataIfNeeded() 72 | } 73 | } 74 | 75 | /// Mode 76 | public var mode: Mode = Default.mode { 77 | didSet { 78 | reloadDataIfNeeded() 79 | } 80 | } 81 | 82 | /// Allow infinite loop 83 | public var infiniteLoop: Bool = Default.infiniteLoop { 84 | didSet { 85 | reloadDataIfNeeded() 86 | } 87 | } 88 | 89 | /// Current top index. 90 | public var currentTopIndex: Int? { 91 | set { 92 | engine.topIndex = newValue 93 | } 94 | get { 95 | return engine.topIndex 96 | } 97 | } 98 | 99 | /// Disable drag action for top card. 100 | public var disableTopCardDrag: Bool = false { 101 | didSet { 102 | if disableTopCardDrag { 103 | engine.removePanGestureForTopCard() 104 | } else { 105 | engine.addPanGestureForTopCard() 106 | } 107 | } 108 | } 109 | 110 | /// Disable tap action for top card. 111 | public var disableTopCardClick: Bool = false { 112 | didSet { 113 | if disableTopCardClick { 114 | engine.removeTapGestureForTopCard() 115 | } else { 116 | engine.addTapGestureForTopCard() 117 | } 118 | } 119 | } 120 | 121 | /// Enable Log. 122 | public var enableLog: Bool { 123 | get { 124 | return Log.enableLog 125 | } 126 | set { 127 | Log.enableLog = newValue 128 | } 129 | } 130 | 131 | private lazy var engine: CardEngine = { 132 | let engine = CardEngine(cardContainer: self) 133 | return engine 134 | }() 135 | 136 | internal lazy var containerView = ContainerView() 137 | 138 | public override init(frame: CGRect) { 139 | super.init(frame: frame) 140 | setupUI() 141 | } 142 | 143 | public required init?(coder: NSCoder) { 144 | super.init(coder: coder) 145 | setupUI() 146 | } 147 | } 148 | 149 | extension DragCardContainer { 150 | private func setupUI() { 151 | addSubview(containerView) 152 | } 153 | } 154 | 155 | extension DragCardContainer { 156 | public override func layoutSubviews() { 157 | super.layoutSubviews() 158 | containerView.frame = bounds 159 | reloadDataIfNeeded() 160 | } 161 | } 162 | 163 | extension DragCardContainer { 164 | public func reloadData(forceReset: Bool, animation: Bool = false) { 165 | engine.start(forceReset: forceReset, animation: animation) 166 | } 167 | 168 | public func rewind(from: Direction) { 169 | engine.rewind(from: from) 170 | } 171 | 172 | public func swipeTopCard(to: Direction) { 173 | engine.swipeTopCard(to: to) 174 | } 175 | } 176 | 177 | extension DragCardContainer { 178 | private func reloadDataIfNeeded() { 179 | if dataSource != nil { 180 | engine.start(forceReset: false, animation: false) 181 | } 182 | } 183 | } 184 | 185 | extension DragCardContainer { 186 | public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 187 | return true 188 | } 189 | } 190 | 191 | 192 | internal class ContainerView: UIView { 193 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 194 | return true 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/DragCardDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragCardDataSource.swift 3 | // YHDragContainer 4 | // 5 | // Created by jun on 2021/10/18. 6 | // Copyright © 2021 yinhe. All rights reserved. 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import UIKit 49 | import ObjectiveC 50 | 51 | public protocol DragCardDataSource: NSObjectProtocol { 52 | func numberOfCards(_ dragCard: DragCardContainer) -> Int 53 | func dragCard(_ dragCard: DragCardContainer, viewForCard index: Int) -> DragCardView 54 | } 55 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/DragCardDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragCardDelegate.swift 3 | // YHDragContainer 4 | // 5 | // Created by jun on 2021/10/18. 6 | // Copyright © 2021 yinhe. All rights reserved. 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import UIKit 49 | import ObjectiveC 50 | 51 | public protocol DragCardDelegate: NSObjectProtocol { 52 | /// This method is called when the top card is displayed. 53 | func dragCard(_ dragCard: DragCardContainer, displayTopCardAt index: Int, with cardView: DragCardView) 54 | 55 | /// This method is called when the top card is removed. 56 | func dragCard(_ dragCard: DragCardContainer, didRemovedTopCardAt index: Int, direction: Direction, with cardView: DragCardView) 57 | 58 | /// This method is called when the last card is removed. 59 | /// **If `infiniteLoop` is true, this method not called** 60 | func dragCard(_ dragCard: DragCardContainer, didRemovedLast cardView: DragCardView) 61 | 62 | /// This method is called when the top card being tapped. 63 | func dragCard(_ dragCard: DragCardContainer, didSelectTopCardAt index: Int, with cardView: DragCardView) 64 | } 65 | 66 | extension DragCardDelegate { 67 | public func dragCard(_ dragCard: DragCardContainer, displayTopCardAt index: Int, with cardView: DragCardView) { } 68 | public func dragCard(_ dragCard: DragCardContainer, didRemovedTopCardAt index: Int, direction: Direction, with cardView: DragCardView) { } 69 | public func dragCard(_ dragCard: DragCardContainer, didRemovedLast cardView: DragCardView) { } 70 | public func dragCard(_ dragCard: DragCardContainer, didSelectTopCardAt index: Int, with cardView: DragCardView) { } 71 | } 72 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/DragCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragCardView.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/3. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import UIKit 49 | 50 | internal protocol CardDelegate: AnyObject { 51 | func cardDidBeginSwipe(_ card: DragCardView) 52 | func cardDidCancelSwipe(_ card: DragCardView) 53 | func cardDidContinueSwipe(_ card: DragCardView) 54 | func cardDidSwipe(_ card: DragCardView, withDirection direction: Direction) 55 | 56 | func cardDidDragout(_ card: DragCardView) 57 | 58 | func cardDidTap(_ card: DragCardView) 59 | } 60 | 61 | 62 | /// A wrapper over `UIView`. 63 | open class DragCardView: UIView { 64 | deinit { 65 | if Log.enableLog { 66 | print("\(self.classForCoder) deinit") 67 | } 68 | } 69 | 70 | internal let identifier: String = UUID().uuidString 71 | 72 | /// The swipe directions to be detected by the view. 73 | /// Set this variable to ignore certain directions. 74 | open var allowedDirection: [Direction] = Direction.horizontal 75 | 76 | /// The maximum rotation angle of the card, measured in degree. 77 | /// Defined as a value in the range `[0, 90]`. 78 | public var maximumRotationAngle: CGFloat = 20.0 { 79 | didSet { 80 | maximumRotationAngle = max(0, min(maximumRotationAngle, 90)) 81 | } 82 | } 83 | 84 | /// The duration of the spring-like animation applied when a swipe is canceled, measured in seconds. 85 | public var totalResetDuration: TimeInterval = 0.6 { 86 | didSet { 87 | totalResetDuration = max(.leastNormalMagnitude, totalResetDuration) 88 | } 89 | } 90 | 91 | /// The duration of the fade animation applied to the overlays after the reverse swipe translation. 92 | /// Measured relative to the total reverse swipe duration. 93 | /// 94 | /// Defined as a value in the range `[0, 1]`. 95 | public var relativeReverseSwipeOverlayFadeDuration: Double = 0.15 { 96 | didSet { 97 | relativeReverseSwipeOverlayFadeDuration = max(0, min(relativeReverseSwipeOverlayFadeDuration, 1)) 98 | } 99 | } 100 | 101 | /// The duration of the fade animation applied to the overlays before the swipe translation. 102 | /// Measured relative to the total swipe duration. 103 | /// 104 | /// Defined as a value in the range `[0, 1]`. 105 | public var relativeSwipeOverlayFadeDuration: Double = 0.15 { 106 | didSet { 107 | relativeSwipeOverlayFadeDuration = max(0, min(relativeSwipeOverlayFadeDuration, 1)) 108 | } 109 | } 110 | 111 | /// The damping coefficient of the spring-like animation applied when a swipe is canceled. 112 | public var resetSpringDamping: CGFloat = 0.5 { 113 | didSet { 114 | resetSpringDamping = max(0, min(resetSpringDamping, 1)) 115 | } 116 | } 117 | 118 | /// The total duration of the swipe animation, measured in seconds. 119 | public var totalSwipeDuration: TimeInterval = 0.7 { 120 | didSet { 121 | totalSwipeDuration = max(.leastNormalMagnitude, totalSwipeDuration) 122 | } 123 | } 124 | 125 | /// The total duration of the rewind animation, measured in seconds. 126 | public var totalRewindDuration: TimeInterval = 0.4 { 127 | didSet { 128 | totalRewindDuration = max(.leastNormalMagnitude, totalRewindDuration) 129 | } 130 | } 131 | 132 | /// The main content view. 133 | public lazy var contentView: UIView = { 134 | let contentView = UIView() 135 | return contentView 136 | }() 137 | 138 | /// The overlay alpha content view. 139 | private lazy var alphaOverlayContentView: UIView = { 140 | let alphaOverlayContentView = UIView() 141 | alphaOverlayContentView.isUserInteractionEnabled = false 142 | return alphaOverlayContentView 143 | }() 144 | 145 | /// The pan gesture recognizer attached to the view. 146 | public var panGestureRecognizer: PanGestureRecognizer? { 147 | for gesture in (gestureRecognizers ?? []) { 148 | if let _gesture_ = gesture as? PanGestureRecognizer { 149 | return _gesture_ 150 | } 151 | } 152 | return nil 153 | } 154 | 155 | /// The tap gesture recognizer attached to the view. 156 | public var tapGestureRecognizer: TapGestureRecognizer? { 157 | for gesture in (gestureRecognizers ?? []) { 158 | if let _gesture_ = gesture as? TapGestureRecognizer { 159 | return _gesture_ 160 | } 161 | } 162 | return nil 163 | } 164 | 165 | /// The minimum required speed on the intended direction to trigger a swipe. 166 | open func minimumSwipeSpeed(on direction: Direction) -> CGFloat { 167 | return 1100 168 | } 169 | 170 | /// The minimum required drag distance on the intended direction to trigger a swipe. 171 | /// Measured from the swipe's initial touch point. 172 | open func minimumSwipeDistance(on direction: Direction) -> CGFloat { 173 | return min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) / 4 174 | } 175 | 176 | internal weak var delegate: CardDelegate? 177 | 178 | internal var initialInfo: BasicInfo = .default 179 | 180 | internal var isOld: Bool = false 181 | 182 | private var internalTouchLocation: CGPoint? 183 | private var overlays: [Direction: UIView] = [:] 184 | 185 | public override init(frame: CGRect) { 186 | super.init(frame: frame) 187 | initUI() 188 | setupUI() 189 | } 190 | 191 | required public init?(coder: NSCoder) { 192 | super.init(coder: coder) 193 | initUI() 194 | setupUI() 195 | } 196 | } 197 | 198 | extension DragCardView { 199 | open override func layoutSubviews() { 200 | super.layoutSubviews() 201 | contentView.frame = bounds 202 | alphaOverlayContentView.frame = bounds 203 | overlays.values.forEach { $0.frame = alphaOverlayContentView.bounds } 204 | bringSubviewToFront(alphaOverlayContentView) 205 | } 206 | } 207 | 208 | extension DragCardView { 209 | private func initUI() { 210 | addPanGesture() 211 | addTapGesture() 212 | } 213 | 214 | private func setupUI() { 215 | addSubview(contentView) 216 | addSubview(alphaOverlayContentView) 217 | } 218 | } 219 | 220 | extension DragCardView { 221 | internal func removePanGesture() { 222 | for gesture in (gestureRecognizers ?? []) { 223 | if let _gesture_ = gesture as? PanGestureRecognizer { 224 | removeGestureRecognizer(_gesture_) 225 | } 226 | } 227 | } 228 | 229 | internal func removeTapGesture() { 230 | for gesture in (gestureRecognizers ?? []) { 231 | if let _gesture_ = gesture as? TapGestureRecognizer { 232 | removeGestureRecognizer(_gesture_) 233 | } 234 | } 235 | } 236 | 237 | internal func addPanGesture() { 238 | removePanGesture() 239 | let panGesture = PanGestureRecognizer(target: self, action: #selector(handlePan(_:))) 240 | addGestureRecognizer(panGesture) 241 | } 242 | 243 | internal func addTapGesture() { 244 | removeTapGesture() 245 | let tapGesture = TapGestureRecognizer(target: self, action: #selector(handleTap(_:))) 246 | addGestureRecognizer(tapGesture) 247 | } 248 | } 249 | 250 | extension DragCardView { 251 | public func swipe(to direction: Direction) { 252 | dragout(direction: direction, forced: true) 253 | } 254 | 255 | public func rewind(from direction: Direction) { 256 | isUserInteractionEnabled = false 257 | 258 | removeAllAnimations() 259 | 260 | let finalTransform = finalTransform(direction, forced: true) 261 | 262 | transform = finalTransform 263 | 264 | for _direction in (allowedDirection.filter { $0 != direction }) { 265 | overlay(forDirection: _direction)?.alpha = 0.0 266 | } 267 | 268 | let overlay = overlay(forDirection: direction) 269 | overlay?.alpha = 1.0 270 | 271 | let relativeOverlayDuration = overlay != nil ? relativeReverseSwipeOverlayFadeDuration : 0.0 272 | 273 | UIView.animateKeyframes(withDuration: totalRewindDuration, 274 | delay: 0, 275 | options: [.calculationModeLinear]) { 276 | UIView.addKeyframe(withRelativeStartTime: 0, 277 | relativeDuration: 1.0 - relativeOverlayDuration) { 278 | self.transform = .identity 279 | } 280 | UIView.addKeyframe(withRelativeStartTime: 1.0 - relativeOverlayDuration, 281 | relativeDuration: relativeOverlayDuration) { 282 | overlay?.alpha = 0.0 283 | } 284 | } completion: { finished in 285 | if !finished { return } 286 | self.isUserInteractionEnabled = true 287 | 288 | if self.isOld { 289 | self.removeFromSuperview() 290 | } 291 | } 292 | } 293 | 294 | public func removeAllAnimations() { 295 | layer.removeAllAnimations() 296 | for o in overlays.values { 297 | o.layer.removeAllAnimations() 298 | } 299 | } 300 | } 301 | 302 | extension DragCardView { 303 | internal func finalDuration(_ direction: Direction, forced: Bool) -> TimeInterval { 304 | if forced { 305 | return totalSwipeDuration 306 | } 307 | 308 | let velocityFactor = dragSpeed(on: direction) / minimumSwipeSpeed(on: direction) 309 | 310 | // Card swiped below the minimum swipe speed. 311 | if velocityFactor < 1.0 { 312 | return totalSwipeDuration 313 | } 314 | 315 | // Card swiped at least the minimum swipe speed -> return relative duration 316 | return 1.0 / TimeInterval(velocityFactor) 317 | } 318 | 319 | private func finalTransform(_ direction: Direction, forced: Bool) -> CGAffineTransform { 320 | let dragTranslation = CGVector(to: panGestureRecognizer?.translation(in: superview) ?? .zero) 321 | 322 | let normalizedDragTranslation = forced ? direction.vector : dragTranslation.normalized 323 | 324 | let actualTranslation = CGPoint(finalTranslation(direction, directionVector: normalizedDragTranslation)) 325 | let actualRotationAngle = finalRotationAngle(direction: direction, forced: forced) 326 | 327 | let transform = CGAffineTransform(translationX: actualTranslation.x + initialInfo.translation.x, y: actualTranslation.y + initialInfo.translation.y) 328 | return transform.rotated(by: actualRotationAngle + initialInfo.rotationAngle).scaledBy(x: initialInfo.scale, y: initialInfo.scale) 329 | 330 | // let t1 = CGAffineTransform(translationX: actualTranslation.x + initialInfo.translation.x, y: actualTranslation.y + initialInfo.translation.y) 331 | // let t2 = CGAffineTransform(rotationAngle: actualRotationAngle + initialInfo.rotationAngle) 332 | // let t3 = CGAffineTransform(scaleX: initialInfo.scale, y: initialInfo.scale) 333 | // return t1.concatenating(t2).concatenating(t3) 334 | } 335 | 336 | private func finalTranslation(_ direction: Direction, directionVector: CGVector) -> CGVector { 337 | let maxScreenLength = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height) 338 | let minimumOffscreenTranslation = CGVector(dx: maxScreenLength, 339 | dy: maxScreenLength) 340 | return CGVector(dx: directionVector.dx * minimumOffscreenTranslation.dx, 341 | dy: directionVector.dy * minimumOffscreenTranslation.dy) 342 | } 343 | 344 | private func finalRotationAngle(direction: Direction, forced: Bool) -> CGFloat { 345 | if direction == .up || direction == .down { return .zero } 346 | 347 | let rotationDirectionY: CGFloat = direction == .left ? -1.0 : 1.0 348 | 349 | if forced { 350 | return rotationDirectionY * maximumRotationAngle.radius 351 | } 352 | 353 | guard let touchPoint = internalTouchLocation else { 354 | return rotationDirectionY * maximumRotationAngle.radius 355 | } 356 | 357 | if (direction == .left && touchPoint.y < bounds.height / 2.0) || (direction == .right && touchPoint.y >= bounds.height / 2) { 358 | return -maximumRotationAngle.radius 359 | } 360 | return maximumRotationAngle.radius 361 | } 362 | } 363 | 364 | extension DragCardView { 365 | private func panForTransform() -> CGAffineTransform { 366 | func panForRotationAngle() -> CGFloat { 367 | let superviewTranslation = panGestureRecognizer?.translation(in: superview) ?? .zero 368 | let percentage = min(abs(superviewTranslation.x) / UIScreen.main.bounds.width, 1) 369 | 370 | if superviewTranslation.x.isEqual(to: .zero) { 371 | return .zero 372 | } 373 | 374 | guard let touchPoint = internalTouchLocation else { 375 | return .zero 376 | } 377 | 378 | if (superviewTranslation.x.isLess(than: .zero) && touchPoint.y < bounds.height / 2.0) || 379 | (!superviewTranslation.x.isLessThanOrEqualTo(.zero) && touchPoint.y >= bounds.height / 2) { 380 | return -percentage * maximumRotationAngle.radius 381 | } 382 | 383 | return percentage * maximumRotationAngle.radius 384 | } 385 | 386 | let dragTranslation = panGestureRecognizer?.translation(in: superview) ?? .zero 387 | 388 | let transform = CGAffineTransform(translationX: dragTranslation.x + initialInfo.translation.x, y: dragTranslation.y + initialInfo.translation.y) 389 | return transform.rotated(by: panForRotationAngle() + initialInfo.rotationAngle).scaledBy(x: initialInfo.scale, y: initialInfo.scale) 390 | 391 | // let t1 = CGAffineTransform(translationX: dragTranslation.x + initialInfo.translation.x, y: dragTranslation.y + initialInfo.translation.y) 392 | // let t2 = CGAffineTransform(rotationAngle: panForRotationAngle() + initialInfo.rotationAngle) 393 | // let t3 = CGAffineTransform(scaleX: initialInfo.scale, y: initialInfo.scale) 394 | // return t1.concatenating(t2).concatenating(t3) 395 | } 396 | 397 | private func panForOverlayPercentage(_ direction: Direction) -> CGFloat { 398 | if direction != activeDirection() { return 0 } 399 | let totalPercentage = allowedDirection.reduce(0) { sum, direction in 400 | return sum + dragPercentage(on: direction) 401 | } 402 | let actualPercentage = 2 * dragPercentage(on: direction) - totalPercentage 403 | return max(0, min(actualPercentage, 1)) 404 | } 405 | 406 | private func dragout(direction: Direction, forced: Bool) { 407 | isUserInteractionEnabled = false 408 | 409 | removeAllAnimations() 410 | 411 | let finalDuration = finalDuration(direction, forced: forced) 412 | let finalTransform = finalTransform(direction, forced: forced) 413 | 414 | let overlay = overlay(forDirection: direction) 415 | 416 | let relativeOverlayDuration = (forced && overlay != nil) ? relativeSwipeOverlayFadeDuration : 0.0 417 | 418 | UIView.animateKeyframes(withDuration: finalDuration, 419 | delay: 0, 420 | options: [.calculationModeLinear]) { 421 | for _direction in self.allowedDirection.filter({ $0 != direction }) { 422 | self.overlay(forDirection: _direction)?.alpha = 0.0 423 | } 424 | UIView.addKeyframe(withRelativeStartTime: 0, 425 | relativeDuration: relativeOverlayDuration) { 426 | overlay?.alpha = 1 427 | } 428 | UIView.addKeyframe(withRelativeStartTime: relativeOverlayDuration, 429 | relativeDuration: 1.0 - relativeOverlayDuration) { 430 | self.transform = finalTransform 431 | } 432 | } completion: { finished in 433 | if !finished { return } 434 | self.isUserInteractionEnabled = true 435 | self.delegate?.cardDidDragout(self) 436 | 437 | if self.isOld { 438 | self.removeFromSuperview() 439 | } 440 | } 441 | } 442 | } 443 | 444 | extension DragCardView { 445 | /// The active swipe direction on the view. 446 | public func activeDirection() -> Direction? { 447 | return allowedDirection.reduce((CGFloat.zero, nil)) { [weak self] lastResult, direction in 448 | guard let self = self else { return lastResult } 449 | let dragPercentage = self.dragPercentage(on: direction) 450 | return (dragPercentage > lastResult.0) ? (dragPercentage, direction) : lastResult 451 | }.1 452 | } 453 | 454 | /// The speed of the current drag velocity. 455 | public func dragSpeed(on direction: Direction) -> CGFloat { 456 | let velocity = panGestureRecognizer?.velocity(in: superview) ?? .zero 457 | return abs(direction.vector * CGVector(to: velocity)) 458 | } 459 | 460 | /// The percentage of `minimumSwipeDistance` the current drag translation attains in the specified direction. 461 | public func dragPercentage(on direction: Direction) -> CGFloat { 462 | let translation = CGVector(to: panGestureRecognizer?.translation(in: superview) ?? .zero) 463 | let scaleFactor = 1 / minimumSwipeDistance(on: direction) 464 | let percentage = scaleFactor * (translation * direction.vector) 465 | return percentage < 0 ? 0 : percentage 466 | } 467 | } 468 | 469 | extension DragCardView { 470 | public func setOverlay(_ overlay: UIView?, forDirection direction: Direction) { 471 | overlays[direction]?.removeFromSuperview() 472 | overlays[direction] = overlay 473 | 474 | if let overlay = overlay { 475 | alphaOverlayContentView.addSubview(overlay) 476 | overlay.alpha = 0 477 | overlay.isUserInteractionEnabled = false 478 | } 479 | } 480 | 481 | public func setOverlays(_ overlays: [Direction: UIView]) { 482 | for (direction, overlay) in overlays { 483 | setOverlay(overlay, forDirection: direction) 484 | } 485 | } 486 | 487 | public func overlay(forDirection direction: Direction) -> UIView? { 488 | return overlays[direction] 489 | } 490 | } 491 | 492 | extension DragCardView { 493 | @objc private func handlePan(_ recognizer: PanGestureRecognizer) { 494 | switch recognizer.state { 495 | case .possible, .began: 496 | removeAllAnimations() 497 | internalTouchLocation = recognizer.location(in: self) 498 | delegate?.cardDidBeginSwipe(self) 499 | case .changed: 500 | transform = panForTransform() 501 | for (direction, overlay) in overlays { 502 | overlay.alpha = panForOverlayPercentage(direction) 503 | } 504 | delegate?.cardDidContinueSwipe(self) 505 | case .ended, .cancelled: 506 | if let direction = activeDirection() { 507 | if dragSpeed(on: direction) >= minimumSwipeSpeed(on: direction) || dragPercentage(on: direction) >= 1 { 508 | // dragout 509 | delegate?.cardDidSwipe(self, withDirection: direction) 510 | dragout(direction: direction, forced: false) 511 | return 512 | } 513 | } 514 | // reset 515 | removeAllAnimations() 516 | delegate?.cardDidCancelSwipe(self) 517 | 518 | UIView.animate(withDuration: totalResetDuration, 519 | delay: 0, 520 | usingSpringWithDamping: resetSpringDamping, 521 | initialSpringVelocity: 0, 522 | options: [.curveLinear, .allowUserInteraction]) { 523 | self.transform = self.initialInfo.transform 524 | self.overlays.values.forEach{ $0.alpha = 0 } 525 | } completion: { finished in 526 | if !finished { return } 527 | if self.isOld { 528 | self.removeFromSuperview() 529 | } 530 | } 531 | default: 532 | break 533 | } 534 | } 535 | } 536 | 537 | extension DragCardView { 538 | @objc private func handleTap(_ recognizer: TapGestureRecognizer) { 539 | delegate?.cardDidTap(self) 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/Gesture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gesture.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/4/14. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import UIKit 49 | 50 | public final class PanGestureRecognizer: UIPanGestureRecognizer { } 51 | 52 | public final class TapGestureRecognizer: UITapGestureRecognizer { } 53 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Log.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/4/20. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | 49 | internal struct Log { 50 | internal static var enableLog: Bool = false 51 | } 52 | 53 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/Metrics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Metrics.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/1. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import UIKit 49 | 50 | internal final class Metrics { 51 | internal let visibleCount: Int 52 | internal let infiniteLoop: Bool 53 | 54 | internal let cardAnchorPoint: CGPoint 55 | internal let cardFrame: CGRect 56 | 57 | internal let numberOfCards: Int 58 | internal let basicInfos: [BasicInfo] 59 | 60 | internal var minimumBasicInfo: BasicInfo { 61 | assert(!basicInfos.isEmpty, "`basicInfos` can not empty") 62 | return basicInfos.last! 63 | } 64 | 65 | internal var maximumBasicInfo: BasicInfo { 66 | assert(!basicInfos.isEmpty, "`basicInfos` can not empty") 67 | return basicInfos.first! 68 | } 69 | 70 | internal init(engine: CardEngine) { 71 | let numberOfCards = engine.cardDataSource.numberOfCards(engine.cardContainer) 72 | assert(numberOfCards >= 0, "`numberOfCards` must be greater or equal to 0") 73 | self.numberOfCards = numberOfCards 74 | 75 | let visibleCount = engine.cardContainer.visibleCount 76 | assert(visibleCount > 0, "`visibleCount` must be greater than 0") 77 | self.visibleCount = visibleCount 78 | 79 | let mode = engine.cardContainer.mode 80 | self.infiniteLoop = engine.cardContainer.infiniteLoop 81 | 82 | self.basicInfos = mode.basicInfos(visibleCount: visibleCount, 83 | containerSize: engine.cardContainer.bounds.size) 84 | self.cardAnchorPoint = mode.cardAnchorPoint() 85 | self.cardFrame = mode.cardFrame(visibleCount: visibleCount, containerSize: engine.cardContainer.bounds.size) 86 | } 87 | 88 | internal static let `default` = Metrics(visibleCount: Default.visibleCount, 89 | infiniteLoop: Default.infiniteLoop, 90 | cardAnchorPoint: Default.cardAnchorPoint, 91 | cardFrame: Default.cardFrame, 92 | numberOfCards: 0, 93 | basicInfos: []) 94 | 95 | private init(visibleCount: Int, 96 | infiniteLoop: Bool, 97 | cardAnchorPoint: CGPoint, 98 | cardFrame: CGRect, 99 | numberOfCards: Int, 100 | basicInfos: [BasicInfo]) { 101 | self.visibleCount = visibleCount 102 | self.infiniteLoop = infiniteLoop 103 | self.cardAnchorPoint = cardAnchorPoint 104 | self.cardFrame = cardFrame 105 | self.numberOfCards = numberOfCards 106 | self.basicInfos = basicInfos 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/Mode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mode.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/3. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | 49 | public protocol Mode { 50 | func cardAnchorPoint() -> CGPoint 51 | func cardFrame(visibleCount: Int, containerSize: CGSize) -> CGRect 52 | func basicInfos(visibleCount: Int, containerSize: CGSize) -> [BasicInfo] 53 | } 54 | -------------------------------------------------------------------------------- /DragCardContainer/Sources/ScaleMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScaleMode.swift 3 | // DragCardContainer 4 | // 5 | // Created by dfsx6 on 2023/3/3. 6 | // 7 | // 8 | // 9 | // ┌───────────────────────────────────────────┐ 10 | // │*******************************************│ 11 | // │*******************************************│ 12 | // │*******************************************│ 13 | // │*******************************************│ 14 | // │*******************************************│ 15 | // │*******************************************│ 16 | // │*******************************************│ 17 | // │*******************************************│ 18 | // │*******************************************│ 19 | // │*******************************************│ 20 | // │*******************************************│ 21 | // │*******************************************│ 22 | // │*******************************************│ 23 | // │*******************************************│ 24 | // │*******************************************│ 25 | // │*******************************************│ 26 | // │*******************************************│ 27 | // │*******************************************│ 28 | // │*******************************************│ 29 | // │*******************************************│ 30 | // │*******************************************│ 31 | // │*******************************************│ 32 | // │*******************************************│ 33 | // │*******************************************│ 34 | // │*******************************************│ 35 | // │*******************************************│ 36 | // │*******************************************│ 37 | // │*******************************************│ 38 | // │*******************************************│ 39 | // │*******************************************│ 40 | // │*******************************************│ 41 | // │*******************************************│ 42 | // │*******************************************│ 43 | // └┬─────────────────────────────────────────┬┘ 44 | // └┬───────────────────────────────────────┬┘ 45 | // └───────────────────────────────────────┘ 46 | 47 | import Foundation 48 | import QuartzCore 49 | 50 | public final class ScaleMode { 51 | 52 | public enum Direction: Equatable { 53 | case top 54 | case bottom 55 | case left 56 | case right 57 | case center 58 | } 59 | 60 | /// Card spacing. 61 | /// 62 | /// - Warning: if `direction = .center`. This attribute is ignored. 63 | public var cardSpacing: CGFloat = 10.0 { 64 | didSet { 65 | cardSpacing = max(.zero, cardSpacing) 66 | } 67 | } 68 | 69 | /// Minimum card scale. 70 | public var minimumScale: CGFloat = 0.8 { 71 | didSet { 72 | minimumScale = max(.zero, min(minimumScale, 1.0)) 73 | } 74 | } 75 | 76 | /// Minimum card alpha. 77 | public var minimumCardAlpha: CGFloat = 0.2 { 78 | didSet { 79 | minimumCardAlpha = max(.zero, min(minimumCardAlpha, 1.0)) 80 | } 81 | } 82 | 83 | /// Maximum rotaion angle. 84 | public var maximumAngle: CGFloat = 0 { 85 | didSet { 86 | maximumAngle = max(0, min(maximumAngle, 90)) 87 | } 88 | } 89 | 90 | /// Scale direction. 91 | public var direction: ScaleMode.Direction = .bottom 92 | 93 | public static let `default` = ScaleMode() 94 | 95 | public init() {} 96 | } 97 | 98 | extension ScaleMode: Mode { 99 | public func cardAnchorPoint() -> CGPoint { 100 | switch direction { 101 | case .bottom: 102 | return CGPoint(x: 0.5, y: 1.0) 103 | case .top: 104 | return CGPoint(x: 0.5, y: 0) 105 | case .left: 106 | return CGPoint(x: 0, y: 0.5) 107 | case .right: 108 | return CGPoint(x: 1, y: 0.5) 109 | case .center: 110 | return CGPoint(x: 0.5, y: 0.5) 111 | } 112 | } 113 | 114 | public func cardFrame(visibleCount: Int, containerSize: CGSize) -> CGRect { 115 | switch direction { 116 | case .bottom: 117 | let normalCardWidth = containerSize.width 118 | let normalCardHeight = containerSize.height - (CGFloat(visibleCount - 1) * CGFloat(cardSpacing)) 119 | let normalCardFrame = CGRect(x: .zero, y: .zero, width: normalCardWidth, height: normalCardHeight) 120 | return normalCardFrame 121 | case .top: 122 | let normalCardWidth = containerSize.width 123 | let normalCardHeight = containerSize.height - (CGFloat(visibleCount - 1) * CGFloat(cardSpacing)) 124 | let normalCardFrame = CGRect(x: .zero, y: cardSpacing * CGFloat(visibleCount - 1), width: normalCardWidth, height: normalCardHeight) 125 | return normalCardFrame 126 | case .left: 127 | let normalCardWidth = containerSize.width - (CGFloat(visibleCount - 1) * CGFloat(cardSpacing)) 128 | let normalCardHeight = containerSize.height 129 | let normalCardFrame = CGRect(x: cardSpacing * CGFloat(visibleCount - 1), y: .zero, width: normalCardWidth, height: normalCardHeight) 130 | return normalCardFrame 131 | case .right: 132 | let normalCardWidth = containerSize.width - (CGFloat(visibleCount - 1) * CGFloat(cardSpacing)) 133 | let normalCardHeight = containerSize.height 134 | let normalCardFrame = CGRect(x: .zero, y: .zero, width: normalCardWidth, height: normalCardHeight) 135 | return normalCardFrame 136 | case .center: 137 | let normalCardWidth = containerSize.width 138 | let normalCardHeight = containerSize.height 139 | let normalCardFrame = CGRect(x: .zero, y: .zero, width: normalCardWidth, height: normalCardHeight) 140 | return normalCardFrame 141 | } 142 | } 143 | 144 | public func basicInfos(visibleCount: Int, containerSize: CGSize) -> [BasicInfo] { 145 | var magnitudeScale: CGFloat 146 | if visibleCount <= 1 { 147 | magnitudeScale = 1 148 | } else { 149 | magnitudeScale = CGFloat(1.0 - minimumScale) / CGFloat(visibleCount - 1) 150 | } 151 | 152 | var magnitudeAlpha: CGFloat 153 | if visibleCount <= 1 { 154 | magnitudeAlpha = 1 155 | } else { 156 | magnitudeAlpha = CGFloat(1.0 - minimumCardAlpha) / CGFloat(visibleCount - 1) 157 | } 158 | 159 | var basicInfos: [BasicInfo] = [] 160 | 161 | switch direction { 162 | case .bottom: 163 | for i in 0.. 27 | 28 | ## Getting Start 29 | 30 | ### Requirements 31 | 32 | - Deployment target iOS 11.0+ 33 | - Swift 5+ 34 | - Xcode 14+ 35 | 36 | ### Installation 37 | 38 | #### CocoaPods 39 | 40 | ```ruby 41 | pod 'DragCardContainer' 42 | ``` 43 | 44 | Or 45 | 46 | ```ruby 47 | pod 'DragCardContainer', :git => "https://github.com/liujunliuhong/DragCardContainer.git" 48 | ``` 49 | 50 | ### Usage 51 | 52 | #### Setting up the card view 53 | 54 | Create your own card container and setting its properties directly. 55 | 56 | ```swift 57 | let cardContainer = DragCardContainer() 58 | // 是否可以无限滑动 59 | cardContainer.infiniteLoop = false 60 | // 数据源 61 | cardContainer.dataSource = self 62 | // 代理 63 | cardContainer.delegate = self 64 | // 可见卡片数量 65 | cardContainer.visibleCount = 3 66 | // 是否可以打印日志 67 | cardContainer.enableLog = true 68 | // 是否禁用卡片拖动 69 | cardContainer.disableTopCardDrag = false 70 | // 是否禁用卡片点击 71 | cardContainer.disableTopCardClick = false 72 | ``` 73 | 74 | #### Mode 75 | 76 | You can custom `mode`. `Mode` is a protocol, `ScaleMode` implements the `Mode` protocol. 77 | 78 | ```swift 79 | let mode = ScaleMode() 80 | // 卡片之间间距 81 | mode.cardSpacing = 10 82 | // 方向(可以运行Demo,修改该参数看实际效果) 83 | mode.direction = .bottom 84 | // 最小缩放比例 85 | mode.minimumScale = 0.7 86 | // 卡片最大旋转角度 87 | mode.maximumAngle = 0 88 | // 赋值mode 89 | cardContainer.mode = mode 90 | ``` 91 | 92 | #### Configuring the card 93 | 94 | Custom Card, inherited from `DragCardView`. 95 | 96 | ```swift 97 | public final class CardView: DragCardView { } 98 | ``` 99 | 100 | #### Configuring the datasource 101 | 102 | You must conform the protocol `DragCardDataSource`. 103 | 104 | ```swift 105 | public func numberOfCards(_ dragCard: DragCardContainer) -> Int { 106 | return 10 107 | } 108 | 109 | public func dragCard(_ dragCard: DragCardContainer, viewForCard index: Int) -> DragCardView { 110 | let cardView = CardView() 111 | cardView.allowedDirection = [.left, .right] 112 | return cardView 113 | } 114 | ``` 115 | 116 | #### Configuring the delegate 117 | 118 | The protocol `DragCardDelegate` is optional. 119 | 120 | ```swift 121 | public func dragCard(_ dragCard: DragCardContainer, displayTopCardAt index: Int, with cardView: DragCardView) { 122 | print("displayTopCardAt: \(index)") 123 | } 124 | 125 | public func dragCard(_ dragCard: DragCardContainer, didRemovedTopCardAt index: Int, direction: Direction, with cardView: DragCardView) { 126 | print("didRemovedTopCardAt: \(index)") 127 | } 128 | 129 | public func dragCard(_ dragCard: DragCardContainer, didRemovedLast cardView: DragCardView) { 130 | print("didRemovedLast") 131 | } 132 | 133 | public func dragCard(_ dragCard: DragCardContainer, didSelectTopCardAt index: Int, with cardView: DragCardView) { 134 | print("didSelectTopCardAt: \(index)") 135 | } 136 | ``` 137 | 138 | #### Moving Views 139 | 140 | ##### Swiping programmatically 141 | 142 | The user can swipe views in the allowed directions. This can also happen programmatically. 143 | 144 | ```swift 145 | cardContainer.swipeTopCard(to: .right) 146 | ``` 147 | 148 | ##### Rewinding 149 | 150 | Returns the most recently swiped card to the top of the card stack. 151 | 152 | ```swift 153 | cardContainer.rewind(from: .right) 154 | ``` 155 | 156 | #### Set top level card index 157 | 158 | You can set top level card index. 159 | 160 | ```swift 161 | cardContainer.currentTopIndex = 2 162 | ``` 163 | 164 | ### Author 165 | 166 | liujun, universegalaxy96@gmail.com 167 | -------------------------------------------------------------------------------- /iOS Example/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '11.0' 3 | target 'iOS Example' do 4 | use_frameworks! 5 | pod 'SnapKit' 6 | end 7 | -------------------------------------------------------------------------------- /iOS Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SnapKit (5.6.0) 3 | 4 | DEPENDENCIES: 5 | - SnapKit 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - SnapKit 10 | 11 | SPEC CHECKSUMS: 12 | SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 13 | 14 | PODFILE CHECKSUM: 44f89e2e25649bf1f9eb0d16802d284e846b4bf0 15 | 16 | COCOAPODS: 1.11.3 17 | -------------------------------------------------------------------------------- /iOS Example/iOS Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1885F2FA3A6B8CBF22EA66E3 /* Pods_iOS_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AE42063DCDE93D2B0037C60 /* Pods_iOS_Example.framework */; }; 11 | 85F076DB29B243E2007F4504 /* CardOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F076DA29B243E2007F4504 /* CardOverlayView.swift */; }; 12 | D107965629B1D16E003CC604 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107965529B1D16E003CC604 /* CardView.swift */; }; 13 | D107965829B1D306003CC604 /* OverlayLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107965729B1D306003CC604 /* OverlayLabelView.swift */; }; 14 | D14DC70429B584EF00DCC01C /* BottomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14DC70329B584EF00DCC01C /* BottomView.swift */; }; 15 | D14DC70629B587E200DCC01C /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14DC70529B587E200DCC01C /* DetailViewController.swift */; }; 16 | D15CD6CB29A30C8D00086A1B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15CD6CA29A30C8D00086A1B /* AppDelegate.swift */; }; 17 | D15CD6CF29A30C8D00086A1B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15CD6CE29A30C8D00086A1B /* ViewController.swift */; }; 18 | D15CD6D229A30C8D00086A1B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D15CD6D029A30C8D00086A1B /* Main.storyboard */; }; 19 | D15CD6D429A30C8E00086A1B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D15CD6D329A30C8E00086A1B /* Assets.xcassets */; }; 20 | D15CD6D729A30C8E00086A1B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D15CD6D529A30C8E00086A1B /* LaunchScreen.storyboard */; }; 21 | D19E8C7229ACADD000B17234 /* DragCardContainer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D19E8C7129ACADD000B17234 /* DragCardContainer.framework */; }; 22 | D19E8C7329ACADD000B17234 /* DragCardContainer.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D19E8C7129ACADD000B17234 /* DragCardContainer.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXCopyFilesBuildPhase section */ 26 | D19E8C7429ACADD000B17234 /* Embed Frameworks */ = { 27 | isa = PBXCopyFilesBuildPhase; 28 | buildActionMask = 2147483647; 29 | dstPath = ""; 30 | dstSubfolderSpec = 10; 31 | files = ( 32 | D19E8C7329ACADD000B17234 /* DragCardContainer.framework in Embed Frameworks */, 33 | ); 34 | name = "Embed Frameworks"; 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXCopyFilesBuildPhase section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 5AE42063DCDE93D2B0037C60 /* Pods_iOS_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 8281919F37B8A1A62947D281 /* Pods-iOS Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS Example.release.xcconfig"; path = "Target Support Files/Pods-iOS Example/Pods-iOS Example.release.xcconfig"; sourceTree = ""; }; 42 | 85F076DA29B243E2007F4504 /* CardOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardOverlayView.swift; sourceTree = ""; }; 43 | C68F1CC37F74AAB7C363B2CC /* Pods-iOS Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS Example.debug.xcconfig"; path = "Target Support Files/Pods-iOS Example/Pods-iOS Example.debug.xcconfig"; sourceTree = ""; }; 44 | D107965529B1D16E003CC604 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; 45 | D107965729B1D306003CC604 /* OverlayLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayLabelView.swift; sourceTree = ""; }; 46 | D14DC70329B584EF00DCC01C /* BottomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomView.swift; sourceTree = ""; }; 47 | D14DC70529B587E200DCC01C /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 48 | D15CD6C729A30C8D00086A1B /* iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | D15CD6CA29A30C8D00086A1B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50 | D15CD6CE29A30C8D00086A1B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 51 | D15CD6D129A30C8D00086A1B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 52 | D15CD6D329A30C8E00086A1B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 53 | D15CD6D629A30C8E00086A1B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 54 | D15CD6D829A30C8E00086A1B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | D19E8C7129ACADD000B17234 /* DragCardContainer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DragCardContainer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | D15CD6C429A30C8D00086A1B /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | D19E8C7229ACADD000B17234 /* DragCardContainer.framework in Frameworks */, 64 | 1885F2FA3A6B8CBF22EA66E3 /* Pods_iOS_Example.framework in Frameworks */, 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXFrameworksBuildPhase section */ 69 | 70 | /* Begin PBXGroup section */ 71 | 7D1431112FFEA6C362D75644 /* Frameworks */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | D19E8C7129ACADD000B17234 /* DragCardContainer.framework */, 75 | 5AE42063DCDE93D2B0037C60 /* Pods_iOS_Example.framework */, 76 | ); 77 | name = Frameworks; 78 | sourceTree = ""; 79 | }; 80 | 8BBDDB377DBF7A983937A4F8 /* Pods */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | C68F1CC37F74AAB7C363B2CC /* Pods-iOS Example.debug.xcconfig */, 84 | 8281919F37B8A1A62947D281 /* Pods-iOS Example.release.xcconfig */, 85 | ); 86 | path = Pods; 87 | sourceTree = ""; 88 | }; 89 | D15CD6BE29A30C8D00086A1B = { 90 | isa = PBXGroup; 91 | children = ( 92 | D15CD6C929A30C8D00086A1B /* iOS Example */, 93 | D15CD6C829A30C8D00086A1B /* Products */, 94 | 8BBDDB377DBF7A983937A4F8 /* Pods */, 95 | 7D1431112FFEA6C362D75644 /* Frameworks */, 96 | ); 97 | sourceTree = ""; 98 | }; 99 | D15CD6C829A30C8D00086A1B /* Products */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | D15CD6C729A30C8D00086A1B /* iOS Example.app */, 103 | ); 104 | name = Products; 105 | sourceTree = ""; 106 | }; 107 | D15CD6C929A30C8D00086A1B /* iOS Example */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | D15CD6CA29A30C8D00086A1B /* AppDelegate.swift */, 111 | D15CD6F729A30D6100086A1B /* Demo */, 112 | D15CD6D029A30C8D00086A1B /* Main.storyboard */, 113 | D15CD6D329A30C8E00086A1B /* Assets.xcassets */, 114 | D15CD6D529A30C8E00086A1B /* LaunchScreen.storyboard */, 115 | D15CD6D829A30C8E00086A1B /* Info.plist */, 116 | ); 117 | path = "iOS Example"; 118 | sourceTree = ""; 119 | }; 120 | D15CD6F729A30D6100086A1B /* Demo */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | D15CD6CE29A30C8D00086A1B /* ViewController.swift */, 124 | D107965529B1D16E003CC604 /* CardView.swift */, 125 | 85F076DA29B243E2007F4504 /* CardOverlayView.swift */, 126 | D107965729B1D306003CC604 /* OverlayLabelView.swift */, 127 | D14DC70329B584EF00DCC01C /* BottomView.swift */, 128 | D14DC70529B587E200DCC01C /* DetailViewController.swift */, 129 | ); 130 | path = Demo; 131 | sourceTree = ""; 132 | }; 133 | /* End PBXGroup section */ 134 | 135 | /* Begin PBXNativeTarget section */ 136 | D15CD6C629A30C8D00086A1B /* iOS Example */ = { 137 | isa = PBXNativeTarget; 138 | buildConfigurationList = D15CD6DB29A30C8E00086A1B /* Build configuration list for PBXNativeTarget "iOS Example" */; 139 | buildPhases = ( 140 | F32376B9F90CFA440DD059D9 /* [CP] Check Pods Manifest.lock */, 141 | D15CD6C329A30C8D00086A1B /* Sources */, 142 | D15CD6C429A30C8D00086A1B /* Frameworks */, 143 | D15CD6C529A30C8D00086A1B /* Resources */, 144 | 4227C07D22FF257543F71878 /* [CP] Embed Pods Frameworks */, 145 | D19E8C7429ACADD000B17234 /* Embed Frameworks */, 146 | ); 147 | buildRules = ( 148 | ); 149 | dependencies = ( 150 | ); 151 | name = "iOS Example"; 152 | productName = "iOS Example"; 153 | productReference = D15CD6C729A30C8D00086A1B /* iOS Example.app */; 154 | productType = "com.apple.product-type.application"; 155 | }; 156 | /* End PBXNativeTarget section */ 157 | 158 | /* Begin PBXProject section */ 159 | D15CD6BF29A30C8D00086A1B /* Project object */ = { 160 | isa = PBXProject; 161 | attributes = { 162 | BuildIndependentTargetsInParallel = 1; 163 | LastSwiftUpdateCheck = 1410; 164 | LastUpgradeCheck = 1410; 165 | TargetAttributes = { 166 | D15CD6C629A30C8D00086A1B = { 167 | CreatedOnToolsVersion = 14.1; 168 | }; 169 | }; 170 | }; 171 | buildConfigurationList = D15CD6C229A30C8D00086A1B /* Build configuration list for PBXProject "iOS Example" */; 172 | compatibilityVersion = "Xcode 14.0"; 173 | developmentRegion = en; 174 | hasScannedForEncodings = 0; 175 | knownRegions = ( 176 | en, 177 | Base, 178 | ); 179 | mainGroup = D15CD6BE29A30C8D00086A1B; 180 | productRefGroup = D15CD6C829A30C8D00086A1B /* Products */; 181 | projectDirPath = ""; 182 | projectRoot = ""; 183 | targets = ( 184 | D15CD6C629A30C8D00086A1B /* iOS Example */, 185 | ); 186 | }; 187 | /* End PBXProject section */ 188 | 189 | /* Begin PBXResourcesBuildPhase section */ 190 | D15CD6C529A30C8D00086A1B /* Resources */ = { 191 | isa = PBXResourcesBuildPhase; 192 | buildActionMask = 2147483647; 193 | files = ( 194 | D15CD6D729A30C8E00086A1B /* LaunchScreen.storyboard in Resources */, 195 | D15CD6D429A30C8E00086A1B /* Assets.xcassets in Resources */, 196 | D15CD6D229A30C8D00086A1B /* Main.storyboard in Resources */, 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | }; 200 | /* End PBXResourcesBuildPhase section */ 201 | 202 | /* Begin PBXShellScriptBuildPhase section */ 203 | 4227C07D22FF257543F71878 /* [CP] Embed Pods Frameworks */ = { 204 | isa = PBXShellScriptBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | ); 208 | inputFileListPaths = ( 209 | "${PODS_ROOT}/Target Support Files/Pods-iOS Example/Pods-iOS Example-frameworks-${CONFIGURATION}-input-files.xcfilelist", 210 | ); 211 | name = "[CP] Embed Pods Frameworks"; 212 | outputFileListPaths = ( 213 | "${PODS_ROOT}/Target Support Files/Pods-iOS Example/Pods-iOS Example-frameworks-${CONFIGURATION}-output-files.xcfilelist", 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | shellPath = /bin/sh; 217 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOS Example/Pods-iOS Example-frameworks.sh\"\n"; 218 | showEnvVarsInLog = 0; 219 | }; 220 | F32376B9F90CFA440DD059D9 /* [CP] Check Pods Manifest.lock */ = { 221 | isa = PBXShellScriptBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | ); 225 | inputFileListPaths = ( 226 | ); 227 | inputPaths = ( 228 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 229 | "${PODS_ROOT}/Manifest.lock", 230 | ); 231 | name = "[CP] Check Pods Manifest.lock"; 232 | outputFileListPaths = ( 233 | ); 234 | outputPaths = ( 235 | "$(DERIVED_FILE_DIR)/Pods-iOS Example-checkManifestLockResult.txt", 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | shellPath = /bin/sh; 239 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 240 | showEnvVarsInLog = 0; 241 | }; 242 | /* End PBXShellScriptBuildPhase section */ 243 | 244 | /* Begin PBXSourcesBuildPhase section */ 245 | D15CD6C329A30C8D00086A1B /* Sources */ = { 246 | isa = PBXSourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | D15CD6CF29A30C8D00086A1B /* ViewController.swift in Sources */, 250 | 85F076DB29B243E2007F4504 /* CardOverlayView.swift in Sources */, 251 | D107965829B1D306003CC604 /* OverlayLabelView.swift in Sources */, 252 | D15CD6CB29A30C8D00086A1B /* AppDelegate.swift in Sources */, 253 | D14DC70429B584EF00DCC01C /* BottomView.swift in Sources */, 254 | D14DC70629B587E200DCC01C /* DetailViewController.swift in Sources */, 255 | D107965629B1D16E003CC604 /* CardView.swift in Sources */, 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | /* End PBXSourcesBuildPhase section */ 260 | 261 | /* Begin PBXVariantGroup section */ 262 | D15CD6D029A30C8D00086A1B /* Main.storyboard */ = { 263 | isa = PBXVariantGroup; 264 | children = ( 265 | D15CD6D129A30C8D00086A1B /* Base */, 266 | ); 267 | name = Main.storyboard; 268 | sourceTree = ""; 269 | }; 270 | D15CD6D529A30C8E00086A1B /* LaunchScreen.storyboard */ = { 271 | isa = PBXVariantGroup; 272 | children = ( 273 | D15CD6D629A30C8E00086A1B /* Base */, 274 | ); 275 | name = LaunchScreen.storyboard; 276 | sourceTree = ""; 277 | }; 278 | /* End PBXVariantGroup section */ 279 | 280 | /* Begin XCBuildConfiguration section */ 281 | D15CD6D929A30C8E00086A1B /* Debug */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ALWAYS_SEARCH_USER_PATHS = NO; 285 | CLANG_ANALYZER_NONNULL = YES; 286 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 288 | CLANG_ENABLE_MODULES = YES; 289 | CLANG_ENABLE_OBJC_ARC = YES; 290 | CLANG_ENABLE_OBJC_WEAK = YES; 291 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 292 | CLANG_WARN_BOOL_CONVERSION = YES; 293 | CLANG_WARN_COMMA = YES; 294 | CLANG_WARN_CONSTANT_CONVERSION = YES; 295 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 296 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 297 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 298 | CLANG_WARN_EMPTY_BODY = YES; 299 | CLANG_WARN_ENUM_CONVERSION = YES; 300 | CLANG_WARN_INFINITE_RECURSION = YES; 301 | CLANG_WARN_INT_CONVERSION = YES; 302 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 303 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 304 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 305 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 306 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 307 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 308 | CLANG_WARN_STRICT_PROTOTYPES = YES; 309 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 310 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 311 | CLANG_WARN_UNREACHABLE_CODE = YES; 312 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 313 | COPY_PHASE_STRIP = NO; 314 | DEBUG_INFORMATION_FORMAT = dwarf; 315 | ENABLE_STRICT_OBJC_MSGSEND = YES; 316 | ENABLE_TESTABILITY = YES; 317 | GCC_C_LANGUAGE_STANDARD = gnu11; 318 | GCC_DYNAMIC_NO_PIC = NO; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_OPTIMIZATION_LEVEL = 0; 321 | GCC_PREPROCESSOR_DEFINITIONS = ( 322 | "DEBUG=1", 323 | "$(inherited)", 324 | ); 325 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 326 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 327 | GCC_WARN_UNDECLARED_SELECTOR = YES; 328 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 329 | GCC_WARN_UNUSED_FUNCTION = YES; 330 | GCC_WARN_UNUSED_VARIABLE = YES; 331 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 332 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 333 | MTL_FAST_MATH = YES; 334 | ONLY_ACTIVE_ARCH = YES; 335 | SDKROOT = iphoneos; 336 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 337 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 338 | }; 339 | name = Debug; 340 | }; 341 | D15CD6DA29A30C8E00086A1B /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ALWAYS_SEARCH_USER_PATHS = NO; 345 | CLANG_ANALYZER_NONNULL = YES; 346 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 347 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 348 | CLANG_ENABLE_MODULES = YES; 349 | CLANG_ENABLE_OBJC_ARC = YES; 350 | CLANG_ENABLE_OBJC_WEAK = YES; 351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_COMMA = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 357 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INFINITE_RECURSION = YES; 361 | CLANG_WARN_INT_CONVERSION = YES; 362 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 364 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 366 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 367 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 368 | CLANG_WARN_STRICT_PROTOTYPES = YES; 369 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 370 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 371 | CLANG_WARN_UNREACHABLE_CODE = YES; 372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 373 | COPY_PHASE_STRIP = NO; 374 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 375 | ENABLE_NS_ASSERTIONS = NO; 376 | ENABLE_STRICT_OBJC_MSGSEND = YES; 377 | GCC_C_LANGUAGE_STANDARD = gnu11; 378 | GCC_NO_COMMON_BLOCKS = YES; 379 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 380 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 381 | GCC_WARN_UNDECLARED_SELECTOR = YES; 382 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 383 | GCC_WARN_UNUSED_FUNCTION = YES; 384 | GCC_WARN_UNUSED_VARIABLE = YES; 385 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 386 | MTL_ENABLE_DEBUG_INFO = NO; 387 | MTL_FAST_MATH = YES; 388 | SDKROOT = iphoneos; 389 | SWIFT_COMPILATION_MODE = wholemodule; 390 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 391 | VALIDATE_PRODUCT = YES; 392 | }; 393 | name = Release; 394 | }; 395 | D15CD6DC29A30C8E00086A1B /* Debug */ = { 396 | isa = XCBuildConfiguration; 397 | baseConfigurationReference = C68F1CC37F74AAB7C363B2CC /* Pods-iOS Example.debug.xcconfig */; 398 | buildSettings = { 399 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 400 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 401 | CODE_SIGN_STYLE = Automatic; 402 | CURRENT_PROJECT_VERSION = 1; 403 | DEVELOPMENT_TEAM = 2E8NVCJR97; 404 | GENERATE_INFOPLIST_FILE = YES; 405 | INFOPLIST_FILE = "iOS Example/Info.plist"; 406 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 407 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 408 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 409 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 410 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 411 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 412 | LD_RUNPATH_SEARCH_PATHS = ( 413 | "$(inherited)", 414 | "@executable_path/Frameworks", 415 | ); 416 | MARKETING_VERSION = 1.0; 417 | PRODUCT_BUNDLE_IDENTIFIER = "com.galaxy.iOS-Example"; 418 | PRODUCT_NAME = "$(TARGET_NAME)"; 419 | SWIFT_EMIT_LOC_STRINGS = YES; 420 | SWIFT_VERSION = 5.0; 421 | TARGETED_DEVICE_FAMILY = "1,2"; 422 | }; 423 | name = Debug; 424 | }; 425 | D15CD6DD29A30C8E00086A1B /* Release */ = { 426 | isa = XCBuildConfiguration; 427 | baseConfigurationReference = 8281919F37B8A1A62947D281 /* Pods-iOS Example.release.xcconfig */; 428 | buildSettings = { 429 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 430 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 431 | CODE_SIGN_STYLE = Automatic; 432 | CURRENT_PROJECT_VERSION = 1; 433 | DEVELOPMENT_TEAM = 2E8NVCJR97; 434 | GENERATE_INFOPLIST_FILE = YES; 435 | INFOPLIST_FILE = "iOS Example/Info.plist"; 436 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 437 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 438 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 439 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 440 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 441 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 442 | LD_RUNPATH_SEARCH_PATHS = ( 443 | "$(inherited)", 444 | "@executable_path/Frameworks", 445 | ); 446 | MARKETING_VERSION = 1.0; 447 | PRODUCT_BUNDLE_IDENTIFIER = "com.galaxy.iOS-Example"; 448 | PRODUCT_NAME = "$(TARGET_NAME)"; 449 | SWIFT_EMIT_LOC_STRINGS = YES; 450 | SWIFT_VERSION = 5.0; 451 | TARGETED_DEVICE_FAMILY = "1,2"; 452 | }; 453 | name = Release; 454 | }; 455 | /* End XCBuildConfiguration section */ 456 | 457 | /* Begin XCConfigurationList section */ 458 | D15CD6C229A30C8D00086A1B /* Build configuration list for PBXProject "iOS Example" */ = { 459 | isa = XCConfigurationList; 460 | buildConfigurations = ( 461 | D15CD6D929A30C8E00086A1B /* Debug */, 462 | D15CD6DA29A30C8E00086A1B /* Release */, 463 | ); 464 | defaultConfigurationIsVisible = 0; 465 | defaultConfigurationName = Release; 466 | }; 467 | D15CD6DB29A30C8E00086A1B /* Build configuration list for PBXNativeTarget "iOS Example" */ = { 468 | isa = XCConfigurationList; 469 | buildConfigurations = ( 470 | D15CD6DC29A30C8E00086A1B /* Debug */, 471 | D15CD6DD29A30C8E00086A1B /* Release */, 472 | ); 473 | defaultConfigurationIsVisible = 0; 474 | defaultConfigurationName = Release; 475 | }; 476 | /* End XCConfigurationList section */ 477 | }; 478 | rootObject = D15CD6BF29A30C8D00086A1B /* Project object */; 479 | } 480 | -------------------------------------------------------------------------------- /iOS Example/iOS Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS Example/iOS Example.xcodeproj/xcshareddata/xcschemes/iOS Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /iOS Example/iOS Example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /iOS Example/iOS Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS Example 4 | // 5 | // Created by dfsx6 on 2023/2/20. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window?.makeKeyAndVisible() 20 | 21 | let vc = ViewController() 22 | let navi = UINavigationController(rootViewController: vc) 23 | window?.rootViewController = navi 24 | 25 | return true 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/heart.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "heart.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/heart.imageset/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujunliuhong/DragCardContainer/42719330d2ad48a21baab1c795afe8f8e1d2daab/iOS Example/iOS Example/Assets.xcassets/heart.imageset/heart.png -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/lightning.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "lightning.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/lightning.imageset/lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujunliuhong/DragCardContainer/42719330d2ad48a21baab1c795afe8f8e1d2daab/iOS Example/iOS Example/Assets.xcassets/lightning.imageset/lightning.png -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/pass.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "pass.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/pass.imageset/pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujunliuhong/DragCardContainer/42719330d2ad48a21baab1c795afe8f8e1d2daab/iOS Example/iOS Example/Assets.xcassets/pass.imageset/pass.png -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/refresh.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "undo.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Assets.xcassets/refresh.imageset/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujunliuhong/DragCardContainer/42719330d2ad48a21baab1c795afe8f8e1d2daab/iOS Example/iOS Example/Assets.xcassets/refresh.imageset/undo.png -------------------------------------------------------------------------------- /iOS Example/iOS Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Demo/BottomView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottomView.swift 3 | // iOS Example 4 | // 5 | // Created by dfsx6 on 2023/3/6. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | public final class BottomView: UIView { 12 | 13 | public private(set) lazy var likeButton: UIButton = { 14 | let button = UIButton(type: .custom) 15 | button.setImage(UIImage(named: "heart"), for: .normal) 16 | return button 17 | }() 18 | 19 | public private(set) lazy var passButton: UIButton = { 20 | let button = UIButton(type: .custom) 21 | button.setImage(UIImage(named: "pass"), for: .normal) 22 | return button 23 | }() 24 | 25 | public private(set) lazy var superLikeButton: UIButton = { 26 | let button = UIButton(type: .custom) 27 | button.setImage(UIImage(named: "lightning"), for: .normal) 28 | return button 29 | }() 30 | 31 | public private(set) lazy var refreshButton: UIButton = { 32 | let button = UIButton(type: .custom) 33 | button.setImage(UIImage(named: "refresh"), for: .normal) 34 | return button 35 | }() 36 | 37 | public override init(frame: CGRect) { 38 | super.init(frame: frame) 39 | initUI() 40 | setupUI() 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | } 47 | 48 | extension BottomView { 49 | private func initUI() { 50 | 51 | } 52 | 53 | private func setupUI() { 54 | let buttons = [refreshButton, passButton, superLikeButton, likeButton] 55 | var leftButton: UIButton? 56 | for button in buttons { 57 | addSubview(button) 58 | if leftButton == nil { 59 | button.snp.makeConstraints { make in 60 | make.left.equalToSuperview() 61 | make.top.equalToSuperview() 62 | make.bottom.equalToSuperview() 63 | make.width.equalTo(50) 64 | } 65 | } else { 66 | button.snp.makeConstraints { make in 67 | make.left.equalTo(leftButton!.snp.right).offset(20) 68 | make.top.equalToSuperview() 69 | make.bottom.equalToSuperview() 70 | make.width.equalTo(50) 71 | } 72 | } 73 | leftButton = button 74 | } 75 | 76 | if leftButton != nil { 77 | leftButton!.snp.makeConstraints { make in 78 | make.right.equalToSuperview() 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Demo/CardOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardOverlayView.swift 3 | // iOS Example 4 | // 5 | // Created by galaxy on 2023/3/3. 6 | // 7 | 8 | import UIKit 9 | import DragCardContainer 10 | import SnapKit 11 | 12 | public final class CardOverlayView: UIView { 13 | 14 | public private(set) lazy var nopeView: OverlayLabelView = { 15 | let nopeView = OverlayLabelView(withTitle: "NOPE", 16 | color: UIColor(red: 252.0 / 255.0, green: 70.0 / 255.0, blue: 93.0 / 255.0, alpha: 1.0)) 17 | return nopeView 18 | }() 19 | 20 | public private(set) lazy var likeView: OverlayLabelView = { 21 | let likeView = OverlayLabelView(withTitle: "LIKE", 22 | color: UIColor(red: 49.0 / 255.0, green: 193.0 / 255.0, blue: 109.0 / 255.0, alpha: 1.0)) 23 | return likeView 24 | }() 25 | 26 | public private(set) lazy var superLikeView: OverlayLabelView = { 27 | let superLikeView = OverlayLabelView(withTitle: "SUPER\nLIKE", 28 | color: UIColor(red: 52 / 255, green: 154 / 255, blue: 254 / 255, alpha: 1)) 29 | return superLikeView 30 | }() 31 | 32 | public init(direction: Direction) { 33 | super.init(frame: .zero) 34 | 35 | switch direction { 36 | case .left: 37 | addSubview(nopeView) 38 | nopeView.snp.makeConstraints { make in 39 | make.right.equalToSuperview().offset(-20) 40 | make.top.equalToSuperview().offset(20) 41 | } 42 | case .up: 43 | addSubview(superLikeView) 44 | superLikeView.snp.makeConstraints { make in 45 | make.bottom.equalToSuperview().offset(-20) 46 | make.centerX.equalToSuperview() 47 | } 48 | case .right: 49 | addSubview(likeView) 50 | likeView.snp.makeConstraints { make in 51 | make.top.equalToSuperview().offset(20) 52 | make.left.equalToSuperview().offset(20) 53 | } 54 | default: 55 | break 56 | } 57 | } 58 | 59 | required init?(coder: NSCoder) { 60 | fatalError("init(coder:) has not been implemented") 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Demo/CardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardView.swift 3 | // iOS Example 4 | // 5 | // Created by dfsx6 on 2023/3/3. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import DragCardContainer 11 | 12 | public final class CardView: DragCardView { 13 | public private(set) lazy var label: UILabel = { 14 | let label = UILabel() 15 | label.textAlignment = .center 16 | label.font = .boldSystemFont(ofSize: 60) 17 | label.numberOfLines = 0 18 | return label 19 | }() 20 | 21 | public override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | initUI() 24 | setupUI() 25 | bindViewModel() 26 | other() 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | } 33 | 34 | extension CardView { 35 | private func initUI() { 36 | backgroundColor = .orange 37 | layer.borderWidth = 1.0 38 | layer.borderColor = UIColor.black.cgColor 39 | layer.cornerRadius = 8.0 40 | layer.masksToBounds = true 41 | } 42 | 43 | private func setupUI() { 44 | contentView.addSubview(label) 45 | 46 | label.snp.makeConstraints { make in 47 | make.edges.equalToSuperview() 48 | } 49 | } 50 | 51 | private func bindViewModel() { 52 | 53 | } 54 | 55 | private func other() { 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Demo/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.swift 3 | // iOS Example 4 | // 5 | // Created by dfsx6 on 2023/3/6. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | 12 | public final class DetailViewController: UIViewController { 13 | 14 | public private(set) lazy var label: UILabel = { 15 | let label = UILabel() 16 | label.textAlignment = .center 17 | label.font = .boldSystemFont(ofSize: 60) 18 | label.numberOfLines = 0 19 | return label 20 | }() 21 | 22 | public override func viewDidLoad() { 23 | super.viewDidLoad() 24 | view.backgroundColor = .white 25 | navigationItem.title = "Detail Page" 26 | 27 | view.addSubview(label) 28 | label.snp.makeConstraints { make in 29 | make.center.equalToSuperview() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Demo/OverlayLabelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayLabelView.swift 3 | // iOS Example 4 | // 5 | // Created by dfsx6 on 2023/3/3. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | public final class OverlayLabelView: UIView { 12 | 13 | private let titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.textAlignment = .center 16 | label.numberOfLines = 0 17 | return label 18 | }() 19 | 20 | public init(withTitle title: String, color: UIColor) { 21 | super.init(frame: CGRect.zero) 22 | 23 | layer.borderColor = color.cgColor 24 | layer.borderWidth = 4 25 | layer.cornerRadius = 4 26 | 27 | addSubview(titleLabel) 28 | titleLabel.textColor = color 29 | titleLabel.attributedText = NSAttributedString(string: title, 30 | attributes: NSAttributedString.Key.overlayAttributes) 31 | addSubview(titleLabel) 32 | titleLabel.snp.makeConstraints { make in 33 | make.left.equalToSuperview().offset(5) 34 | make.top.equalToSuperview().offset(5) 35 | make.right.equalToSuperview().offset(-5) 36 | make.bottom.equalToSuperview().offset(-5) 37 | } 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder) { 41 | return nil 42 | } 43 | } 44 | 45 | extension NSAttributedString.Key { 46 | fileprivate static var overlayAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue-Bold", size: 42)!,NSAttributedString.Key.kern: 5.0] 47 | } 48 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // iOS Example 4 | // 5 | // Created by dfsx6 on 2023/2/20. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import DragCardContainer 11 | import CoreGraphics 12 | 13 | private let titles: [String] = ["水星", "金星", "地球", "火星", "木星", "土星", "天王星", "海王星", "木卫一", "土卫一"] 14 | private let allowedDirection: [Direction] = [.left, .up, .right] 15 | 16 | public final class ViewController: UIViewController { 17 | 18 | private lazy var cardContainer: DragCardContainer = { 19 | let cardContainer = DragCardContainer() 20 | // 是否可以无限滑动 21 | cardContainer.infiniteLoop = false 22 | // 数据源 23 | cardContainer.dataSource = self 24 | // 代理 25 | cardContainer.delegate = self 26 | // 可见卡片数量 27 | cardContainer.visibleCount = 3 28 | // 是否可以打印日志 29 | cardContainer.enableLog = true 30 | // 是否禁用卡片拖动 31 | cardContainer.disableTopCardDrag = false 32 | // 是否禁用卡片点击 33 | cardContainer.disableTopCardClick = false 34 | 35 | let mode = ScaleMode() 36 | // 卡片之间间距 37 | mode.cardSpacing = 10 38 | // 方向(可以运行Demo,修改该参数看实际效果) 39 | mode.direction = .bottom 40 | // 最小缩放比例 41 | mode.minimumScale = 0.7 42 | // 卡片最大旋转角度 43 | mode.maximumAngle = 0 44 | // 赋值mode 45 | cardContainer.mode = mode 46 | 47 | return cardContainer 48 | }() 49 | 50 | private lazy var bottomView: BottomView = { 51 | let bottomView = BottomView() 52 | bottomView.likeButton.addTarget(self, action: #selector(likeAction), for: .touchUpInside) 53 | bottomView.passButton.addTarget(self, action: #selector(passAction), for: .touchUpInside) 54 | bottomView.superLikeButton.addTarget(self, action: #selector(superLikeAction), for: .touchUpInside) 55 | bottomView.refreshButton.addTarget(self, action: #selector(refreshAction), for: .touchUpInside) 56 | return bottomView 57 | }() 58 | 59 | public override func viewDidLoad() { 60 | super.viewDidLoad() 61 | view.backgroundColor = .white 62 | 63 | view.addSubview(cardContainer) 64 | view.addSubview(bottomView) 65 | 66 | cardContainer.snp.makeConstraints { make in 67 | make.centerX.equalToSuperview() 68 | make.width.equalTo(280) 69 | make.top.equalToSuperview().offset(150) 70 | make.bottom.equalToSuperview().offset(-150) 71 | } 72 | bottomView.snp.makeConstraints { make in 73 | make.centerX.equalToSuperview() 74 | make.top.equalTo(cardContainer.snp.bottom).offset(35) 75 | make.height.equalTo(50) 76 | } 77 | } 78 | } 79 | 80 | extension ViewController { 81 | @objc private func likeAction() { 82 | cardContainer.swipeTopCard(to: .right) 83 | } 84 | 85 | @objc private func passAction() { 86 | cardContainer.swipeTopCard(to: .left) 87 | } 88 | 89 | @objc private func superLikeAction() { 90 | cardContainer.swipeTopCard(to: .up) 91 | } 92 | 93 | @objc private func refreshAction() { 94 | cardContainer.reloadData(forceReset: true, animation: true) 95 | } 96 | } 97 | 98 | extension ViewController: DragCardDataSource { 99 | public func numberOfCards(_ dragCard: DragCardContainer) -> Int { 100 | return titles.count 101 | } 102 | 103 | public func dragCard(_ dragCard: DragCardContainer, viewForCard index: Int) -> DragCardView { 104 | let cardView = CardView() 105 | 106 | cardView.allowedDirection = allowedDirection 107 | 108 | for direction in allowedDirection { 109 | cardView.setOverlay(CardOverlayView(direction: direction), forDirection: direction) 110 | } 111 | 112 | cardView.label.text = "Index: \(index)\n\(titles[index])" 113 | 114 | return cardView 115 | } 116 | } 117 | 118 | extension ViewController: DragCardDelegate { 119 | public func dragCard(_ dragCard: DragCardContainer, displayTopCardAt index: Int, with cardView: DragCardView) { 120 | print("displayTopCardAt: \(index)") 121 | navigationItem.title = "Index: \(index)" 122 | } 123 | 124 | public func dragCard(_ dragCard: DragCardContainer, didRemovedTopCardAt index: Int, direction: Direction, with cardView: DragCardView) { 125 | print("didRemovedTopCardAt: \(index)") 126 | } 127 | 128 | public func dragCard(_ dragCard: DragCardContainer, didRemovedLast cardView: DragCardView) { 129 | print("didRemovedLast") 130 | } 131 | 132 | public func dragCard(_ dragCard: DragCardContainer, didSelectTopCardAt index: Int, with cardView: DragCardView) { 133 | print("didSelectTopCardAt: \(index)") 134 | 135 | let vc = DetailViewController() 136 | vc.label.text = titles[index] 137 | navigationController?.pushViewController(vc, animated: true) 138 | } 139 | } 140 | 141 | 142 | internal func RBA(R: CGFloat, G: CGFloat, B: CGFloat, A: CGFloat = 1.0) -> UIColor { 143 | return UIColor(red: (R / 255.0), green: (G / 255.0), blue: (B / 255.0), alpha: A) 144 | } 145 | 146 | /// 获取一个随机颜色 147 | internal func RandomColor() -> UIColor { 148 | let R: CGFloat = CGFloat(Int.random(in: Range(uncheckedBounds: (0, 255)))) 149 | let G: CGFloat = CGFloat(Int.random(in: Range(uncheckedBounds: (0, 255)))) 150 | let B: CGFloat = CGFloat(Int.random(in: Range(uncheckedBounds: (0, 255)))) 151 | let A: CGFloat = 1.0 152 | return RBA(R: R, G: G, B: B, A: A) 153 | } 154 | 155 | -------------------------------------------------------------------------------- /iOS Example/iOS Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | --------------------------------------------------------------------------------