├── .gitignore ├── .swift-version ├── DemoApp ├── DemoApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── DemoApp │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── DemoChainsViewController.swift │ ├── DemoLayerViewAnimationsViewController.swift │ ├── DemoMultipleAnimationsViewController.swift │ ├── DemoSpringAnimationsViewController.swift │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ └── Info.plist ├── EasyAnimation.podspec ├── EasyAnimation.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── EasyAnimation tvOS.xcscheme │ └── EasyAnimation.xcscheme ├── EasyAnimation ├── EAAnimationFuture.swift ├── EasyAnimation.h ├── EasyAnimation.swift ├── Info-tvOS.plist ├── Info.plist └── RBBSpringAnimation │ ├── LICENSE │ ├── RBBBlockBasedArray.swift │ ├── RBBLinearInterpolation.swift │ └── RBBSpringAnimation.swift ├── EasyAnimationDemo.playground ├── Pages │ ├── Chains.xcplaygroundpage │ │ ├── Contents.swift │ │ └── timeline.xctimeline │ ├── LayerViewAnimation.xcplaygroundpage │ │ ├── Contents.swift │ │ └── timeline.xctimeline │ ├── Multiple Animation.xcplaygroundpage │ │ ├── Contents.swift │ │ └── timeline.xctimeline │ └── Spring Animation.xcplaygroundpage │ │ ├── Contents.swift │ │ └── timeline.xctimeline ├── Sources │ ├── EAAnimationFuture.swift │ ├── EasyAnimation.swift │ ├── RBBBlockBasedArray.swift │ ├── RBBLinearInterpolation.swift │ └── RBBSpringAnimation.swift ├── contents.xcplayground └── playground.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── README.md └── etc ├── EA.png ├── chain.gif ├── corners.gif ├── moveX.gif └── spring.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | # Pods/ 27 | 28 | # Carthage 29 | # 30 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 31 | # Carthage/Checkouts 32 | 33 | Carthage/Build 34 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /DemoApp/DemoApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9C2C93C71ADBC44400C56262 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C2C93C61ADBC44400C56262 /* AppDelegate.swift */; }; 11 | 9C2C93CC1ADBC44400C56262 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9C2C93CA1ADBC44400C56262 /* Main.storyboard */; }; 12 | 9C2C93CE1ADBC44400C56262 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C2C93CD1ADBC44400C56262 /* Images.xcassets */; }; 13 | 9C2C93D11ADBC44400C56262 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9C2C93CF1ADBC44400C56262 /* LaunchScreen.xib */; }; 14 | 9C5B83BD1B148EA7008F2E79 /* EAAnimationFuture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5B83BC1B148EA7008F2E79 /* EAAnimationFuture.swift */; }; 15 | 9C9A05E11B13C3B80050DAEA /* RBBBlockBasedArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9A05DE1B13C3B80050DAEA /* RBBBlockBasedArray.swift */; }; 16 | 9C9A05E21B13C3B80050DAEA /* RBBLinearInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9A05DF1B13C3B80050DAEA /* RBBLinearInterpolation.swift */; }; 17 | 9C9A05E31B13C3B80050DAEA /* RBBSpringAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9A05E01B13C3B80050DAEA /* RBBSpringAnimation.swift */; }; 18 | 9CC6BED11B184C1200110DEB /* DemoLayerViewAnimationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CC6BED01B184C1200110DEB /* DemoLayerViewAnimationsViewController.swift */; }; 19 | 9CC6BED31B184FA300110DEB /* DemoSpringAnimationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CC6BED21B184FA300110DEB /* DemoSpringAnimationsViewController.swift */; }; 20 | 9CC6BED51B18527D00110DEB /* DemoChainsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CC6BED41B18527D00110DEB /* DemoChainsViewController.swift */; }; 21 | 9CC6BED71B18554E00110DEB /* DemoMultipleAnimationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CC6BED61B18554E00110DEB /* DemoMultipleAnimationsViewController.swift */; }; 22 | 9CFE78B61B0FE544006DBC7A /* EasyAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFE78B51B0FE544006DBC7A /* EasyAnimation.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 9C2C93C11ADBC44400C56262 /* DemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 9C2C93C51ADBC44400C56262 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | 9C2C93C61ADBC44400C56262 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 29 | 9C2C93CB1ADBC44400C56262 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 30 | 9C2C93CD1ADBC44400C56262 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 31 | 9C2C93D01ADBC44400C56262 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 32 | 9C5B83BC1B148EA7008F2E79 /* EAAnimationFuture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EAAnimationFuture.swift; sourceTree = ""; }; 33 | 9C9A05DE1B13C3B80050DAEA /* RBBBlockBasedArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RBBBlockBasedArray.swift; sourceTree = ""; }; 34 | 9C9A05DF1B13C3B80050DAEA /* RBBLinearInterpolation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RBBLinearInterpolation.swift; sourceTree = ""; }; 35 | 9C9A05E01B13C3B80050DAEA /* RBBSpringAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RBBSpringAnimation.swift; sourceTree = ""; }; 36 | 9CC6BED01B184C1200110DEB /* DemoLayerViewAnimationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoLayerViewAnimationsViewController.swift; sourceTree = ""; }; 37 | 9CC6BED21B184FA300110DEB /* DemoSpringAnimationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoSpringAnimationsViewController.swift; sourceTree = ""; }; 38 | 9CC6BED41B18527D00110DEB /* DemoChainsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoChainsViewController.swift; sourceTree = ""; }; 39 | 9CC6BED61B18554E00110DEB /* DemoMultipleAnimationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoMultipleAnimationsViewController.swift; sourceTree = ""; }; 40 | 9CFE78B51B0FE544006DBC7A /* EasyAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EasyAnimation.swift; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 9C2C93BE1ADBC44400C56262 /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 9C2C93B81ADBC44400C56262 = { 55 | isa = PBXGroup; 56 | children = ( 57 | 9CFE78B41B0FE544006DBC7A /* EasyAnimation */, 58 | 9C2C93C31ADBC44400C56262 /* DemoApp */, 59 | 9C2C93C21ADBC44400C56262 /* Products */, 60 | ); 61 | sourceTree = ""; 62 | }; 63 | 9C2C93C21ADBC44400C56262 /* Products */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 9C2C93C11ADBC44400C56262 /* DemoApp.app */, 67 | ); 68 | name = Products; 69 | sourceTree = ""; 70 | }; 71 | 9C2C93C31ADBC44400C56262 /* DemoApp */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 9CC6BED91B185E0C00110DEB /* assets */, 75 | 9CC6BED81B185DF700110DEB /* Demos */, 76 | 9C2C93C61ADBC44400C56262 /* AppDelegate.swift */, 77 | ); 78 | path = DemoApp; 79 | sourceTree = ""; 80 | }; 81 | 9C2C93C41ADBC44400C56262 /* Supporting Files */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 9C2C93C51ADBC44400C56262 /* Info.plist */, 85 | ); 86 | name = "Supporting Files"; 87 | sourceTree = ""; 88 | }; 89 | 9C9A05DD1B13C3B80050DAEA /* RBBSpringAnimation */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 9C9A05DE1B13C3B80050DAEA /* RBBBlockBasedArray.swift */, 93 | 9C9A05DF1B13C3B80050DAEA /* RBBLinearInterpolation.swift */, 94 | 9C9A05E01B13C3B80050DAEA /* RBBSpringAnimation.swift */, 95 | ); 96 | path = RBBSpringAnimation; 97 | sourceTree = ""; 98 | }; 99 | 9CC6BED81B185DF700110DEB /* Demos */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 9CC6BED01B184C1200110DEB /* DemoLayerViewAnimationsViewController.swift */, 103 | 9CC6BED21B184FA300110DEB /* DemoSpringAnimationsViewController.swift */, 104 | 9CC6BED41B18527D00110DEB /* DemoChainsViewController.swift */, 105 | 9CC6BED61B18554E00110DEB /* DemoMultipleAnimationsViewController.swift */, 106 | ); 107 | name = Demos; 108 | sourceTree = ""; 109 | }; 110 | 9CC6BED91B185E0C00110DEB /* assets */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 9C2C93CA1ADBC44400C56262 /* Main.storyboard */, 114 | 9C2C93CD1ADBC44400C56262 /* Images.xcassets */, 115 | 9C2C93CF1ADBC44400C56262 /* LaunchScreen.xib */, 116 | 9C2C93C41ADBC44400C56262 /* Supporting Files */, 117 | ); 118 | name = assets; 119 | sourceTree = ""; 120 | }; 121 | 9CFE78B41B0FE544006DBC7A /* EasyAnimation */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 9CFE78B51B0FE544006DBC7A /* EasyAnimation.swift */, 125 | 9C5B83BC1B148EA7008F2E79 /* EAAnimationFuture.swift */, 126 | 9C9A05DD1B13C3B80050DAEA /* RBBSpringAnimation */, 127 | ); 128 | name = EasyAnimation; 129 | path = ../EasyAnimation; 130 | sourceTree = ""; 131 | }; 132 | /* End PBXGroup section */ 133 | 134 | /* Begin PBXNativeTarget section */ 135 | 9C2C93C01ADBC44400C56262 /* DemoApp */ = { 136 | isa = PBXNativeTarget; 137 | buildConfigurationList = 9C2C93E01ADBC44400C56262 /* Build configuration list for PBXNativeTarget "DemoApp" */; 138 | buildPhases = ( 139 | 9C2C93BD1ADBC44400C56262 /* Sources */, 140 | 9C2C93BE1ADBC44400C56262 /* Frameworks */, 141 | 9C2C93BF1ADBC44400C56262 /* Resources */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = DemoApp; 148 | productName = DemoApp; 149 | productReference = 9C2C93C11ADBC44400C56262 /* DemoApp.app */; 150 | productType = "com.apple.product-type.application"; 151 | }; 152 | /* End PBXNativeTarget section */ 153 | 154 | /* Begin PBXProject section */ 155 | 9C2C93B91ADBC44400C56262 /* Project object */ = { 156 | isa = PBXProject; 157 | attributes = { 158 | LastSwiftUpdateCheck = 0700; 159 | LastUpgradeCheck = 1000; 160 | ORGANIZATIONNAME = "Underplot ltd."; 161 | TargetAttributes = { 162 | 9C2C93C01ADBC44400C56262 = { 163 | CreatedOnToolsVersion = 6.3; 164 | LastSwiftMigration = 0800; 165 | }; 166 | }; 167 | }; 168 | buildConfigurationList = 9C2C93BC1ADBC44400C56262 /* Build configuration list for PBXProject "DemoApp" */; 169 | compatibilityVersion = "Xcode 3.2"; 170 | developmentRegion = English; 171 | hasScannedForEncodings = 0; 172 | knownRegions = ( 173 | en, 174 | Base, 175 | ); 176 | mainGroup = 9C2C93B81ADBC44400C56262; 177 | productRefGroup = 9C2C93C21ADBC44400C56262 /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 9C2C93C01ADBC44400C56262 /* DemoApp */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 9C2C93BF1ADBC44400C56262 /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 9C2C93CC1ADBC44400C56262 /* Main.storyboard in Resources */, 192 | 9C2C93D11ADBC44400C56262 /* LaunchScreen.xib in Resources */, 193 | 9C2C93CE1ADBC44400C56262 /* Images.xcassets in Resources */, 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | }; 197 | /* End PBXResourcesBuildPhase section */ 198 | 199 | /* Begin PBXSourcesBuildPhase section */ 200 | 9C2C93BD1ADBC44400C56262 /* Sources */ = { 201 | isa = PBXSourcesBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | 9CC6BED11B184C1200110DEB /* DemoLayerViewAnimationsViewController.swift in Sources */, 205 | 9C2C93C71ADBC44400C56262 /* AppDelegate.swift in Sources */, 206 | 9CC6BED71B18554E00110DEB /* DemoMultipleAnimationsViewController.swift in Sources */, 207 | 9C5B83BD1B148EA7008F2E79 /* EAAnimationFuture.swift in Sources */, 208 | 9CC6BED51B18527D00110DEB /* DemoChainsViewController.swift in Sources */, 209 | 9C9A05E31B13C3B80050DAEA /* RBBSpringAnimation.swift in Sources */, 210 | 9C9A05E11B13C3B80050DAEA /* RBBBlockBasedArray.swift in Sources */, 211 | 9CFE78B61B0FE544006DBC7A /* EasyAnimation.swift in Sources */, 212 | 9C9A05E21B13C3B80050DAEA /* RBBLinearInterpolation.swift in Sources */, 213 | 9CC6BED31B184FA300110DEB /* DemoSpringAnimationsViewController.swift in Sources */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXSourcesBuildPhase section */ 218 | 219 | /* Begin PBXVariantGroup section */ 220 | 9C2C93CA1ADBC44400C56262 /* Main.storyboard */ = { 221 | isa = PBXVariantGroup; 222 | children = ( 223 | 9C2C93CB1ADBC44400C56262 /* Base */, 224 | ); 225 | name = Main.storyboard; 226 | sourceTree = ""; 227 | }; 228 | 9C2C93CF1ADBC44400C56262 /* LaunchScreen.xib */ = { 229 | isa = PBXVariantGroup; 230 | children = ( 231 | 9C2C93D01ADBC44400C56262 /* Base */, 232 | ); 233 | name = LaunchScreen.xib; 234 | sourceTree = ""; 235 | }; 236 | /* End PBXVariantGroup section */ 237 | 238 | /* Begin XCBuildConfiguration section */ 239 | 9C2C93DE1ADBC44400C56262 /* Debug */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | ALWAYS_SEARCH_USER_PATHS = NO; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 248 | CLANG_WARN_BOOL_CONVERSION = YES; 249 | CLANG_WARN_COMMA = YES; 250 | CLANG_WARN_CONSTANT_CONVERSION = YES; 251 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 252 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 253 | CLANG_WARN_EMPTY_BODY = YES; 254 | CLANG_WARN_ENUM_CONVERSION = YES; 255 | CLANG_WARN_INFINITE_RECURSION = YES; 256 | CLANG_WARN_INT_CONVERSION = YES; 257 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 258 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 259 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 262 | CLANG_WARN_STRICT_PROTOTYPES = YES; 263 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 264 | CLANG_WARN_UNREACHABLE_CODE = YES; 265 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 266 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 267 | COPY_PHASE_STRIP = NO; 268 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 269 | ENABLE_STRICT_OBJC_MSGSEND = YES; 270 | ENABLE_TESTABILITY = YES; 271 | GCC_C_LANGUAGE_STANDARD = gnu99; 272 | GCC_DYNAMIC_NO_PIC = NO; 273 | GCC_NO_COMMON_BLOCKS = YES; 274 | GCC_OPTIMIZATION_LEVEL = 0; 275 | GCC_PREPROCESSOR_DEFINITIONS = ( 276 | "DEBUG=1", 277 | "$(inherited)", 278 | ); 279 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 280 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 281 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 282 | GCC_WARN_UNDECLARED_SELECTOR = YES; 283 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 284 | GCC_WARN_UNUSED_FUNCTION = YES; 285 | GCC_WARN_UNUSED_VARIABLE = YES; 286 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 287 | MTL_ENABLE_DEBUG_INFO = YES; 288 | ONLY_ACTIVE_ARCH = YES; 289 | SDKROOT = iphoneos; 290 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 291 | }; 292 | name = Debug; 293 | }; 294 | 9C2C93DF1ADBC44400C56262 /* Release */ = { 295 | isa = XCBuildConfiguration; 296 | buildSettings = { 297 | ALWAYS_SEARCH_USER_PATHS = NO; 298 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 299 | CLANG_CXX_LIBRARY = "libc++"; 300 | CLANG_ENABLE_MODULES = YES; 301 | CLANG_ENABLE_OBJC_ARC = YES; 302 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 303 | CLANG_WARN_BOOL_CONVERSION = YES; 304 | CLANG_WARN_COMMA = YES; 305 | CLANG_WARN_CONSTANT_CONVERSION = YES; 306 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 307 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 308 | CLANG_WARN_EMPTY_BODY = YES; 309 | CLANG_WARN_ENUM_CONVERSION = YES; 310 | CLANG_WARN_INFINITE_RECURSION = YES; 311 | CLANG_WARN_INT_CONVERSION = YES; 312 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 313 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 314 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 315 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 316 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 317 | CLANG_WARN_STRICT_PROTOTYPES = YES; 318 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 319 | CLANG_WARN_UNREACHABLE_CODE = YES; 320 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 321 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 322 | COPY_PHASE_STRIP = NO; 323 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 324 | ENABLE_NS_ASSERTIONS = NO; 325 | ENABLE_STRICT_OBJC_MSGSEND = YES; 326 | GCC_C_LANGUAGE_STANDARD = gnu99; 327 | GCC_NO_COMMON_BLOCKS = YES; 328 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 329 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 330 | GCC_WARN_UNDECLARED_SELECTOR = YES; 331 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 332 | GCC_WARN_UNUSED_FUNCTION = YES; 333 | GCC_WARN_UNUSED_VARIABLE = YES; 334 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 335 | MTL_ENABLE_DEBUG_INFO = NO; 336 | SDKROOT = iphoneos; 337 | VALIDATE_PRODUCT = YES; 338 | }; 339 | name = Release; 340 | }; 341 | 9C2C93E11ADBC44400C56262 /* Debug */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 345 | INFOPLIST_FILE = DemoApp/Info.plist; 346 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 347 | PRODUCT_BUNDLE_IDENTIFIER = "com.underplot.$(PRODUCT_NAME:rfc1034identifier)"; 348 | PRODUCT_NAME = "$(TARGET_NAME)"; 349 | SWIFT_VERSION = 4.2; 350 | }; 351 | name = Debug; 352 | }; 353 | 9C2C93E21ADBC44400C56262 /* Release */ = { 354 | isa = XCBuildConfiguration; 355 | buildSettings = { 356 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 357 | INFOPLIST_FILE = DemoApp/Info.plist; 358 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 359 | PRODUCT_BUNDLE_IDENTIFIER = "com.underplot.$(PRODUCT_NAME:rfc1034identifier)"; 360 | PRODUCT_NAME = "$(TARGET_NAME)"; 361 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 362 | SWIFT_VERSION = 4.2; 363 | }; 364 | name = Release; 365 | }; 366 | /* End XCBuildConfiguration section */ 367 | 368 | /* Begin XCConfigurationList section */ 369 | 9C2C93BC1ADBC44400C56262 /* Build configuration list for PBXProject "DemoApp" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | 9C2C93DE1ADBC44400C56262 /* Debug */, 373 | 9C2C93DF1ADBC44400C56262 /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | 9C2C93E01ADBC44400C56262 /* Build configuration list for PBXNativeTarget "DemoApp" */ = { 379 | isa = XCConfigurationList; 380 | buildConfigurations = ( 381 | 9C2C93E11ADBC44400C56262 /* Debug */, 382 | 9C2C93E21ADBC44400C56262 /* Release */, 383 | ); 384 | defaultConfigurationIsVisible = 0; 385 | defaultConfigurationName = Release; 386 | }; 387 | /* End XCConfigurationList section */ 388 | }; 389 | rootObject = 9C2C93B91ADBC44400C56262 /* Project object */; 390 | } 391 | -------------------------------------------------------------------------------- /DemoApp/DemoApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DemoApp/DemoApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DemoApp/DemoApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DemoApp 4 | // 5 | // Created by Marin Todorov on 4/13/15. 6 | // Copyright (c) 2015 Underplot ltd. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | func delay(seconds: Double, completion: @escaping () -> Void) { 12 | DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: completion) 13 | } 14 | 15 | @UIApplicationMain 16 | class AppDelegate: UIResponder, UIApplicationDelegate { 17 | 18 | var window: UIWindow? 19 | 20 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 21 | EasyAnimation.enable() 22 | return true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DemoApp/DemoApp/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /DemoApp/DemoApp/DemoChainsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoChainsViewController.swift 3 | // DemoApp 4 | // 5 | // Created by Marin Todorov on 5/29/15. 6 | // Copyright (c) 2015 Underplot ltd. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DemoChainsViewController: UIViewController { 12 | 13 | @IBOutlet weak var redSquare: UIView! 14 | @IBOutlet weak var blueSquare: UIView! 15 | 16 | @IBOutlet weak var redTopConstraint: NSLayoutConstraint! 17 | @IBOutlet weak var redLeftConstraint: NSLayoutConstraint! 18 | 19 | @IBOutlet weak var blueTopConstraint: NSLayoutConstraint! 20 | 21 | weak var chain: EAAnimationFuture? 22 | 23 | override func viewDidAppear(_ animated: Bool) { 24 | super.viewDidAppear(animated) 25 | 26 | // chain calls to animateWithDuration... to easily make animation sequences 27 | 28 | chain = UIView.animateAndChain(withDuration: 1.0, delay: 0.0, options: [], animations: { 29 | 30 | self.redTopConstraint.constant += 150.0 31 | self.view.layoutIfNeeded() 32 | 33 | }, completion: nil).animate(withDuration: 1.0, animations: { 34 | 35 | self.redLeftConstraint.constant += 150.0 36 | self.view.layoutIfNeeded() 37 | 38 | }).animate(withDuration: 1.0, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options: [], animations: { 39 | 40 | self.redSquare.transform = CGAffineTransform(rotationAngle: CGFloat(-Double.pi/4)) 41 | self.blueSquare.layer.transform = CATransform3DMakeRotation(CGFloat(Double.pi/4), 0.0, 0.0, 1.0) 42 | 43 | }, completion: nil).animate(withDuration: 0.5, animations: { 44 | 45 | self.redTopConstraint.constant -= 150.0 46 | self.blueTopConstraint.constant -= 150.0 47 | self.view.layoutIfNeeded() 48 | 49 | }).animate(withDuration: 2.0, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options: .repeat, animations: { 50 | 51 | self.redLeftConstraint.constant -= 150.0 52 | self.blueTopConstraint.constant += 150.0 53 | self.view.layoutIfNeeded() 54 | self.redSquare.transform = CGAffineTransform.identity 55 | self.blueSquare.layer.transform = CATransform3DIdentity 56 | 57 | }, completion: {_ in 58 | print("sequence finished - will loop from start") 59 | }) 60 | 61 | } 62 | 63 | @IBAction func actionCancelSequence(_ sender: AnyObject) { 64 | 65 | if let sender = sender as? UIButton { 66 | sender.setTitle("Cancelled", for: UIControl.State()) 67 | sender.isEnabled = false 68 | } 69 | 70 | chain!.cancelAnimationChain({ 71 | 72 | self.redLeftConstraint.constant = 0 73 | self.redTopConstraint.constant = 0 74 | self.redSquare.transform = CGAffineTransform.identity 75 | self.blueTopConstraint.constant = 0 76 | self.blueSquare.layer.transform = CATransform3DIdentity 77 | 78 | UIView.animate(withDuration: 0.5, animations: { 79 | self.view.layoutIfNeeded() 80 | }) 81 | }) 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /DemoApp/DemoApp/DemoLayerViewAnimationsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoLayerViewAnimationsViewController.swift 3 | // DemoApp 4 | // 5 | // Created by Marin Todorov on 5/29/15. 6 | // Copyright (c) 2015 Underplot ltd. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DemoLayerViewAnimationsViewController: UIViewController { 12 | 13 | @IBOutlet weak var redSquare: UIView! 14 | @IBOutlet weak var blueSquare: UIView! 15 | 16 | override func viewDidAppear(_ animated: Bool) { 17 | super.viewDidAppear(animated) 18 | 19 | //animate the view and the layer 20 | UIView.animate(withDuration: 0.33, delay: 0.0, options: [.curveEaseOut, .repeat, .autoreverse], 21 | animations: { () -> Void in 22 | 23 | //view property animation 24 | self.redSquare.transform = CGAffineTransform(scaleX: 1.33, y: 1.5) 25 | .concatenating(CGAffineTransform(translationX: 0.0, y: 50.0)) 26 | 27 | //layer properties animations 28 | self.blueSquare.layer.cornerRadius = 30.0 29 | self.blueSquare.layer.borderWidth = 10.0 30 | self.blueSquare.layer.borderColor = UIColor.blue.cgColor 31 | self.blueSquare.layer.shadowColor = UIColor.gray.cgColor 32 | self.blueSquare.layer.shadowOffset = CGSize(width: 15.0, height: 15.0) 33 | self.blueSquare.layer.shadowOpacity = 0.5 34 | 35 | var trans3d = CATransform3DIdentity 36 | trans3d.m34 = -1.0/500.0 37 | 38 | let rotationTransform = CATransform3DRotate(trans3d, CGFloat(-Double.pi/4), 0.0, 1.0, 0.0) 39 | let translationTransform = CATransform3DMakeTranslation(-50.0, 0, 0) 40 | self.blueSquare.layer.transform = CATransform3DConcat(rotationTransform, translationTransform) 41 | 42 | }, completion: nil) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DemoApp/DemoApp/DemoMultipleAnimationsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoMultipleAnimationsViewController.swift 3 | // DemoApp 4 | // 5 | // Created by Marin Todorov on 5/29/15. 6 | // Copyright (c) 2015 Underplot ltd. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DemoMultipleAnimationsViewController: UIViewController { 12 | 13 | var viewCount: Int = 0 14 | let maxViews: Int = 190 15 | 16 | override func viewDidAppear(_ animated: Bool) { 17 | super.viewDidAppear(animated) 18 | 19 | spawn() 20 | } 21 | 22 | func spawn() { 23 | viewCount += 1 24 | if viewCount > maxViews { 25 | return 26 | } 27 | 28 | let v = UIView(frame: CGRect(x: 50, y: 100, width: 100, height: 100)) 29 | v.backgroundColor = UIColor(hue: CGFloat(Double(self.view.subviews.count)/Double(maxViews)), saturation: 1.0, brightness: 1.0, alpha: 1.0) 30 | v.layer.cornerRadius = 50.0 31 | view.addSubview(v) 32 | 33 | let duration = 5.0 34 | 35 | UIView.animateAndChain(withDuration: duration, delay: 0.0, options: [], animations: { 36 | v.center.y += 250.0 37 | }, completion: nil).animate(withDuration: duration, animations: { 38 | v.center.x += 200.0 39 | }).animate(withDuration: duration, animations: { 40 | v.center.y -= 250.0 41 | }).animate(withDuration: duration, delay: 0.0, options: .repeat, animations: { 42 | v.center.x -= 200.0 43 | }, completion: nil) 44 | 45 | self.title = "\(viewCount) views" 46 | 47 | delay(seconds: 0.10, completion: { 48 | self.spawn() 49 | }) 50 | } 51 | 52 | override func viewWillDisappear(_ animated: Bool) { 53 | super.viewWillDisappear(animated) 54 | 55 | viewCount = 1000 56 | 57 | for chain in EAAnimationFuture.animations { 58 | chain.cancelAnimationChain() 59 | } 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /DemoApp/DemoApp/DemoSpringAnimationsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoSpringAnimationsViewController.swift 3 | // DemoApp 4 | // 5 | // Created by Marin Todorov on 5/29/15. 6 | // Copyright (c) 2015 Underplot ltd. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DemoSpringAnimationsViewController: UIViewController { 12 | 13 | 14 | @IBOutlet weak var redSquare: UIView! 15 | @IBOutlet weak var blueSquare: UIView! 16 | 17 | override func viewDidAppear(_ animated: Bool) { 18 | super.viewDidAppear(animated) 19 | 20 | animate() 21 | } 22 | 23 | func animate() { 24 | 25 | UIView.animateAndChain(withDuration: 1.0, delay: 0.0, usingSpringWithDamping: 0.33, initialSpringVelocity: 0.0, options: [], 26 | animations: { 27 | //spring animate the view 28 | self.redSquare.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi/2)) 29 | .concatenating(CGAffineTransform(scaleX: 1.5, y: 1.5)) 30 | 31 | //spring animate the layer 32 | self.blueSquare.layer.transform = CATransform3DConcat( 33 | CATransform3DMakeRotation(CGFloat(-Double.pi/4), 0.0, 0.0, 1.0), 34 | CATransform3DMakeScale(1.33, 1.33, 1.33) 35 | ) 36 | self.blueSquare.layer.cornerRadius = 50.0 37 | 38 | }, completion: nil).animate(withDuration: 1.0, delay: 0.0, usingSpringWithDamping: 0.33, initialSpringVelocity: 0.0, options: .repeat, 39 | animations: { 40 | 41 | //spring animate the view 42 | self.redSquare.transform = CGAffineTransform.identity 43 | 44 | //spring animate the layer 45 | self.blueSquare.layer.transform = CATransform3DIdentity 46 | self.blueSquare.layer.cornerRadius = 0.0 47 | 48 | }, completion: nil) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /DemoApp/DemoApp/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /DemoApp/DemoApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /EasyAnimation.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "EasyAnimation" 3 | s.version = "2.2" 4 | s.summary = "A Swift library to take the power of UIView.animateWithDuration(_:, animations:...) to a whole new level!" 5 | s.description = <<-DESC 6 | EasyAnimation extends the animation methods that are built-in in UIKit and allows you to: 7 | 8 | * animate layer properties from within animateWithDuration:animations: 9 | * mix view and layer animations together 10 | * spring animations for view and layers 11 | * chain easily animation together 12 | * cancel animation chains 13 | 14 | DESC 15 | s.homepage = "https://github.com/icanzilb/EasyAnimation" 16 | s.screenshots = "https://raw.githubusercontent.com/icanzilb/EasyAnimation/master/etc/EA.png" 17 | s.license = 'MIT' 18 | s.author = { "Marin Todorov" => "touch-code-magazine@underplot.com" } 19 | s.source = { :git => "https://github.com/icanzilb/EasyAnimation.git", :tag => s.version.to_s } 20 | s.social_media_url = 'https://twitter.com/icanzilb' 21 | 22 | s.platform = :ios, '9.0' 23 | s.requires_arc = true 24 | 25 | s.source_files = 'EasyAnimation/**/*.swift' 26 | end 27 | -------------------------------------------------------------------------------- /EasyAnimation.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4034825A1B8E01ED006C3C7C /* EasyAnimation.h in Headers */ = {isa = PBXBuildFile; fileRef = 403482591B8E01ED006C3C7C /* EasyAnimation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 403482771B8E024F006C3C7C /* EAAnimationFuture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482701B8E024F006C3C7C /* EAAnimationFuture.swift */; }; 12 | 403482781B8E024F006C3C7C /* EasyAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482711B8E024F006C3C7C /* EasyAnimation.swift */; }; 13 | 403482791B8E024F006C3C7C /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 403482731B8E024F006C3C7C /* LICENSE */; }; 14 | 4034827A1B8E024F006C3C7C /* RBBBlockBasedArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482741B8E024F006C3C7C /* RBBBlockBasedArray.swift */; }; 15 | 4034827B1B8E024F006C3C7C /* RBBLinearInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482751B8E024F006C3C7C /* RBBLinearInterpolation.swift */; }; 16 | 4034827C1B8E024F006C3C7C /* RBBSpringAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482761B8E024F006C3C7C /* RBBSpringAnimation.swift */; }; 17 | 72A03EEC1C29784A002DA467 /* RBBLinearInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482751B8E024F006C3C7C /* RBBLinearInterpolation.swift */; }; 18 | 72A03EED1C29784A002DA467 /* RBBSpringAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482761B8E024F006C3C7C /* RBBSpringAnimation.swift */; }; 19 | 72A03EEE1C29784A002DA467 /* EasyAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482711B8E024F006C3C7C /* EasyAnimation.swift */; }; 20 | 72A03EEF1C29784A002DA467 /* EAAnimationFuture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482701B8E024F006C3C7C /* EAAnimationFuture.swift */; }; 21 | 72A03EF01C29784A002DA467 /* RBBBlockBasedArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403482741B8E024F006C3C7C /* RBBBlockBasedArray.swift */; }; 22 | 72A03EF31C29784A002DA467 /* EasyAnimation.h in Headers */ = {isa = PBXBuildFile; fileRef = 403482591B8E01ED006C3C7C /* EasyAnimation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 23 | 72A03EF51C29784A002DA467 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 403482731B8E024F006C3C7C /* LICENSE */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 403482541B8E01ED006C3C7C /* EasyAnimation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EasyAnimation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 403482581B8E01ED006C3C7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 403482591B8E01ED006C3C7C /* EasyAnimation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EasyAnimation.h; sourceTree = ""; }; 30 | 403482701B8E024F006C3C7C /* EAAnimationFuture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EAAnimationFuture.swift; sourceTree = ""; }; 31 | 403482711B8E024F006C3C7C /* EasyAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EasyAnimation.swift; sourceTree = ""; }; 32 | 403482731B8E024F006C3C7C /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 33 | 403482741B8E024F006C3C7C /* RBBBlockBasedArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RBBBlockBasedArray.swift; sourceTree = ""; }; 34 | 403482751B8E024F006C3C7C /* RBBLinearInterpolation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RBBLinearInterpolation.swift; sourceTree = ""; }; 35 | 403482761B8E024F006C3C7C /* RBBSpringAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RBBSpringAnimation.swift; sourceTree = ""; }; 36 | 72A03EF91C29784A002DA467 /* EasyAnimation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EasyAnimation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 9C23194E1C6E161700CD61E6 /* Info-tvOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-tvOS.plist"; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 403482501B8E01ED006C3C7C /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | 72A03EF11C29784A002DA467 /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 4034824A1B8E01ED006C3C7C = { 59 | isa = PBXGroup; 60 | children = ( 61 | 403482561B8E01ED006C3C7C /* EasyAnimation */, 62 | 403482551B8E01ED006C3C7C /* Products */, 63 | ); 64 | sourceTree = ""; 65 | }; 66 | 403482551B8E01ED006C3C7C /* Products */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 403482541B8E01ED006C3C7C /* EasyAnimation.framework */, 70 | 72A03EF91C29784A002DA467 /* EasyAnimation.framework */, 71 | ); 72 | name = Products; 73 | sourceTree = ""; 74 | }; 75 | 403482561B8E01ED006C3C7C /* EasyAnimation */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 403482701B8E024F006C3C7C /* EAAnimationFuture.swift */, 79 | 403482711B8E024F006C3C7C /* EasyAnimation.swift */, 80 | 403482721B8E024F006C3C7C /* RBBSpringAnimation */, 81 | 403482591B8E01ED006C3C7C /* EasyAnimation.h */, 82 | 403482571B8E01ED006C3C7C /* Supporting Files */, 83 | ); 84 | path = EasyAnimation; 85 | sourceTree = ""; 86 | }; 87 | 403482571B8E01ED006C3C7C /* Supporting Files */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 9C23194E1C6E161700CD61E6 /* Info-tvOS.plist */, 91 | 403482581B8E01ED006C3C7C /* Info.plist */, 92 | ); 93 | name = "Supporting Files"; 94 | sourceTree = ""; 95 | }; 96 | 403482721B8E024F006C3C7C /* RBBSpringAnimation */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 403482731B8E024F006C3C7C /* LICENSE */, 100 | 403482741B8E024F006C3C7C /* RBBBlockBasedArray.swift */, 101 | 403482751B8E024F006C3C7C /* RBBLinearInterpolation.swift */, 102 | 403482761B8E024F006C3C7C /* RBBSpringAnimation.swift */, 103 | ); 104 | path = RBBSpringAnimation; 105 | sourceTree = ""; 106 | }; 107 | /* End PBXGroup section */ 108 | 109 | /* Begin PBXHeadersBuildPhase section */ 110 | 403482511B8E01ED006C3C7C /* Headers */ = { 111 | isa = PBXHeadersBuildPhase; 112 | buildActionMask = 2147483647; 113 | files = ( 114 | 4034825A1B8E01ED006C3C7C /* EasyAnimation.h in Headers */, 115 | ); 116 | runOnlyForDeploymentPostprocessing = 0; 117 | }; 118 | 72A03EF21C29784A002DA467 /* Headers */ = { 119 | isa = PBXHeadersBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | 72A03EF31C29784A002DA467 /* EasyAnimation.h in Headers */, 123 | ); 124 | runOnlyForDeploymentPostprocessing = 0; 125 | }; 126 | /* End PBXHeadersBuildPhase section */ 127 | 128 | /* Begin PBXNativeTarget section */ 129 | 403482531B8E01ED006C3C7C /* EasyAnimation */ = { 130 | isa = PBXNativeTarget; 131 | buildConfigurationList = 4034826A1B8E01ED006C3C7C /* Build configuration list for PBXNativeTarget "EasyAnimation" */; 132 | buildPhases = ( 133 | 4034824F1B8E01ED006C3C7C /* Sources */, 134 | 403482501B8E01ED006C3C7C /* Frameworks */, 135 | 403482511B8E01ED006C3C7C /* Headers */, 136 | 403482521B8E01ED006C3C7C /* Resources */, 137 | ); 138 | buildRules = ( 139 | ); 140 | dependencies = ( 141 | ); 142 | name = EasyAnimation; 143 | productName = EasyAnimation; 144 | productReference = 403482541B8E01ED006C3C7C /* EasyAnimation.framework */; 145 | productType = "com.apple.product-type.framework"; 146 | }; 147 | 72A03EEA1C29784A002DA467 /* EasyAnimation tvOS */ = { 148 | isa = PBXNativeTarget; 149 | buildConfigurationList = 72A03EF61C29784A002DA467 /* Build configuration list for PBXNativeTarget "EasyAnimation tvOS" */; 150 | buildPhases = ( 151 | 72A03EEB1C29784A002DA467 /* Sources */, 152 | 72A03EF11C29784A002DA467 /* Frameworks */, 153 | 72A03EF21C29784A002DA467 /* Headers */, 154 | 72A03EF41C29784A002DA467 /* Resources */, 155 | ); 156 | buildRules = ( 157 | ); 158 | dependencies = ( 159 | ); 160 | name = "EasyAnimation tvOS"; 161 | productName = EasyAnimation; 162 | productReference = 72A03EF91C29784A002DA467 /* EasyAnimation.framework */; 163 | productType = "com.apple.product-type.framework"; 164 | }; 165 | /* End PBXNativeTarget section */ 166 | 167 | /* Begin PBXProject section */ 168 | 4034824B1B8E01ED006C3C7C /* Project object */ = { 169 | isa = PBXProject; 170 | attributes = { 171 | LastSwiftUpdateCheck = 0700; 172 | LastUpgradeCheck = 0800; 173 | ORGANIZATIONNAME = EasyAnimation; 174 | TargetAttributes = { 175 | 403482531B8E01ED006C3C7C = { 176 | CreatedOnToolsVersion = 6.4; 177 | DevelopmentTeamName = "Bakken & Bæck AS"; 178 | LastSwiftMigration = 0800; 179 | }; 180 | 72A03EEA1C29784A002DA467 = { 181 | DevelopmentTeamName = "Bakken & Bæck AS"; 182 | }; 183 | }; 184 | }; 185 | buildConfigurationList = 4034824E1B8E01ED006C3C7C /* Build configuration list for PBXProject "EasyAnimation" */; 186 | compatibilityVersion = "Xcode 3.2"; 187 | developmentRegion = English; 188 | hasScannedForEncodings = 0; 189 | knownRegions = ( 190 | en, 191 | ); 192 | mainGroup = 4034824A1B8E01ED006C3C7C; 193 | productRefGroup = 403482551B8E01ED006C3C7C /* Products */; 194 | projectDirPath = ""; 195 | projectRoot = ""; 196 | targets = ( 197 | 403482531B8E01ED006C3C7C /* EasyAnimation */, 198 | 72A03EEA1C29784A002DA467 /* EasyAnimation tvOS */, 199 | ); 200 | }; 201 | /* End PBXProject section */ 202 | 203 | /* Begin PBXResourcesBuildPhase section */ 204 | 403482521B8E01ED006C3C7C /* Resources */ = { 205 | isa = PBXResourcesBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | 403482791B8E024F006C3C7C /* LICENSE in Resources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | 72A03EF41C29784A002DA467 /* Resources */ = { 213 | isa = PBXResourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | 72A03EF51C29784A002DA467 /* LICENSE in Resources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | /* End PBXResourcesBuildPhase section */ 221 | 222 | /* Begin PBXSourcesBuildPhase section */ 223 | 4034824F1B8E01ED006C3C7C /* Sources */ = { 224 | isa = PBXSourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | 4034827B1B8E024F006C3C7C /* RBBLinearInterpolation.swift in Sources */, 228 | 4034827C1B8E024F006C3C7C /* RBBSpringAnimation.swift in Sources */, 229 | 403482781B8E024F006C3C7C /* EasyAnimation.swift in Sources */, 230 | 403482771B8E024F006C3C7C /* EAAnimationFuture.swift in Sources */, 231 | 4034827A1B8E024F006C3C7C /* RBBBlockBasedArray.swift in Sources */, 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | }; 235 | 72A03EEB1C29784A002DA467 /* Sources */ = { 236 | isa = PBXSourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | 72A03EEC1C29784A002DA467 /* RBBLinearInterpolation.swift in Sources */, 240 | 72A03EED1C29784A002DA467 /* RBBSpringAnimation.swift in Sources */, 241 | 72A03EEE1C29784A002DA467 /* EasyAnimation.swift in Sources */, 242 | 72A03EEF1C29784A002DA467 /* EAAnimationFuture.swift in Sources */, 243 | 72A03EF01C29784A002DA467 /* RBBBlockBasedArray.swift in Sources */, 244 | ); 245 | runOnlyForDeploymentPostprocessing = 0; 246 | }; 247 | /* End PBXSourcesBuildPhase section */ 248 | 249 | /* Begin XCBuildConfiguration section */ 250 | 403482681B8E01ED006C3C7C /* Debug */ = { 251 | isa = XCBuildConfiguration; 252 | buildSettings = { 253 | ALWAYS_SEARCH_USER_PATHS = NO; 254 | BITCODE_GENERATION_MODE = marker; 255 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 256 | CLANG_CXX_LIBRARY = "libc++"; 257 | CLANG_ENABLE_MODULES = YES; 258 | CLANG_ENABLE_OBJC_ARC = YES; 259 | CLANG_WARN_BOOL_CONVERSION = YES; 260 | CLANG_WARN_CONSTANT_CONVERSION = YES; 261 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 262 | CLANG_WARN_EMPTY_BODY = YES; 263 | CLANG_WARN_ENUM_CONVERSION = YES; 264 | CLANG_WARN_INT_CONVERSION = YES; 265 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 266 | CLANG_WARN_UNREACHABLE_CODE = YES; 267 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 268 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 269 | COPY_PHASE_STRIP = NO; 270 | CURRENT_PROJECT_VERSION = 1; 271 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 272 | ENABLE_STRICT_OBJC_MSGSEND = YES; 273 | ENABLE_TESTABILITY = YES; 274 | GCC_C_LANGUAGE_STANDARD = gnu99; 275 | GCC_DYNAMIC_NO_PIC = NO; 276 | GCC_NO_COMMON_BLOCKS = YES; 277 | GCC_OPTIMIZATION_LEVEL = 0; 278 | GCC_PREPROCESSOR_DEFINITIONS = ( 279 | "DEBUG=1", 280 | "$(inherited)", 281 | ); 282 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 283 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 284 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 285 | GCC_WARN_UNDECLARED_SELECTOR = YES; 286 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 287 | GCC_WARN_UNUSED_FUNCTION = YES; 288 | GCC_WARN_UNUSED_VARIABLE = YES; 289 | IPHONEOS_DEPLOYMENT_TARGET = 8.4; 290 | MTL_ENABLE_DEBUG_INFO = YES; 291 | ONLY_ACTIVE_ARCH = YES; 292 | SDKROOT = iphoneos; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | TARGETED_DEVICE_FAMILY = "1,2"; 295 | VERSIONING_SYSTEM = "apple-generic"; 296 | VERSION_INFO_PREFIX = ""; 297 | }; 298 | name = Debug; 299 | }; 300 | 403482691B8E01ED006C3C7C /* Release */ = { 301 | isa = XCBuildConfiguration; 302 | buildSettings = { 303 | ALWAYS_SEARCH_USER_PATHS = NO; 304 | BITCODE_GENERATION_MODE = bitcode; 305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 306 | CLANG_CXX_LIBRARY = "libc++"; 307 | CLANG_ENABLE_MODULES = YES; 308 | CLANG_ENABLE_OBJC_ARC = YES; 309 | CLANG_WARN_BOOL_CONVERSION = YES; 310 | CLANG_WARN_CONSTANT_CONVERSION = YES; 311 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 312 | CLANG_WARN_EMPTY_BODY = YES; 313 | CLANG_WARN_ENUM_CONVERSION = YES; 314 | CLANG_WARN_INT_CONVERSION = YES; 315 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 316 | CLANG_WARN_UNREACHABLE_CODE = YES; 317 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 318 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 319 | COPY_PHASE_STRIP = NO; 320 | CURRENT_PROJECT_VERSION = 1; 321 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 322 | ENABLE_NS_ASSERTIONS = NO; 323 | ENABLE_STRICT_OBJC_MSGSEND = YES; 324 | GCC_C_LANGUAGE_STANDARD = gnu99; 325 | GCC_NO_COMMON_BLOCKS = YES; 326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 328 | GCC_WARN_UNDECLARED_SELECTOR = YES; 329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 330 | GCC_WARN_UNUSED_FUNCTION = YES; 331 | GCC_WARN_UNUSED_VARIABLE = YES; 332 | IPHONEOS_DEPLOYMENT_TARGET = 8.4; 333 | MTL_ENABLE_DEBUG_INFO = NO; 334 | SDKROOT = iphoneos; 335 | TARGETED_DEVICE_FAMILY = "1,2"; 336 | VALIDATE_PRODUCT = YES; 337 | VERSIONING_SYSTEM = "apple-generic"; 338 | VERSION_INFO_PREFIX = ""; 339 | }; 340 | name = Release; 341 | }; 342 | 4034826B1B8E01ED006C3C7C /* Debug */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | CODE_SIGN_IDENTITY = "iPhone Developer"; 346 | DEFINES_MODULE = YES; 347 | DEVELOPMENT_TEAM = ""; 348 | DYLIB_COMPATIBILITY_VERSION = 1; 349 | DYLIB_CURRENT_VERSION = 1; 350 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 351 | INFOPLIST_FILE = EasyAnimation/Info.plist; 352 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 353 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 354 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 355 | PRODUCT_BUNDLE_IDENTIFIER = "com.easyanimation.$(PRODUCT_NAME:rfc1034identifier)"; 356 | PRODUCT_NAME = "$(TARGET_NAME)"; 357 | SKIP_INSTALL = YES; 358 | SWIFT_VERSION = 4.2; 359 | }; 360 | name = Debug; 361 | }; 362 | 4034826C1B8E01ED006C3C7C /* Release */ = { 363 | isa = XCBuildConfiguration; 364 | buildSettings = { 365 | DEFINES_MODULE = YES; 366 | DEVELOPMENT_TEAM = ""; 367 | DYLIB_COMPATIBILITY_VERSION = 1; 368 | DYLIB_CURRENT_VERSION = 1; 369 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 370 | INFOPLIST_FILE = EasyAnimation/Info.plist; 371 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 372 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 373 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 374 | PRODUCT_BUNDLE_IDENTIFIER = "com.easyanimation.$(PRODUCT_NAME:rfc1034identifier)"; 375 | PRODUCT_NAME = "$(TARGET_NAME)"; 376 | SKIP_INSTALL = YES; 377 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 378 | SWIFT_VERSION = 4.2; 379 | }; 380 | name = Release; 381 | }; 382 | 72A03EF71C29784A002DA467 /* Debug */ = { 383 | isa = XCBuildConfiguration; 384 | buildSettings = { 385 | DEFINES_MODULE = YES; 386 | DEVELOPMENT_TEAM = ""; 387 | DYLIB_COMPATIBILITY_VERSION = 1; 388 | DYLIB_CURRENT_VERSION = 1; 389 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 390 | INFOPLIST_FILE = "$(SRCROOT)/EasyAnimation/Info-tvOS.plist"; 391 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 392 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 393 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 394 | PRODUCT_BUNDLE_IDENTIFIER = "com.easyanimation.$(PRODUCT_NAME:rfc1034identifier)"; 395 | PRODUCT_NAME = EasyAnimation; 396 | SDKROOT = appletvos; 397 | SKIP_INSTALL = YES; 398 | SWIFT_VERSION = 4.2; 399 | TARGETED_DEVICE_FAMILY = 3; 400 | TVOS_DEPLOYMENT_TARGET = 9.0; 401 | }; 402 | name = Debug; 403 | }; 404 | 72A03EF81C29784A002DA467 /* Release */ = { 405 | isa = XCBuildConfiguration; 406 | buildSettings = { 407 | DEFINES_MODULE = YES; 408 | DEVELOPMENT_TEAM = ""; 409 | DYLIB_COMPATIBILITY_VERSION = 1; 410 | DYLIB_CURRENT_VERSION = 1; 411 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 412 | INFOPLIST_FILE = "$(SRCROOT)/EasyAnimation/Info-tvOS.plist"; 413 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 414 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 415 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 416 | PRODUCT_BUNDLE_IDENTIFIER = "com.easyanimation.$(PRODUCT_NAME:rfc1034identifier)"; 417 | PRODUCT_NAME = EasyAnimation; 418 | SDKROOT = appletvos; 419 | SKIP_INSTALL = YES; 420 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 421 | SWIFT_VERSION = 4.2; 422 | TARGETED_DEVICE_FAMILY = 3; 423 | TVOS_DEPLOYMENT_TARGET = 9.0; 424 | }; 425 | name = Release; 426 | }; 427 | /* End XCBuildConfiguration section */ 428 | 429 | /* Begin XCConfigurationList section */ 430 | 4034824E1B8E01ED006C3C7C /* Build configuration list for PBXProject "EasyAnimation" */ = { 431 | isa = XCConfigurationList; 432 | buildConfigurations = ( 433 | 403482681B8E01ED006C3C7C /* Debug */, 434 | 403482691B8E01ED006C3C7C /* Release */, 435 | ); 436 | defaultConfigurationIsVisible = 0; 437 | defaultConfigurationName = Release; 438 | }; 439 | 4034826A1B8E01ED006C3C7C /* Build configuration list for PBXNativeTarget "EasyAnimation" */ = { 440 | isa = XCConfigurationList; 441 | buildConfigurations = ( 442 | 4034826B1B8E01ED006C3C7C /* Debug */, 443 | 4034826C1B8E01ED006C3C7C /* Release */, 444 | ); 445 | defaultConfigurationIsVisible = 0; 446 | defaultConfigurationName = Release; 447 | }; 448 | 72A03EF61C29784A002DA467 /* Build configuration list for PBXNativeTarget "EasyAnimation tvOS" */ = { 449 | isa = XCConfigurationList; 450 | buildConfigurations = ( 451 | 72A03EF71C29784A002DA467 /* Debug */, 452 | 72A03EF81C29784A002DA467 /* Release */, 453 | ); 454 | defaultConfigurationIsVisible = 0; 455 | defaultConfigurationName = Release; 456 | }; 457 | /* End XCConfigurationList section */ 458 | }; 459 | rootObject = 4034824B1B8E01ED006C3C7C /* Project object */; 460 | } 461 | -------------------------------------------------------------------------------- /EasyAnimation.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EasyAnimation.xcodeproj/xcshareddata/xcschemes/EasyAnimation tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /EasyAnimation.xcodeproj/xcshareddata/xcschemes/EasyAnimation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /EasyAnimation/EAAnimationFuture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EAAnimationFuture.swift 3 | // 4 | // Created by Marin Todorov on 5/26/15. 5 | // Copyright (c) 2015-2016 Underplot ltd. All rights reserved. 6 | // 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import UIKit 27 | 28 | /** 29 | A class that is used behind the scene to chain and/or delay animations. 30 | You do not need to create instances directly - they are created automatically when you use 31 | animateWithDuration:animation: and the like. 32 | */ 33 | 34 | public class EAAnimationFuture: Equatable, CustomStringConvertible { 35 | 36 | /* debug helpers */ 37 | private var debug: Bool = false 38 | private var debugNumber: Int = 0 39 | static private var debugCount: Int = 0 40 | 41 | /* animation properties */ 42 | var duration: CFTimeInterval = 0.0 43 | var delay: CFTimeInterval = 0.0 44 | var options: UIView.AnimationOptions = [] 45 | var animations: (() -> Void)? 46 | var completion: ((Bool) -> Void)? 47 | 48 | var identifier: String 49 | 50 | var springDamping: CGFloat = 0.0 51 | var springVelocity: CGFloat = 0.0 52 | 53 | private var loopsChain = false 54 | 55 | private static var cancelCompletions: [String: ()->Void] = [:] 56 | 57 | /* animation chain links */ 58 | var prevDelayedAnimation: EAAnimationFuture? { 59 | didSet { 60 | if let prev = prevDelayedAnimation { 61 | identifier = prev.identifier 62 | } 63 | } 64 | } 65 | var nextDelayedAnimation: EAAnimationFuture? 66 | 67 | //MARK: - Animation lifecycle 68 | 69 | init() { 70 | EAAnimationFuture.debugCount += 1 71 | self.debugNumber = EAAnimationFuture.debugCount 72 | if debug { 73 | print("animation #\(self.debugNumber)") 74 | } 75 | self.identifier = UUID().uuidString 76 | } 77 | 78 | deinit { 79 | if debug { 80 | print("deinit \(self)") 81 | } 82 | } 83 | 84 | /** 85 | An array of all "root" animations for all currently animating chains. I.e. this array contains 86 | the first link in each currently animating chain. Handy if you want to cancel all chains - just 87 | loop over `animations` and call `cancelAnimationChain` on each one. 88 | */ 89 | public static var animations: [EAAnimationFuture] = [] 90 | 91 | //MARK: Animation methods 92 | 93 | @discardableResult 94 | public func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void) -> EAAnimationFuture { 95 | return animate(withDuration: duration, animations: animations, completion: completion) 96 | } 97 | 98 | @discardableResult 99 | public func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 100 | return animate(withDuration: duration, delay: delay, options: [], animations: animations, completion: completion) 101 | } 102 | 103 | @discardableResult 104 | public func animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 105 | return animateAndChain(withDuration: duration, delay: delay, options: options, animations: animations, completion: completion) 106 | } 107 | 108 | @discardableResult 109 | public func animate(withDuration duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping dampingRatio: CGFloat, initialSpringVelocity velocity: CGFloat, options: UIView.AnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 110 | let anim = animateAndChain(withDuration: duration, delay: delay, options: options, animations: animations, completion: completion) 111 | self.springDamping = dampingRatio 112 | self.springVelocity = velocity 113 | return anim 114 | } 115 | 116 | @discardableResult 117 | public func animateAndChain(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 118 | var options = options 119 | 120 | if options.contains(.repeat) { 121 | options.remove(.repeat) 122 | loopsChain = true 123 | } 124 | 125 | self.duration = duration 126 | self.delay = delay 127 | self.options = options 128 | self.animations = animations 129 | self.completion = completion 130 | 131 | nextDelayedAnimation = EAAnimationFuture() 132 | nextDelayedAnimation!.prevDelayedAnimation = self 133 | return nextDelayedAnimation! 134 | } 135 | 136 | //MARK: - Animation control methods 137 | 138 | /** 139 | A method to cancel the animation chain of the current animation. 140 | This method cancels and removes all animations that are chained to each other in one chain. 141 | The animations will not stop immediately - the currently running animation will finish and then 142 | the complete chain will be stopped and removed. 143 | 144 | :param: completion completion closure 145 | */ 146 | 147 | public func cancelAnimationChain(_ completion: (()->Void)? = nil) { 148 | EAAnimationFuture.cancelCompletions[identifier] = completion 149 | 150 | var link = self 151 | while link.nextDelayedAnimation != nil { 152 | link = link.nextDelayedAnimation! 153 | } 154 | 155 | link.detachFromChain() 156 | 157 | if debug { 158 | print("cancelled top animation: \(link)") 159 | } 160 | } 161 | 162 | private func detachFromChain() { 163 | self.nextDelayedAnimation = nil 164 | if let previous = self.prevDelayedAnimation { 165 | if debug { 166 | print("dettach \(self)") 167 | } 168 | previous.nextDelayedAnimation = nil 169 | previous.detachFromChain() 170 | } else { 171 | if let index = EAAnimationFuture.animations.index(of: self) { 172 | if debug { 173 | print("cancel root animation #\(EAAnimationFuture.animations[index])") 174 | } 175 | EAAnimationFuture.animations.remove(at: index) 176 | } 177 | } 178 | self.prevDelayedAnimation = nil 179 | } 180 | 181 | func run() { 182 | if debug { 183 | print("run animation #\(debugNumber)") 184 | } 185 | //TODO: Check if layer-only animations fire a proper completion block 186 | if let animations = animations { 187 | options.insert(.beginFromCurrentState) 188 | let animationDelay = DispatchTime.now() + Double(Int64( Double(NSEC_PER_SEC) * self.delay )) / Double(NSEC_PER_SEC) 189 | 190 | DispatchQueue.main.asyncAfter(deadline: animationDelay) { 191 | if self.springDamping > 0.0 { 192 | //spring animation 193 | UIView.animate(withDuration: self.duration, delay: 0, usingSpringWithDamping: self.springDamping, initialSpringVelocity: self.springVelocity, options: self.options, animations: animations, completion: self.animationCompleted) 194 | } else { 195 | //basic animation 196 | UIView.animate(withDuration: self.duration, delay: 0, options: self.options, animations: animations, completion: self.animationCompleted) 197 | } 198 | } 199 | } 200 | } 201 | 202 | private func animationCompleted(_ finished: Bool) { 203 | 204 | //animation's own completion 205 | self.completion?(finished) 206 | 207 | //chain has been cancelled 208 | if let cancelCompletion = EAAnimationFuture.cancelCompletions[identifier] { 209 | if debug { 210 | print("run chain cancel completion") 211 | } 212 | cancelCompletion() 213 | detachFromChain() 214 | return 215 | } 216 | 217 | //check for .Repeat 218 | if finished && self.loopsChain { 219 | //find first animation in the chain and run it next 220 | var link = self 221 | while link.prevDelayedAnimation != nil { 222 | link = link.prevDelayedAnimation! 223 | } 224 | if debug { 225 | print("loop to \(link)") 226 | } 227 | link.run() 228 | return 229 | } 230 | 231 | //run next or destroy chain 232 | if self.nextDelayedAnimation?.animations != nil { 233 | self.nextDelayedAnimation?.run() 234 | } else { 235 | //last animation in the chain 236 | self.detachFromChain() 237 | } 238 | 239 | } 240 | 241 | public var description: String { 242 | get { 243 | if debug { 244 | return "animation #\(self.debugNumber) [\(self.identifier)] prev: \(String(describing: self.prevDelayedAnimation?.debugNumber)) next: \(String(describing: self.nextDelayedAnimation?.debugNumber))" 245 | } else { 246 | return "" 247 | } 248 | } 249 | } 250 | } 251 | 252 | public func == (lhs: EAAnimationFuture , rhs: EAAnimationFuture) -> Bool { 253 | return lhs === rhs 254 | } 255 | -------------------------------------------------------------------------------- /EasyAnimation/EasyAnimation.h: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAnimation.h 3 | // EasyAnimation 4 | // 5 | // Created by Ian Ynda-Hummel on 8/26/15. 6 | // Copyright (c) 2015 EasyAnimation. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for EasyAnimation. 12 | FOUNDATION_EXPORT double EasyAnimationVersionNumber; 13 | 14 | //! Project version string for EasyAnimation. 15 | FOUNDATION_EXPORT const unsigned char EasyAnimationVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | -------------------------------------------------------------------------------- /EasyAnimation/EasyAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAnimation.swift 3 | // 4 | // Created by Marin Todorov on 4/11/15. 5 | // Copyright (c) 2015-present Underplot ltd. All rights reserved. 6 | // 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import UIKit 27 | import ObjectiveC 28 | 29 | // MARK: EA private structures 30 | 31 | private struct PendingAnimation { 32 | let layer: CALayer 33 | let keyPath: String 34 | let fromValue: Any 35 | } 36 | 37 | private class AnimationContext { 38 | var duration: TimeInterval = 1.0 39 | var currentTime: TimeInterval = {CACurrentMediaTime()}() 40 | var delay: TimeInterval = 0.0 41 | var options: UIView.AnimationOptions? = nil 42 | var pendingAnimations = [PendingAnimation]() 43 | 44 | //spring additions 45 | var springDamping: CGFloat = 0.0 46 | var springVelocity: CGFloat = 0.0 47 | 48 | var nrOfUIKitAnimations: Int = 0 49 | } 50 | 51 | private class CompletionBlock { 52 | var context: AnimationContext 53 | var completion: ((Bool) -> Void) 54 | var nrOfExecutions: Int = 0 55 | 56 | init(context c: AnimationContext, completion cb: @escaping (Bool) -> Void) { 57 | context = c 58 | completion = cb 59 | } 60 | 61 | func wrapCompletion(_ completed: Bool) { 62 | // if no uikit animations uikit calls completion immediately 63 | nrOfExecutions+=1 64 | 65 | if context.nrOfUIKitAnimations > 0 || 66 | 67 | //if no layer animations DO call completion 68 | context.pendingAnimations.count == 0 || 69 | 70 | //skip every other call if no uikit and there are layer animations 71 | //(e.g. jump over the first immediate uikit call to completion) 72 | nrOfExecutions % 2 == 0 { 73 | 74 | completion(completed) 75 | } 76 | } 77 | } 78 | 79 | @objc public class EasyAnimation: NSObject { 80 | static fileprivate var activeAnimationContexts = [AnimationContext]() 81 | 82 | @discardableResult 83 | override init() { } 84 | 85 | public static func enable() { 86 | _ = swizzle 87 | } 88 | 89 | static private let swizzle: Void = { 90 | UIView.replaceAnimationMethods() 91 | CALayer.replaceAnimationMethods() 92 | }() 93 | } 94 | 95 | // MARK: EA animatable properties 96 | 97 | private let vanillaLayerKeys = [ 98 | "anchorPoint", "backgroundColor", "borderColor", "borderWidth", "bounds", 99 | "contentsRect", "cornerRadius", 100 | "opacity", "position", 101 | "shadowColor", "shadowOffset", "shadowOpacity", "shadowRadius", 102 | "sublayerTransform", "transform", "zPosition" 103 | ] 104 | 105 | private let specializedLayerKeys: [String: [String]] = [ 106 | CAEmitterLayer.self.description(): ["emitterPosition", "emitterZPosition", "emitterSize", "spin", "velocity", "birthRate", "lifetime"], 107 | CAGradientLayer.self.description(): ["colors", "locations", "endPoint", "startPoint"], 108 | CAReplicatorLayer.self.description(): ["instanceDelay", "instanceTransform", "instanceColor", "instanceRedOffset", "instanceGreenOffset", "instanceBlueOffset", "instanceAlphaOffset"], 109 | CAShapeLayer.self.description(): ["path", "fillColor", "lineDashPhase", "lineWidth", "miterLimit", "strokeColor", "strokeStart", "strokeEnd"], 110 | CATextLayer.self.description(): ["fontSize", "foregroundColor"] 111 | ] 112 | 113 | public extension UIView.AnimationOptions { 114 | //CA Fill modes 115 | static let fillModeNone = UIView.AnimationOptions(rawValue: 0) 116 | static let fillModeForwards = UIView.AnimationOptions(rawValue: 1024) 117 | static let fillModeBackwards = UIView.AnimationOptions(rawValue: 2048) 118 | static let fillModeBoth = UIView.AnimationOptions(rawValue: 1024 + 2048) 119 | 120 | //CA Remove on completion 121 | static let isRemovedOnCompletion = UIView.AnimationOptions(rawValue: 0) 122 | static let isNotRemovedOnCompletion = UIView.AnimationOptions(rawValue: 16384) 123 | } 124 | 125 | /** 126 | A `UIView` extension that adds super powers to animateWithDuration:animations: and the like. 127 | Check the README for code examples of what features this extension adds. 128 | */ 129 | 130 | extension UIView { 131 | 132 | // MARK: UIView animation & action methods 133 | 134 | fileprivate static func replaceAnimationMethods() { 135 | //replace actionForLayer... 136 | if 137 | let origMethod = class_getInstanceMethod(self, #selector(UIView.action(for:forKey:))), 138 | let eaMethod = class_getInstanceMethod(self, #selector(UIView.EA_actionForLayer(_:forKey:))) { 139 | method_exchangeImplementations(origMethod, eaMethod) 140 | } 141 | 142 | //replace animateWithDuration... 143 | if 144 | let origMethod = class_getClassMethod(self, #selector(UIView.animate(withDuration:animations:))), 145 | let eaMethod = class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:animations:))) { 146 | method_exchangeImplementations(origMethod, eaMethod) 147 | } 148 | 149 | if 150 | let origMethod = class_getClassMethod(self, #selector(UIView.animate(withDuration:animations:completion:))), 151 | let eaMethod = class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:animations:completion:))) { 152 | method_exchangeImplementations(origMethod, eaMethod) 153 | } 154 | 155 | if 156 | let origMethod = class_getClassMethod(self, #selector(UIView.animate(withDuration:delay:options:animations:completion:))), 157 | let eaMethod = class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:delay:options:animations:completion:))) { 158 | method_exchangeImplementations(origMethod, eaMethod) 159 | } 160 | 161 | if 162 | let origMethod = class_getClassMethod(self, #selector(UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:))), 163 | let eaMethod = class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:))) { 164 | method_exchangeImplementations(origMethod, eaMethod) 165 | } 166 | 167 | } 168 | 169 | @objc 170 | func EA_actionForLayer(_ layer: CALayer!, forKey key: String!) -> CAAction! { 171 | 172 | let result = EA_actionForLayer(layer, forKey: key) 173 | 174 | if let activeContext = EasyAnimation.activeAnimationContexts.last { 175 | if let _ = result as? NSNull { 176 | 177 | if vanillaLayerKeys.contains(key) || 178 | (specializedLayerKeys[layer.classForCoder.description()] != nil && specializedLayerKeys[layer.classForCoder.description()]!.contains(key)) { 179 | 180 | var currentKeyValue = layer.value(forKey: key) 181 | 182 | //exceptions 183 | if currentKeyValue == nil && key.hasSuffix("Color") { 184 | currentKeyValue = UIColor.clear.cgColor 185 | } 186 | 187 | //found an animatable property - add the pending animation 188 | if let currentKeyValue = currentKeyValue { 189 | activeContext.pendingAnimations.append( 190 | PendingAnimation(layer: layer, keyPath: key, fromValue: currentKeyValue) 191 | ) 192 | } 193 | } 194 | } else { 195 | activeContext.nrOfUIKitAnimations+=1 196 | } 197 | } 198 | 199 | return result 200 | } 201 | 202 | @objc 203 | class func EA_animate(withDuration duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping dampingRatio: CGFloat, initialSpringVelocity velocity: CGFloat, options: UIView.AnimationOptions, animations: () -> Void, completion: ((Bool) -> Void)?) { 204 | //create context 205 | let context = AnimationContext() 206 | context.duration = duration 207 | context.delay = delay 208 | context.options = options 209 | context.springDamping = dampingRatio 210 | context.springVelocity = velocity 211 | 212 | //push context 213 | EasyAnimation.activeAnimationContexts.append(context) 214 | 215 | //enable layer actions 216 | CATransaction.begin() 217 | CATransaction.setDisableActions(false) 218 | 219 | var completionBlock: CompletionBlock? = nil 220 | 221 | //spring animations 222 | if let completion = completion { 223 | //wrap a completion block 224 | completionBlock = CompletionBlock(context: context, completion: completion) 225 | EA_animate(withDuration: duration, delay: delay, usingSpringWithDamping: dampingRatio, initialSpringVelocity: velocity, options: options, animations: animations, completion: completionBlock!.wrapCompletion) 226 | } else { 227 | //simply schedule the animation 228 | EA_animate(withDuration: duration, delay: delay, usingSpringWithDamping: dampingRatio, initialSpringVelocity: velocity, options: options, animations: animations, completion: nil) 229 | } 230 | 231 | //pop context 232 | EasyAnimation.activeAnimationContexts.removeLast() 233 | 234 | //run pending animations 235 | for anim in context.pendingAnimations { 236 | anim.layer.add(EA_animation(anim, context: context), forKey: nil) 237 | } 238 | 239 | CATransaction.commit() 240 | } 241 | 242 | @objc 243 | class func EA_animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions, animations: () -> Void, completion: ((Bool) -> Void)?) { 244 | 245 | //create context 246 | let context = AnimationContext() 247 | context.duration = duration 248 | context.delay = delay 249 | context.options = options 250 | 251 | //push context 252 | EasyAnimation.activeAnimationContexts.append(context) 253 | 254 | //enable layer actions 255 | CATransaction.begin() 256 | CATransaction.setDisableActions(false) 257 | 258 | var completionBlock: CompletionBlock? = nil 259 | 260 | //animations 261 | if let completion = completion { 262 | //wrap a completion block 263 | completionBlock = CompletionBlock(context: context, completion: completion) 264 | EA_animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: completionBlock!.wrapCompletion) 265 | } else { 266 | //simply schedule the animation 267 | EA_animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: nil) 268 | } 269 | 270 | //pop context 271 | EasyAnimation.activeAnimationContexts.removeLast() 272 | 273 | //run pending animations 274 | for anim in context.pendingAnimations { 275 | //print("pending: \(anim.keyPath) from \(anim.fromValue) to \(anim.layer.value(forKeyPath: anim.keyPath))") 276 | anim.layer.add(EA_animation(anim, context: context), forKey: nil) 277 | } 278 | 279 | //try a timer now, than see about animation delegate 280 | if let completionBlock = completionBlock, context.nrOfUIKitAnimations == 0, context.pendingAnimations.count > 0 { 281 | Timer.scheduledTimer(timeInterval: context.duration, target: self, selector: #selector(UIView.EA_wrappedCompletionHandler(_:)), userInfo: completionBlock, repeats: false) 282 | } 283 | 284 | CATransaction.commit() 285 | } 286 | 287 | @objc 288 | class func EA_animate(withDuration duration: TimeInterval, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) { 289 | animate(withDuration: duration, delay: 0.0, options: [], animations: animations, completion: completion) 290 | } 291 | 292 | @objc 293 | class func EA_animate(withDuration duration: TimeInterval, animations: @escaping () -> Void) { 294 | animate(withDuration: duration, animations: animations, completion: nil) 295 | } 296 | 297 | @objc 298 | class func EA_wrappedCompletionHandler(_ timer: Timer) { 299 | if let completionBlock = timer.userInfo as? CompletionBlock { 300 | completionBlock.wrapCompletion(true) 301 | } 302 | } 303 | 304 | // MARK: create CA animation 305 | 306 | private class func EA_animation(_ pending: PendingAnimation, context: AnimationContext) -> CAAnimation { 307 | 308 | let anim: CAAnimation 309 | 310 | if (context.springDamping > 0.0) { 311 | //create a layer spring animation 312 | 313 | if #available(iOS 9, *) { // iOS9! 314 | anim = CASpringAnimation(keyPath: pending.keyPath) 315 | if let anim = anim as? CASpringAnimation { 316 | anim.fromValue = pending.fromValue 317 | anim.toValue = pending.layer.value(forKey: pending.keyPath) 318 | 319 | let epsilon = 0.001 320 | anim.damping = CGFloat(-2.0 * log(epsilon) / context.duration) 321 | anim.stiffness = CGFloat(pow(anim.damping, 2)) / CGFloat(pow(context.springDamping * 2, 2)) 322 | anim.mass = 1.0 323 | anim.initialVelocity = 0.0 324 | } 325 | } else { 326 | anim = RBBSpringAnimation(keyPath: pending.keyPath) 327 | if let anim = anim as? RBBSpringAnimation { 328 | anim.from = pending.fromValue 329 | anim.to = pending.layer.value(forKey: pending.keyPath) 330 | 331 | //TODO: refine the spring animation setup 332 | //lotta magic numbers to mimic UIKit springs 333 | let epsilon = 0.001 334 | anim.damping = -2.0 * log(epsilon) / context.duration 335 | anim.stiffness = Double(pow(anim.damping, 2)) / Double(pow(context.springDamping * 2, 2)) 336 | anim.mass = 1.0 337 | anim.velocity = 0.0 338 | } 339 | } 340 | } else { 341 | //create property animation 342 | anim = CABasicAnimation(keyPath: pending.keyPath) 343 | (anim as! CABasicAnimation).fromValue = pending.fromValue 344 | (anim as! CABasicAnimation).toValue = pending.layer.value(forKey: pending.keyPath) 345 | } 346 | 347 | anim.duration = context.duration 348 | 349 | if context.delay > 0.0 { 350 | anim.beginTime = context.currentTime + context.delay 351 | anim.fillMode = CAMediaTimingFillMode.backwards 352 | } 353 | 354 | //options 355 | if let options = context.options?.rawValue { 356 | 357 | if options & UIView.AnimationOptions.beginFromCurrentState.rawValue == 0 { //only repeat if not in a chain 358 | anim.autoreverses = (options & UIView.AnimationOptions.autoreverse.rawValue == UIView.AnimationOptions.autoreverse.rawValue) 359 | anim.repeatCount = (options & UIView.AnimationOptions.repeat.rawValue == UIView.AnimationOptions.repeat.rawValue) ? Float.infinity : 0 360 | } 361 | 362 | //easing 363 | var timingFunctionName = CAMediaTimingFunctionName.easeInEaseOut 364 | 365 | if options & UIView.AnimationOptions.curveLinear.rawValue == UIView.AnimationOptions.curveLinear.rawValue { 366 | //first check for linear (it's this way to take up only 2 bits) 367 | timingFunctionName = CAMediaTimingFunctionName.linear 368 | } else if options & UIView.AnimationOptions.curveEaseIn.rawValue == UIView.AnimationOptions.curveEaseIn.rawValue { 369 | timingFunctionName = CAMediaTimingFunctionName.easeIn 370 | } else if options & UIView.AnimationOptions.curveEaseOut.rawValue == UIView.AnimationOptions.curveEaseOut.rawValue { 371 | timingFunctionName = CAMediaTimingFunctionName.easeOut 372 | } 373 | 374 | anim.timingFunction = CAMediaTimingFunction(name: timingFunctionName) 375 | 376 | //fill mode 377 | if options & UIView.AnimationOptions.fillModeBoth.rawValue == UIView.AnimationOptions.fillModeBoth.rawValue { 378 | //both 379 | anim.fillMode = CAMediaTimingFillMode.both 380 | } else if options & UIView.AnimationOptions.fillModeForwards.rawValue == UIView.AnimationOptions.fillModeForwards.rawValue { 381 | //forward 382 | anim.fillMode = (anim.fillMode == CAMediaTimingFillMode.backwards) ? CAMediaTimingFillMode.both : CAMediaTimingFillMode.forwards 383 | } else if options & UIView.AnimationOptions.fillModeBackwards.rawValue == UIView.AnimationOptions.fillModeBackwards.rawValue { 384 | //backwards 385 | anim.fillMode = CAMediaTimingFillMode.backwards 386 | } 387 | 388 | //is removed on completion 389 | if options & UIView.AnimationOptions.isNotRemovedOnCompletion.rawValue == UIView.AnimationOptions.isNotRemovedOnCompletion.rawValue { 390 | anim.isRemovedOnCompletion = false 391 | } else { 392 | anim.isRemovedOnCompletion = true 393 | } 394 | } 395 | 396 | return anim 397 | } 398 | 399 | // MARK: chain animations 400 | 401 | /** 402 | Creates and runs an animation which allows other animations to be chained to it and to each other. 403 | 404 | :param: duration The animation duration in seconds 405 | :param: delay The delay before the animation starts 406 | :param: options A UIViewAnimationOptions bitmask (check UIView.animationWithDuration:delay:options:animations:completion: for more info) 407 | :param: animations Animation closure 408 | :param: completion Completion closure of type (Bool)->Void 409 | 410 | :returns: The created request. 411 | */ 412 | public class func animateAndChain(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 413 | 414 | let currentAnimation = EAAnimationFuture() 415 | currentAnimation.duration = duration 416 | currentAnimation.delay = delay 417 | currentAnimation.options = options 418 | currentAnimation.animations = animations 419 | currentAnimation.completion = completion 420 | 421 | currentAnimation.nextDelayedAnimation = EAAnimationFuture() 422 | currentAnimation.nextDelayedAnimation!.prevDelayedAnimation = currentAnimation 423 | currentAnimation.run() 424 | 425 | EAAnimationFuture.animations.append(currentAnimation) 426 | 427 | return currentAnimation.nextDelayedAnimation! 428 | } 429 | 430 | /** 431 | Creates and runs an animation which allows other animations to be chained to it and to each other. 432 | 433 | :param: duration The animation duration in seconds 434 | :param: delay The delay before the animation starts 435 | :param: usingSpringWithDamping the spring damping 436 | :param: initialSpringVelocity initial velocity of the animation 437 | :param: options A UIViewAnimationOptions bitmask (check UIView.animationWithDuration:delay:options:animations:completion: for more info) 438 | :param: animations Animation closure 439 | :param: completion Completion closure of type (Bool)->Void 440 | 441 | :returns: The created request. 442 | */ 443 | public class func animateAndChain(withDuration duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping dampingRatio: CGFloat, initialSpringVelocity velocity: CGFloat, options: UIView.AnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 444 | 445 | let currentAnimation = EAAnimationFuture() 446 | currentAnimation.duration = duration 447 | currentAnimation.delay = delay 448 | currentAnimation.options = options 449 | currentAnimation.animations = animations 450 | currentAnimation.completion = completion 451 | currentAnimation.springDamping = dampingRatio 452 | currentAnimation.springVelocity = velocity 453 | 454 | currentAnimation.nextDelayedAnimation = EAAnimationFuture() 455 | currentAnimation.nextDelayedAnimation!.prevDelayedAnimation = currentAnimation 456 | currentAnimation.run() 457 | 458 | EAAnimationFuture.animations.append(currentAnimation) 459 | 460 | return currentAnimation.nextDelayedAnimation! 461 | } 462 | } 463 | 464 | extension CALayer { 465 | // MARK: CALayer animations 466 | fileprivate static func replaceAnimationMethods() { 467 | //replace actionForKey 468 | if 469 | let origMethod = class_getInstanceMethod(self, #selector(CALayer.action(forKey:))), 470 | let eaMethod = class_getInstanceMethod(self, #selector(CALayer.EA_action(forKey:))) { 471 | method_exchangeImplementations(origMethod, eaMethod) 472 | } 473 | } 474 | 475 | @objc 476 | public func EA_action(forKey key: String!) -> CAAction! { 477 | 478 | //check if the layer has a view-delegate 479 | if let _ = delegate as? UIView { 480 | return EA_action(forKey: key) // -> this passes the ball to UIView.actionForLayer:forKey: 481 | } 482 | 483 | //create a custom easy animation and add it to the animation stack 484 | if let activeContext = EasyAnimation.activeAnimationContexts.last, 485 | vanillaLayerKeys.contains(key) || 486 | (specializedLayerKeys[self.classForCoder.description()] != nil && 487 | specializedLayerKeys[self.classForCoder.description()]!.contains(key)) { 488 | 489 | var currentKeyValue = value(forKey: key) 490 | 491 | //exceptions 492 | if currentKeyValue == nil && key.hasSuffix("Color") { 493 | currentKeyValue = UIColor.clear.cgColor 494 | } 495 | 496 | //found an animatable property - add the pending animation 497 | if let currentKeyValue = currentKeyValue { 498 | activeContext.pendingAnimations.append( 499 | PendingAnimation(layer: self, keyPath: key, fromValue: currentKeyValue 500 | ) 501 | ) 502 | } 503 | } 504 | 505 | return nil 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /EasyAnimation/Info-tvOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /EasyAnimation/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /EasyAnimation/RBBSpringAnimation/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Robert Böhnke. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /EasyAnimation/RBBSpringAnimation/RBBBlockBasedArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBBSpringAnimation.h 3 | // RBBAnimation 4 | // 5 | // Created by Robert Böhnke on 10/14/13. 6 | // Copyright (c) 2013 Robert Böhnke. All rights reserved. 7 | // 8 | 9 | // 10 | // RBBBlockBasedArray.swift 11 | // 12 | // Swift intepretation of the Objective-C original by Marin Todorov 13 | // Copyright (c) 2015-2016 Underplot ltd. All rights reserved. 14 | // 15 | 16 | import Foundation 17 | 18 | typealias RBBBlockBasedArrayBlock = (Int) -> Any 19 | 20 | class RBBBlockBasedArray: NSArray { 21 | 22 | private var countBlockBased: Int = 0 23 | private var block: RBBBlockBasedArrayBlock? = nil 24 | 25 | //can't do custom init because it's declared in an NSArray extension originally 26 | //and can't override it from here in Swift 1.2; need to do initialization from an ordinary method 27 | 28 | func setCount(_ count: Int, block: @escaping RBBBlockBasedArrayBlock) { 29 | self.countBlockBased = count; 30 | self.block = block 31 | } 32 | 33 | override var count: Int { 34 | return countBlockBased 35 | } 36 | 37 | //will crash if block is not set 38 | 39 | override func object(at index: Int) -> Any { 40 | return block!(index) 41 | } 42 | 43 | func asAnys() -> [Any] { 44 | return map {$0 as Any} 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /EasyAnimation/RBBSpringAnimation/RBBLinearInterpolation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBBLinearInterpolation.h 3 | // RBBAnimation 4 | // 5 | // Created by Robert Böhnke on 10/25/13. 6 | // Copyright (c) 2013 Robert Böhnke. All rights reserved. 7 | // 8 | 9 | // 10 | // RBBLinearInterpolation.swift 11 | // 12 | // Swift intepretation of the Objective-C original by Marin Todorov 13 | // Copyright (c) 2015-2016 Underplot ltd. All rights reserved. 14 | // 15 | 16 | import UIKit 17 | 18 | typealias RBBLinearInterpolation = (_ fraction: CGFloat) -> Any 19 | 20 | class RBBInterpolator 21 | { 22 | // MARK: public interpolation methods 23 | static func interpolate(_ from: Any, to: Any) -> RBBLinearInterpolation 24 | { 25 | //Int 26 | if let from = from as? Int, let to = to as? Int { 27 | return self.RBBInterpolateCGFloat(CGFloat(from), to: CGFloat(to)) 28 | } 29 | 30 | //Double 31 | if let from = from as? Double, let to = to as? Double { 32 | return self.RBBInterpolateCGFloat(CGFloat(from), to: CGFloat(to)) 33 | } 34 | 35 | //CGFloat 36 | if let from = from as? CGFloat, let to = to as? CGFloat { 37 | return self.RBBInterpolateCGFloat(from, to: to) 38 | } 39 | 40 | //UIColor 41 | if let from = from as? UIColor, let to = to as? UIColor { 42 | return self.RBBInterpolateUIColor(from, to: to) 43 | } 44 | 45 | //CGColorRef 46 | 47 | //NSValue 48 | if let from = from as? NSValue, let to = to as? NSValue { 49 | let type = String(cString: from.objCType) //should check to's type too? 50 | 51 | //CGPoint 52 | if type.hasPrefix("{CGPoint") { 53 | return self.RBBInterpolateCGPoint(from.cgPointValue, to: to.cgPointValue) 54 | } 55 | //CGSize 56 | if type.hasPrefix("{CGSize") { 57 | return self.RBBInterpolateCGSize(from.cgSizeValue, to: to.cgSizeValue) 58 | } 59 | //CGRect 60 | if type.hasPrefix("{CGRect") { 61 | return self.RBBInterpolateCGRect(from.cgRectValue, to: to.cgRectValue) 62 | } 63 | //CATransform3D 64 | if type.hasPrefix("{CATransform3D") { 65 | return self.RBBInterpolateCATransform3D(from.caTransform3DValue, to: to.caTransform3DValue) 66 | } 67 | } 68 | 69 | //other type? 70 | let _: RBBLinearInterpolation = {fraction in 71 | return ((fraction < 0.5) ? from : to) as AnyObject 72 | } 73 | 74 | //core foundation 75 | let fromRefType = CFGetTypeID(from as CFTypeRef) 76 | let toRefType = CFGetTypeID(to as CFTypeRef) 77 | 78 | if fromRefType == CGColor.typeID && toRefType == CGColor.typeID { 79 | //CGColor 80 | let fromCGColor = from as! CGColor 81 | let toCGColor = to as! CGColor 82 | 83 | return self.RBBInterpolateUIColor(UIColor(cgColor: fromCGColor), to: UIColor(cgColor: toCGColor)) 84 | } 85 | 86 | fatalError("Unknown type of animated property") 87 | } 88 | 89 | // MARK: private interpolation methods 90 | private static func RBBInterpolateCATransform3D(_ from: CATransform3D, to: CATransform3D) -> RBBLinearInterpolation 91 | { 92 | let delta = CATransform3D( 93 | m11: to.m11 - from.m11, 94 | m12: to.m12 - from.m12, 95 | m13: to.m13 - from.m13, 96 | m14: to.m14 - from.m14, 97 | m21: to.m21 - from.m21, 98 | m22: to.m22 - from.m22, 99 | m23: to.m23 - from.m23, 100 | m24: to.m24 - from.m24, 101 | m31: to.m31 - from.m31, 102 | m32: to.m32 - from.m32, 103 | m33: to.m33 - from.m33, 104 | m34: to.m34 - from.m34, 105 | m41: to.m41 - from.m41, 106 | m42: to.m42 - from.m42, 107 | m43: to.m43 - from.m43, 108 | m44: to.m44 - from.m44 109 | ) 110 | 111 | let result: RBBLinearInterpolation = {fraction in 112 | let transform = CATransform3D( 113 | m11: from.m11 + fraction * delta.m11, 114 | m12: from.m12 + fraction * delta.m12, 115 | m13: from.m13 + fraction * delta.m13, 116 | m14: from.m14 + fraction * delta.m14, 117 | m21: from.m21 + fraction * delta.m21, 118 | m22: from.m22 + fraction * delta.m22, 119 | m23: from.m23 + fraction * delta.m23, 120 | m24: from.m24 + fraction * delta.m24, 121 | m31: from.m31 + fraction * delta.m31, 122 | m32: from.m32 + fraction * delta.m32, 123 | m33: from.m33 + fraction * delta.m33, 124 | m34: from.m34 + fraction * delta.m34, 125 | m41: from.m41 + fraction * delta.m41, 126 | m42: from.m42 + fraction * delta.m42, 127 | m43: from.m43 + fraction * delta.m43, 128 | m44: from.m44 + fraction * delta.m44) 129 | 130 | return NSValue(caTransform3D: transform) 131 | } 132 | 133 | return result 134 | } 135 | 136 | private static func RBBInterpolateCGRect(_ from: CGRect, to: CGRect) -> RBBLinearInterpolation 137 | { 138 | let deltaX = to.origin.x - from.origin.x 139 | let deltaY = to.origin.y - from.origin.y 140 | let deltaWidth = to.size.width - from.size.width 141 | let deltaHeight = to.size.height - from.size.height 142 | 143 | let result: RBBLinearInterpolation = {fraction in 144 | let rect = CGRect( 145 | x: from.origin.x + fraction * deltaX, 146 | y: from.origin.y + fraction * deltaY, 147 | width: from.size.width + fraction * deltaWidth, 148 | height: from.size.height + fraction * deltaHeight) 149 | 150 | return NSValue(cgRect: rect) 151 | } 152 | 153 | return result 154 | } 155 | 156 | private static func RBBInterpolateCGPoint(_ from: CGPoint, to: CGPoint) -> RBBLinearInterpolation 157 | { 158 | let deltaX = to.x - from.x 159 | let deltaY = to.y - from.y 160 | 161 | let result: RBBLinearInterpolation = {fraction in 162 | let point = CGPoint( 163 | x: from.x + fraction * deltaX, 164 | y: from.y + fraction * deltaY 165 | ) 166 | 167 | return NSValue(cgPoint: point) 168 | } 169 | 170 | return result 171 | } 172 | 173 | private static func RBBInterpolateCGSize(_ from: CGSize, to: CGSize) -> RBBLinearInterpolation 174 | { 175 | let deltaWidth = to.width - from.width 176 | let deltaHeight = to.height - from.height 177 | 178 | let result: RBBLinearInterpolation = {fraction in 179 | let size = CGSize( 180 | width: from.width + fraction * deltaWidth, 181 | height: from.height + fraction * deltaHeight 182 | ) 183 | 184 | return NSValue(cgSize: size) 185 | } 186 | 187 | return result 188 | } 189 | 190 | private static func RBBInterpolateCGFloat(_ from: CGFloat, to: CGFloat) -> RBBLinearInterpolation 191 | { 192 | let delta = to - from 193 | 194 | let result: RBBLinearInterpolation = {fraction in 195 | return (from + fraction * delta) as AnyObject 196 | } 197 | 198 | return result 199 | } 200 | 201 | private static func RBBInterpolateUIColor(_ from: UIColor, to: UIColor) -> RBBLinearInterpolation 202 | { 203 | var fromHue: CGFloat = 0.0 204 | var fromSaturation: CGFloat = 0.0 205 | var fromBrightness: CGFloat = 0.0 206 | var fromAlpha: CGFloat = 0.0 207 | 208 | from.getHue(&fromHue, saturation: &fromSaturation, brightness: &fromBrightness, alpha: &fromAlpha) 209 | 210 | var toHue: CGFloat = 0.0 211 | var toSaturation: CGFloat = 0.0 212 | var toBrightness: CGFloat = 0.0 213 | var toAlpha: CGFloat = 0.0 214 | 215 | to.getHue(&toHue, saturation: &toSaturation, brightness: &toBrightness, alpha: &toAlpha) 216 | 217 | let deltaHue = toHue - fromHue 218 | let deltaSaturation = toSaturation - fromSaturation 219 | let deltaBrightness = toBrightness - fromBrightness 220 | let deltaAlpha = toAlpha - fromAlpha 221 | 222 | let result: RBBLinearInterpolation = {fraction in 223 | let hue: CGFloat = fromHue + fraction * deltaHue 224 | let saturation: CGFloat = fromSaturation + fraction * deltaSaturation 225 | let brightness: CGFloat = fromBrightness + fraction * deltaBrightness 226 | let alpha: CGFloat = fromAlpha + fraction * deltaAlpha 227 | 228 | return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha).cgColor 229 | } 230 | 231 | return result 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /EasyAnimation/RBBSpringAnimation/RBBSpringAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBBSpringAnimation.h 3 | // RBBAnimation 4 | // 5 | // Created by Robert Böhnke on 10/14/13. 6 | // Copyright (c) 2013 Robert Böhnke. All rights reserved. 7 | // 8 | 9 | // 10 | // RBBSpringAnimation.swift 11 | // 12 | // Swift intepretation of the Objective-C original by Marin Todorov 13 | // Copyright (c) 2015-2016 Underplot ltd. All rights reserved. 14 | // 15 | 16 | import UIKit 17 | 18 | class RBBSpringAnimation: CAKeyframeAnimation { 19 | 20 | var damping: Double = 0.01 21 | var velocity: Double = 0.0 22 | 23 | var from: Any? 24 | var to: Any? 25 | 26 | var allowsOverdamping: Bool = true 27 | 28 | typealias RBBAnimationBlock = (CGFloat, CGFloat) -> Any //(t, duration) 29 | 30 | var mass: Double = 1.0 31 | var stiffness: Double = 0.0 32 | 33 | private func durationForEpsilon(_ epsilon: Double) -> CFTimeInterval { 34 | let beta = damping / (2 * mass) 35 | var duration: CFTimeInterval = 0 36 | 37 | while (exp(-beta * duration) >= epsilon) { 38 | duration += 0.1 39 | } 40 | 41 | return duration 42 | } 43 | 44 | private lazy var blockArrayValues: RBBBlockBasedArray = { 45 | var result = RBBBlockBasedArray() 46 | let block: RBBBlockBasedArrayBlock = {index in 47 | return self.animationBlock(CGFloat(index) / 60.0, CGFloat(self.duration)) 48 | } 49 | result.setCount(Int(self.duration * 60), block: block) 50 | return result 51 | }() 52 | 53 | override var values: [Any]! { 54 | get { 55 | return blockArrayValues.asAnys() 56 | } 57 | set { 58 | //no storage for this property 59 | } 60 | } 61 | 62 | override init() { 63 | super.init() 64 | 65 | damping = 10 66 | mass = 1 67 | stiffness = 100 68 | 69 | calculationMode = CAAnimationCalculationMode.discrete 70 | } 71 | 72 | required init(coder aDecoder: NSCoder) { 73 | fatalError("init(coder:) has not been implemented") 74 | } 75 | 76 | // MARK: RBBAnimation 77 | private var animationBlock: RBBAnimationBlock { 78 | 79 | let b = CGFloat(damping) 80 | let m = CGFloat(mass) 81 | let k = CGFloat(stiffness) 82 | let v0 = CGFloat(velocity) 83 | 84 | if b <= 0.0 || k <= 0.0 || b <= 0.0 { 85 | fatalError("Incorrect animation values") 86 | } 87 | 88 | var beta: CGFloat = b / (2 * m) 89 | let omega0: CGFloat = sqrt(k / m) 90 | let omega1: CGFloat = sqrt((omega0 * omega0) - (beta * beta)) 91 | let omega2: CGFloat = sqrt((beta * beta) - (omega0 * omega0)) 92 | 93 | let x0: CGFloat = -1 94 | 95 | if allowsOverdamping && beta > omega0 { 96 | beta = omega0 97 | } 98 | 99 | var oscillation: (CGFloat)->CGFloat 100 | 101 | if beta < omega0 { 102 | // Underdamped 103 | oscillation = {t in 104 | let envelope: CGFloat = exp(-beta * t) 105 | 106 | let part2: CGFloat = x0 * cos(omega1 * t) 107 | let part3: CGFloat = ((beta * x0 + v0) / omega1) * sin(omega1 * t) 108 | return -x0 + envelope * (part2 + part3) 109 | }; 110 | } else if beta == omega0 { 111 | // Critically damped 112 | oscillation = {t in 113 | let envelope: CGFloat = exp(-beta * t) 114 | return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 115 | }; 116 | } else { 117 | // Overdamped 118 | oscillation = {t in 119 | let envelope: CGFloat = exp(-beta * t) 120 | let part2: CGFloat = x0 * cosh(omega2 * t) 121 | let part3: CGFloat = ((beta * x0 + v0) / omega2) * sinh(omega2 * t) 122 | return -x0 + envelope * (part2 + part3); 123 | }; 124 | } 125 | 126 | let lerp = RBBInterpolator.interpolate(self.from!, to: self.to!) 127 | let result: RBBAnimationBlock = {t, _ in 128 | return lerp(oscillation(t)) 129 | } 130 | return result 131 | } 132 | 133 | 134 | override func copy(with zone: NSZone?) -> Any { 135 | let anim = super.copy(with: zone) as! RBBSpringAnimation 136 | 137 | anim.damping = self.damping 138 | anim.velocity = self.velocity 139 | anim.duration = self.duration 140 | 141 | anim.from = self.from 142 | anim.to = self.to 143 | 144 | anim.mass = self.mass 145 | anim.stiffness = self.stiffness 146 | 147 | anim.allowsOverdamping = self.allowsOverdamping 148 | 149 | return anim 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Pages/Chains.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Spring Animation 2 | 3 | import Foundation 4 | import UIKit 5 | import XCPlayground 6 | import PlaygroundSupport 7 | 8 | 9 | func delay(seconds: Double, completion:@escaping ()->()) { 10 | DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: { 11 | completion() 12 | }) 13 | } 14 | 15 | 16 | 17 | 18 | let containerRect = CGRect(x: 0,y: 0,width: 400,height: 800) 19 | let containerView = UIView(frame: containerRect) 20 | containerView.backgroundColor = UIColor.white 21 | 22 | PlaygroundPage.current.liveView = containerView 23 | 24 | 25 | //MARK: Setup SubViews 26 | var redSquare: UIView = UIView() 27 | redSquare.backgroundColor = UIColor.red 28 | var blueSquare: UIView = UIView() 29 | blueSquare.backgroundColor = UIColor.blue 30 | containerView.addSubview(redSquare) 31 | containerView.addSubview(blueSquare) 32 | redSquare.translatesAutoresizingMaskIntoConstraints = false 33 | blueSquare.translatesAutoresizingMaskIntoConstraints = false 34 | //MARK: Container view -to- buuttons relations 35 | var redTopConstraint: NSLayoutConstraint = NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: containerView, attribute: NSLayoutAttribute.top, multiplier: 1, constant: 50.0) 36 | var redLeftConstraint: NSLayoutConstraint = NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.left, relatedBy: NSLayoutRelation.equal, toItem: containerView, attribute: NSLayoutAttribute.left, multiplier: 1, constant: 50.0) 37 | var blueTopConstraint: NSLayoutConstraint = NSLayoutConstraint(item: blueSquare, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: containerView, attribute: NSLayoutAttribute.top, multiplier: 1, constant: 50.0) 38 | var blueLeftConstraint: NSLayoutConstraint = NSLayoutConstraint(item: blueSquare, attribute: NSLayoutAttribute.left, relatedBy: NSLayoutRelation.equal, toItem: containerView, attribute: NSLayoutAttribute.left, multiplier: 1, constant: 250.0) 39 | 40 | 41 | //MARK: redSquare constraints 42 | var redwidth: NSLayoutConstraint = NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.width, multiplier: 1, constant: 100) 43 | var redHeightConstraint: NSLayoutConstraint = NSLayoutConstraint(item: redSquare, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.width, multiplier: 1, constant: 100) 44 | redSquare.addConstraints([redwidth,redHeightConstraint]) 45 | redSquare.layoutIfNeeded() 46 | 47 | //MARK: blueSquare Constraints 48 | var bluewidth: NSLayoutConstraint = NSLayoutConstraint(item: blueSquare, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.greaterThanOrEqual, toItem: nil, attribute: NSLayoutAttribute.width, multiplier: 1, constant: 100) 49 | var blueheight: NSLayoutConstraint = NSLayoutConstraint(item: blueSquare, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.greaterThanOrEqual, toItem: nil, attribute: NSLayoutAttribute.width, multiplier: 1, constant: 100) 50 | blueSquare.addConstraints([bluewidth,blueheight]) 51 | 52 | containerView.addConstraints([redLeftConstraint,redTopConstraint,blueTopConstraint,blueLeftConstraint]) 53 | containerView.layoutIfNeeded() 54 | 55 | //MARK: Animation 56 | var chain: EAAnimationFuture? 57 | 58 | func startTheChain() { 59 | 60 | // chain calls to animateWithDuration... to easily make animation sequences 61 | chain = UIView.animateAndChain(withDuration: 2.0, delay: 0.0, options: [], animations: { 62 | redTopConstraint.constant += 150.0 63 | redSquare.layoutIfNeeded() 64 | }, completion: nil).animate(withDuration: 2.0, delay: 0.0, options: [], animations: { 65 | redLeftConstraint.constant += 150.0 66 | redSquare.layoutIfNeeded() 67 | }, completion: nil).animate(withDuration: 1.0, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options: [], animations: { 68 | redSquare.transform = CGAffineTransform(rotationAngle: CGFloat(-M_PI_2)) 69 | blueSquare.layer.transform = CATransform3DMakeRotation(CGFloat(M_PI_2), 0.0, 0.0, 1.0) 70 | 71 | }, completion: nil).animate(withDuration: 0.2, animations: { 72 | redTopConstraint.constant -= 150.0 73 | blueTopConstraint.constant -= 150.0 74 | containerView.layoutIfNeeded() 75 | 76 | }).animate(withDuration: 2.0, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options:.repeat, animations: { 77 | redLeftConstraint.constant -= 150.0 78 | blueTopConstraint.constant += 150.0 79 | containerView.layoutIfNeeded() 80 | redSquare.transform = CGAffineTransform.identity 81 | blueSquare.layer.transform = CATransform3DIdentity 82 | 83 | }, completion: { (f) in 84 | print("sequence finished - will loop from start") 85 | 86 | }) 87 | 88 | 89 | } 90 | 91 | 92 | func actionCancelSequence() { 93 | 94 | print("Cancel") 95 | chain!.cancelAnimationChain({ 96 | 97 | redLeftConstraint.constant = 0 98 | redTopConstraint.constant = 0 99 | redSquare.transform = CGAffineTransform.identity 100 | blueTopConstraint.constant = 0 101 | blueSquare.layer.transform = CATransform3DIdentity 102 | 103 | UIView.animate(withDuration: 0.5, animations: { 104 | containerView.layoutIfNeeded() 105 | }) 106 | }) 107 | } 108 | 109 | delay(seconds: 1.0) { () -> () in 110 | startTheChain() 111 | } 112 | 113 | delay(seconds: 10.0) { () -> () in 114 | actionCancelSequence() 115 | } 116 | 117 | 118 | 119 | let cancelbutotn = UIButton(frame: CGRect(x: 150.0,y:300.0,width:150.0,height:40.0)) 120 | cancelbutotn.setTitle("Cancel Sequence", for: UIControlState.normal) 121 | cancelbutotn.setTitleColor(UIColor.blue, for: UIControlState.normal) 122 | cancelbutotn.addTarget(PlaygroundPage.current, action: Selector(("actionCancelSequence:")), for: UIControlEvents.touchUpInside) 123 | containerView.addSubview(cancelbutotn) 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Pages/Chains.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Pages/LayerViewAnimation.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | 2 | //: Spring Animation 3 | 4 | import Foundation 5 | import UIKit 6 | import XCPlayground 7 | import PlaygroundSupport 8 | 9 | 10 | let containerRect = CGRect(x: 0,y: 0,width: 400,height: 800) 11 | let containerView = UIView(frame: containerRect) 12 | containerView.backgroundColor = UIColor.white 13 | 14 | PlaygroundPage.current.liveView = containerView 15 | 16 | 17 | 18 | let redSquare: UIView = UIView(frame: CGRect(x:50,y:50,width:100,height:100)) 19 | redSquare.backgroundColor = UIColor.red 20 | containerView.addSubview(redSquare) 21 | 22 | let blueSquare: UIView = UIView(frame: CGRect(x:250,y:50,width:100,height:100)) 23 | blueSquare.backgroundColor = UIColor.blue 24 | containerView.addSubview(blueSquare) 25 | 26 | //animate the view and the layer 27 | UIView.animate(withDuration: 0.33, delay: 0.0, options: [.curveEaseOut, .repeat, .autoreverse], 28 | animations: { () -> Void in 29 | 30 | //view property animation 31 | redSquare.transform = CGAffineTransform(scaleX: 1.33, y: 1.5).concatenating( 32 | CGAffineTransform(translationX: 0.0, y: 50.0) 33 | ) 34 | 35 | //layer properties animations 36 | blueSquare.layer.cornerRadius = 30.0 37 | blueSquare.layer.borderWidth = 10.0 38 | blueSquare.layer.borderColor = UIColor.blue.cgColor 39 | blueSquare.layer.shadowColor = UIColor.gray.cgColor 40 | blueSquare.layer.shadowOffset = CGSize(width: 15.0, height: 15.0) 41 | blueSquare.layer.shadowOpacity = 0.5 42 | 43 | var trans3d = CATransform3DIdentity 44 | trans3d.m34 = -1.0/500.0 45 | 46 | let rotationTransform = CATransform3DRotate(trans3d, CGFloat(-M_PI_4), 0.0, 1.0, 0.0) 47 | let translationTransform = CATransform3DMakeTranslation(-50.0, 0, 0) 48 | blueSquare.layer.transform = CATransform3DConcat(rotationTransform, translationTransform) 49 | 50 | }, completion: nil) 51 | 52 | 53 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Pages/LayerViewAnimation.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Pages/Multiple Animation.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Multiple Animation 2 | 3 | import Foundation 4 | import UIKit 5 | import XCPlayground 6 | import PlaygroundSupport 7 | 8 | func delay(seconds: Double, completion:@escaping ()->()) { 9 | DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: { 10 | completion() 11 | }) 12 | } 13 | 14 | //: Views Setup 15 | var viewCount: Int = 0 16 | let maxViews: Int = 190 17 | 18 | let containerRect = CGRect(x: 0,y: 0,width: 400,height: 800) 19 | let containerView = UIView(frame: containerRect) 20 | containerView.backgroundColor = UIColor.white 21 | 22 | let countLabel: UILabel = UILabel(frame: CGRect(x: 170,y: 50,width: 200,height: 50)) 23 | containerView.addSubview(countLabel) 24 | 25 | PlaygroundPage.current.liveView = containerView 26 | 27 | //: Animation 28 | func spawn() { 29 | viewCount += 1 30 | if viewCount > maxViews { 31 | return 32 | } 33 | 34 | let v = UIView(frame: CGRect(x: 50, y: 100, width: 100, height: 100)) 35 | v.backgroundColor = UIColor(hue: CGFloat(Double(containerView.subviews.count)/Double(maxViews)), saturation: 1.0, brightness: 1.0, alpha: 1.0) 36 | v.layer.cornerRadius = 50.0 37 | containerView.addSubview(v) 38 | 39 | let duration = 5.0 40 | UIView.animateAndChain(withDuration: duration, delay: 0.0, options: [], animations: { 41 | v.center.y += 250.0 42 | }, completion: nil).animate(withDuration: duration) { 43 | v.center.x += 200.0 44 | }.animate(withDuration: duration) { 45 | v.center.y -= 250.0 46 | }.animate(withDuration: duration, delay: 0.0, options: .repeat, animations: { 47 | v.center.x -= 200.0 48 | }, completion: nil) 49 | 50 | countLabel.text = "\(viewCount) views" 51 | 52 | delay(seconds: 0.10, completion: { 53 | spawn() 54 | }) 55 | } 56 | 57 | 58 | //MARK: Animate 59 | spawn() 60 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Pages/Multiple Animation.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Pages/Spring Animation.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Spring Animation 2 | 3 | import Foundation 4 | import UIKit 5 | import XCPlayground 6 | import PlaygroundSupport 7 | 8 | let containerRect = CGRect(x: 0,y: 0,width: 400,height: 800) 9 | let containerView = UIView(frame: containerRect) 10 | containerView.backgroundColor = UIColor.white 11 | 12 | 13 | let redSquare = UIView(frame: CGRect(x: 50,y: 100,width: 100,height: 100)) 14 | redSquare.backgroundColor = UIColor.red 15 | containerView.addSubview(redSquare) 16 | 17 | let blueSquare = UIView(frame: CGRect(x: 250,y: 100,width: 100,height: 100)) 18 | blueSquare.backgroundColor = UIColor.blue 19 | blueSquare.layer.borderColor = UIColor(white: 0.2, alpha: 1.0).cgColor 20 | containerView.addSubview(blueSquare) 21 | 22 | 23 | /// Animation function 24 | func animate() { 25 | 26 | UIView.animateAndChain(withDuration: 1.0, delay: 0.0, usingSpringWithDamping: 0.33, initialSpringVelocity: 0.0, options: [], animations: { 27 | //spring animate the view 28 | redSquare.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2)).concatenating( 29 | CGAffineTransform(scaleX: 1.5, y: 1.5) 30 | ) 31 | 32 | //spring animate the layer 33 | blueSquare.layer.transform = CATransform3DConcat( 34 | CATransform3DMakeRotation(CGFloat(-M_PI_2), 0.0, 0.0, 1.0), 35 | CATransform3DMakeScale(1.5, 1.5, 1.5) 36 | ) 37 | blueSquare.layer.cornerRadius = 50.0 38 | blueSquare.layer.borderWidth = 0 39 | }, completion: nil).animate(withDuration: 1.0, delay: 0.0, usingSpringWithDamping: 0.33, initialSpringVelocity: 0.0, options: .repeat, animations: { 40 | 41 | //spring animate the view 42 | redSquare.transform = CGAffineTransform.identity 43 | 44 | //spring animate the layer 45 | blueSquare.layer.transform = CATransform3DIdentity 46 | blueSquare.layer.cornerRadius = 0.0 47 | blueSquare.layer.borderWidth = 2 48 | }, completion: nil) 49 | 50 | } 51 | 52 | //MARK: Animate 53 | animate() 54 | 55 | PlaygroundPage.current.liveView = containerView 56 | 57 | 58 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Pages/Spring Animation.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Sources/EAAnimationFuture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EAAnimationFuture.swift 3 | // 4 | // Created by Marin Todorov on 5/26/15. 5 | // Copyright (c) 2015-2016 Underplot ltd. All rights reserved. 6 | // 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import UIKit 27 | 28 | /** 29 | A class that is used behind the scene to chain and/or delay animations. 30 | You do not need to create instances directly - they are created automatically when you use 31 | animateWithDuration:animation: and the like. 32 | */ 33 | 34 | public class EAAnimationFuture: Equatable, CustomStringConvertible { 35 | 36 | /* debug helpers */ 37 | private var debug: Bool = false 38 | private var debugNumber: Int = 0 39 | static private var debugCount: Int = 0 40 | 41 | /* animation properties */ 42 | var duration: CFTimeInterval = 0.0 43 | var delay: CFTimeInterval = 0.0 44 | var options: UIViewAnimationOptions = [] 45 | var animations: (() -> Void)? 46 | var completion: ((Bool) -> Void)? 47 | 48 | var identifier: String 49 | 50 | var springDamping: CGFloat = 0.0 51 | var springVelocity: CGFloat = 0.0 52 | 53 | private var loopsChain = false 54 | 55 | private static var cancelCompletions: [String: ()->Void] = [:] 56 | 57 | /* animation chain links */ 58 | var prevDelayedAnimation: EAAnimationFuture? { 59 | didSet { 60 | if let prev = prevDelayedAnimation { 61 | identifier = prev.identifier 62 | } 63 | } 64 | } 65 | var nextDelayedAnimation: EAAnimationFuture? 66 | 67 | //MARK: - Animation lifecycle 68 | 69 | init() { 70 | EAAnimationFuture.debugCount += 1 71 | self.debugNumber = EAAnimationFuture.debugCount 72 | if debug { 73 | print("animation #\(self.debugNumber)") 74 | } 75 | self.identifier = UUID().uuidString 76 | } 77 | 78 | deinit { 79 | if debug { 80 | print("deinit \(self)") 81 | } 82 | } 83 | 84 | /** 85 | An array of all "root" animations for all currently animating chains. I.e. this array contains 86 | the first link in each currently animating chain. Handy if you want to cancel all chains - just 87 | loop over `animations` and call `cancelAnimationChain` on each one. 88 | */ 89 | public static var animations: [EAAnimationFuture] = [] 90 | 91 | //MARK: Animation methods 92 | 93 | @discardableResult 94 | public func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void) -> EAAnimationFuture { 95 | return animate(withDuration: duration, animations: animations, completion: completion) 96 | } 97 | 98 | @discardableResult 99 | public func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 100 | return animate(withDuration: duration, delay: delay, options: [], animations: animations, completion: completion) 101 | } 102 | 103 | @discardableResult 104 | public func animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIViewAnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 105 | return animateAndChain(withDuration: duration, delay: delay, options: options, animations: animations, completion: completion) 106 | } 107 | 108 | @discardableResult 109 | public func animate(withDuration duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping dampingRatio: CGFloat, initialSpringVelocity velocity: CGFloat, options: UIViewAnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 110 | let anim = animateAndChain(withDuration: duration, delay: delay, options: options, animations: animations, completion: completion) 111 | self.springDamping = dampingRatio 112 | self.springVelocity = velocity 113 | return anim 114 | } 115 | 116 | @discardableResult 117 | public func animateAndChain(withDuration duration: TimeInterval, delay: TimeInterval, options: UIViewAnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 118 | var options = options 119 | 120 | if options.contains(.repeat) { 121 | options.remove(.repeat) 122 | loopsChain = true 123 | } 124 | 125 | self.duration = duration 126 | self.delay = delay 127 | self.options = options 128 | self.animations = animations 129 | self.completion = completion 130 | 131 | nextDelayedAnimation = EAAnimationFuture() 132 | nextDelayedAnimation!.prevDelayedAnimation = self 133 | return nextDelayedAnimation! 134 | } 135 | 136 | //MARK: - Animation control methods 137 | 138 | /** 139 | A method to cancel the animation chain of the current animation. 140 | This method cancels and removes all animations that are chained to each other in one chain. 141 | The animations will not stop immediately - the currently running animation will finish and then 142 | the complete chain will be stopped and removed. 143 | 144 | :param: completion completion closure 145 | */ 146 | 147 | public func cancelAnimationChain(_ completion: (()->Void)? = nil) { 148 | EAAnimationFuture.cancelCompletions[identifier] = completion 149 | 150 | var link = self 151 | while link.nextDelayedAnimation != nil { 152 | link = link.nextDelayedAnimation! 153 | } 154 | 155 | link.detachFromChain() 156 | 157 | if debug { 158 | print("cancelled top animation: \(link)") 159 | } 160 | } 161 | 162 | private func detachFromChain() { 163 | self.nextDelayedAnimation = nil 164 | if let previous = self.prevDelayedAnimation { 165 | if debug { 166 | print("dettach \(self)") 167 | } 168 | previous.nextDelayedAnimation = nil 169 | previous.detachFromChain() 170 | } else { 171 | if let index = EAAnimationFuture.animations.index(of: self) { 172 | if debug { 173 | print("cancel root animation #\(EAAnimationFuture.animations[index])") 174 | } 175 | EAAnimationFuture.animations.remove(at: index) 176 | } 177 | } 178 | self.prevDelayedAnimation = nil 179 | } 180 | 181 | func run() { 182 | if debug { 183 | print("run animation #\(debugNumber)") 184 | } 185 | //TODO: Check if layer-only animations fire a proper completion block 186 | if let animations = animations { 187 | options.insert(.beginFromCurrentState) 188 | let animationDelay = DispatchTime.now() + Double(Int64( Double(NSEC_PER_SEC) * self.delay )) / Double(NSEC_PER_SEC) 189 | 190 | DispatchQueue.main.asyncAfter(deadline: animationDelay) { 191 | if self.springDamping > 0.0 { 192 | //spring animation 193 | UIView.animate(withDuration: self.duration, delay: 0, usingSpringWithDamping: self.springDamping, initialSpringVelocity: self.springVelocity, options: self.options, animations: animations, completion: self.animationCompleted) 194 | } else { 195 | //basic animation 196 | UIView.animate(withDuration: self.duration, delay: 0, options: self.options, animations: animations, completion: self.animationCompleted) 197 | } 198 | } 199 | } 200 | } 201 | 202 | private func animationCompleted(_ finished: Bool) { 203 | 204 | //animation's own completion 205 | self.completion?(finished) 206 | 207 | //chain has been cancelled 208 | if let cancelCompletion = EAAnimationFuture.cancelCompletions[identifier] { 209 | if debug { 210 | print("run chain cancel completion") 211 | } 212 | cancelCompletion() 213 | detachFromChain() 214 | return 215 | } 216 | 217 | //check for .Repeat 218 | if finished && self.loopsChain { 219 | //find first animation in the chain and run it next 220 | var link = self 221 | while link.prevDelayedAnimation != nil { 222 | link = link.prevDelayedAnimation! 223 | } 224 | if debug { 225 | print("loop to \(link)") 226 | } 227 | link.run() 228 | return 229 | } 230 | 231 | //run next or destroy chain 232 | if self.nextDelayedAnimation?.animations != nil { 233 | self.nextDelayedAnimation?.run() 234 | } else { 235 | //last animation in the chain 236 | self.detachFromChain() 237 | } 238 | 239 | } 240 | 241 | public var description: String { 242 | get { 243 | if debug { 244 | return "animation #\(self.debugNumber) [\(self.identifier)] prev: \(self.prevDelayedAnimation?.debugNumber) next: \(self.nextDelayedAnimation?.debugNumber)" 245 | } else { 246 | return "" 247 | } 248 | } 249 | } 250 | } 251 | 252 | public func == (lhs: EAAnimationFuture , rhs: EAAnimationFuture) -> Bool { 253 | return lhs === rhs 254 | } 255 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Sources/EasyAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EasyAnimation.swift 3 | // 4 | // Created by Marin Todorov on 4/11/15. 5 | // Copyright (c) 2015-present Underplot ltd. All rights reserved. 6 | // 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import UIKit 27 | import ObjectiveC 28 | 29 | // MARK: EA private structures 30 | 31 | private struct PendingAnimation { 32 | let layer: CALayer 33 | let keyPath: String 34 | let fromValue: Any 35 | } 36 | 37 | private class AnimationContext { 38 | var duration: TimeInterval = 1.0 39 | var currentTime: TimeInterval = {CACurrentMediaTime()}() 40 | var delay: TimeInterval = 0.0 41 | var options: UIViewAnimationOptions? = nil 42 | var pendingAnimations = [PendingAnimation]() 43 | 44 | //spring additions 45 | var springDamping: CGFloat = 0.0 46 | var springVelocity: CGFloat = 0.0 47 | 48 | var nrOfUIKitAnimations: Int = 0 49 | } 50 | 51 | private class CompletionBlock { 52 | var context: AnimationContext 53 | var completion: ((Bool) -> Void) 54 | var nrOfExecutions: Int = 0 55 | 56 | init(context c: AnimationContext, completion cb: @escaping (Bool) -> Void) { 57 | context = c 58 | completion = cb 59 | } 60 | 61 | func wrapCompletion(_ completed: Bool) { 62 | // if no uikit animations uikit calls completion immediately 63 | nrOfExecutions+=1 64 | 65 | if context.nrOfUIKitAnimations > 0 || 66 | 67 | //if no layer animations DO call completion 68 | context.pendingAnimations.count == 0 || 69 | 70 | //skip every other call if no uikit and there are layer animations 71 | //(e.g. jump over the first immediate uikit call to completion) 72 | nrOfExecutions % 2 == 0 { 73 | 74 | completion(completed) 75 | } 76 | } 77 | } 78 | 79 | @objc public class EasyAnimation: NSObject { 80 | static fileprivate var activeAnimationContexts = [AnimationContext]() 81 | 82 | @discardableResult 83 | override init() {} 84 | 85 | @objc override public class func initialize() { 86 | #if !EANoSwizzling 87 | UIView.replaceAnimationMethods() 88 | CALayer.replaceAnimationMethods() 89 | #endif 90 | } 91 | 92 | static private let _disableSwizzling: Void = { 93 | #if !EANoSwizzling 94 | UIView.replaceAnimationMethods() 95 | CALayer.replaceAnimationMethods() 96 | #endif 97 | }() 98 | 99 | public class func disableSwizzling() { 100 | _disableSwizzling 101 | } 102 | } 103 | 104 | // MARK: EA animatable properties 105 | 106 | private let vanillaLayerKeys = [ 107 | "anchorPoint", "backgroundColor", "borderColor", "borderWidth", "bounds", 108 | "contentsRect", "cornerRadius", 109 | "opacity", "position", 110 | "shadowColor", "shadowOffset", "shadowOpacity", "shadowRadius", 111 | "sublayerTransform", "transform", "zPosition" 112 | ] 113 | 114 | private let specializedLayerKeys: [String: [String]] = [ 115 | CAEmitterLayer.self.description(): ["emitterPosition", "emitterZPosition", "emitterSize", "spin", "velocity", "birthRate", "lifetime"], 116 | //TODO: test animating arrays, eg colors & locations 117 | CAGradientLayer.self.description(): ["colors", "locations", "endPoint", "startPoint"], 118 | CAReplicatorLayer.self.description(): ["instanceDelay", "instanceTransform", "instanceColor", "instanceRedOffset", "instanceGreenOffset", "instanceBlueOffset", "instanceAlphaOffset"], 119 | //TODO: test animating paths 120 | CAShapeLayer.self.description(): ["path", "fillColor", "lineDashPhase", "lineWidth", "miterLimit", "strokeColor", "strokeStart", "strokeEnd"], 121 | CATextLayer.self.description(): ["fontSize", "foregroundColor"] 122 | ] 123 | 124 | public extension UIViewAnimationOptions { 125 | //CA Fill modes 126 | static let fillModeNone = UIViewAnimationOptions(rawValue: 0) 127 | static let fillModeForwards = UIViewAnimationOptions(rawValue: 1024) 128 | static let fillModeBackwards = UIViewAnimationOptions(rawValue: 2048) 129 | static let fillModeBoth = UIViewAnimationOptions(rawValue: 1024 + 2048) 130 | 131 | //CA Remove on completion 132 | static let isRemovedOnCompletion = UIViewAnimationOptions(rawValue: 0) 133 | static let isNotRemovedOnCompletion = UIViewAnimationOptions(rawValue: 16384) 134 | } 135 | 136 | /** 137 | A `UIView` extension that adds super powers to animateWithDuration:animations: and the like. 138 | Check the README for code examples of what features this extension adds. 139 | */ 140 | 141 | extension UIView { 142 | 143 | //TODO: experiment more with path animations 144 | //public var animationPath: CGPath? { set {} get {return nil}} 145 | 146 | // MARK: UIView animation & action methods 147 | 148 | override open class func initialize() { 149 | super.initialize() 150 | EasyAnimation() 151 | } 152 | 153 | fileprivate static func replaceAnimationMethods() { 154 | //replace actionForLayer... 155 | method_exchangeImplementations( 156 | class_getInstanceMethod(self, #selector(UIView.action(for:forKey:))), 157 | class_getInstanceMethod(self, #selector(UIView.EA_actionForLayer(_:forKey:)))) 158 | 159 | //replace animateWithDuration... 160 | method_exchangeImplementations( 161 | class_getClassMethod(self, #selector(UIView.animate(withDuration:animations:))), 162 | class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:animations:)))) 163 | method_exchangeImplementations( 164 | class_getClassMethod(self, #selector(UIView.animate(withDuration:animations:completion:))), 165 | class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:animations:completion:)))) 166 | method_exchangeImplementations( 167 | class_getClassMethod(self, #selector(UIView.animate(withDuration:delay:options:animations:completion:))), 168 | class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:delay:options:animations:completion:)))) 169 | method_exchangeImplementations( 170 | class_getClassMethod(self, #selector(UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:))), 171 | class_getClassMethod(self, #selector(UIView.EA_animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)))) 172 | 173 | } 174 | 175 | func EA_actionForLayer(_ layer: CALayer!, forKey key: String!) -> CAAction! { 176 | 177 | let result = EA_actionForLayer(layer, forKey: key) 178 | 179 | if let activeContext = EasyAnimation.activeAnimationContexts.last { 180 | if let _ = result as? NSNull { 181 | 182 | if vanillaLayerKeys.contains(key) || 183 | (specializedLayerKeys[layer.classForCoder.description()] != nil && specializedLayerKeys[layer.classForCoder.description()]!.contains(key)) { 184 | 185 | var currentKeyValue = layer.value(forKey: key) 186 | 187 | //exceptions 188 | if currentKeyValue == nil && key.hasSuffix("Color") { 189 | currentKeyValue = UIColor.clear.cgColor 190 | } 191 | 192 | //found an animatable property - add the pending animation 193 | if let currentKeyValue = currentKeyValue { 194 | activeContext.pendingAnimations.append( 195 | PendingAnimation(layer: layer, keyPath: key, fromValue: currentKeyValue) 196 | ) 197 | } 198 | } 199 | } else { 200 | activeContext.nrOfUIKitAnimations+=1 201 | } 202 | } 203 | 204 | return result 205 | } 206 | 207 | class func EA_animate(withDuration duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping dampingRatio: CGFloat, initialSpringVelocity velocity: CGFloat, options: UIViewAnimationOptions, animations: () -> Void, completion: ((Bool) -> Void)?) { 208 | //create context 209 | let context = AnimationContext() 210 | context.duration = duration 211 | context.delay = delay 212 | context.options = options 213 | context.springDamping = dampingRatio 214 | context.springVelocity = velocity 215 | 216 | //push context 217 | EasyAnimation.activeAnimationContexts.append(context) 218 | 219 | //enable layer actions 220 | CATransaction.begin() 221 | CATransaction.setDisableActions(false) 222 | 223 | var completionBlock: CompletionBlock? = nil 224 | 225 | //spring animations 226 | if let completion = completion { 227 | //wrap a completion block 228 | completionBlock = CompletionBlock(context: context, completion: completion) 229 | EA_animate(withDuration: duration, delay: delay, usingSpringWithDamping: dampingRatio, initialSpringVelocity: velocity, options: options, animations: animations, completion: completionBlock!.wrapCompletion) 230 | } else { 231 | //simply schedule the animation 232 | EA_animate(withDuration: duration, delay: delay, usingSpringWithDamping: dampingRatio, initialSpringVelocity: velocity, options: options, animations: animations, completion: nil) 233 | } 234 | 235 | //pop context 236 | EasyAnimation.activeAnimationContexts.removeLast() 237 | 238 | //run pending animations 239 | for anim in context.pendingAnimations { 240 | anim.layer.add(EA_animation(anim, context: context), forKey: nil) 241 | } 242 | 243 | CATransaction.commit() 244 | } 245 | 246 | class func EA_animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIViewAnimationOptions, animations: () -> Void, completion: ((Bool) -> Void)?) { 247 | 248 | //create context 249 | let context = AnimationContext() 250 | context.duration = duration 251 | context.delay = delay 252 | context.options = options 253 | 254 | //push context 255 | EasyAnimation.activeAnimationContexts.append(context) 256 | 257 | //enable layer actions 258 | CATransaction.begin() 259 | CATransaction.setDisableActions(false) 260 | 261 | var completionBlock: CompletionBlock? = nil 262 | 263 | //animations 264 | if let completion = completion { 265 | //wrap a completion block 266 | completionBlock = CompletionBlock(context: context, completion: completion) 267 | EA_animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: completionBlock!.wrapCompletion) 268 | } else { 269 | //simply schedule the animation 270 | EA_animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: nil) 271 | } 272 | 273 | //pop context 274 | EasyAnimation.activeAnimationContexts.removeLast() 275 | 276 | //run pending animations 277 | for anim in context.pendingAnimations { 278 | //print("pending: \(anim.keyPath) from \(anim.fromValue) to \(anim.layer.value(forKeyPath: anim.keyPath))") 279 | anim.layer.add(EA_animation(anim, context: context), forKey: nil) 280 | } 281 | 282 | //try a timer now, than see about animation delegate 283 | if let completionBlock = completionBlock, context.nrOfUIKitAnimations == 0, context.pendingAnimations.count > 0 { 284 | Timer.scheduledTimer(timeInterval: context.duration, target: self, selector: #selector(UIView.EA_wrappedCompletionHandler(_:)), userInfo: completionBlock, repeats: false) 285 | } 286 | 287 | CATransaction.commit() 288 | } 289 | 290 | class func EA_animate(withDuration duration: TimeInterval, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) { 291 | animate(withDuration: duration, delay: 0.0, options: [], animations: animations, completion: completion) 292 | } 293 | 294 | class func EA_animate(withDuration duration: TimeInterval, animations: @escaping () -> Void) { 295 | animate(withDuration: duration, animations: animations, completion: nil) 296 | } 297 | 298 | class func EA_wrappedCompletionHandler(_ timer: Timer) { 299 | if let completionBlock = timer.userInfo as? CompletionBlock { 300 | completionBlock.wrapCompletion(true) 301 | } 302 | } 303 | 304 | // MARK: create CA animation 305 | 306 | private class func EA_animation(_ pending: PendingAnimation, context: AnimationContext) -> CAAnimation { 307 | 308 | let anim: CAAnimation 309 | 310 | if (context.springDamping > 0.0) { 311 | //create a layer spring animation 312 | 313 | if #available(iOS 9, *) { // iOS9! 314 | anim = CASpringAnimation(keyPath: pending.keyPath) 315 | if let anim = anim as? CASpringAnimation { 316 | anim.fromValue = pending.fromValue 317 | anim.toValue = pending.layer.value(forKey: pending.keyPath) 318 | 319 | let epsilon = 0.001 320 | anim.damping = CGFloat(-2.0 * log(epsilon) / context.duration) 321 | anim.stiffness = CGFloat(pow(anim.damping, 2)) / CGFloat(pow(context.springDamping * 2, 2)) 322 | anim.mass = 1.0 323 | anim.initialVelocity = 0.0 324 | } 325 | } else { 326 | anim = RBBSpringAnimation(keyPath: pending.keyPath) 327 | if let anim = anim as? RBBSpringAnimation { 328 | anim.from = pending.fromValue 329 | anim.to = pending.layer.value(forKey: pending.keyPath) 330 | 331 | //TODO: refine the spring animation setup 332 | //lotta magic numbers to mimic UIKit springs 333 | let epsilon = 0.001 334 | anim.damping = -2.0 * log(epsilon) / context.duration 335 | anim.stiffness = Double(pow(anim.damping, 2)) / Double(pow(context.springDamping * 2, 2)) 336 | anim.mass = 1.0 337 | anim.velocity = 0.0 338 | } 339 | } 340 | } else { 341 | //create property animation 342 | anim = CABasicAnimation(keyPath: pending.keyPath) 343 | (anim as! CABasicAnimation).fromValue = pending.fromValue 344 | (anim as! CABasicAnimation).toValue = pending.layer.value(forKey: pending.keyPath) 345 | } 346 | 347 | anim.duration = context.duration 348 | 349 | if context.delay > 0.0 { 350 | anim.beginTime = context.currentTime + context.delay 351 | anim.fillMode = kCAFillModeBackwards 352 | } 353 | 354 | //options 355 | if let options = context.options?.rawValue { 356 | 357 | if options & UIViewAnimationOptions.beginFromCurrentState.rawValue == 0 { //only repeat if not in a chain 358 | anim.autoreverses = (options & UIViewAnimationOptions.autoreverse.rawValue == UIViewAnimationOptions.autoreverse.rawValue) 359 | anim.repeatCount = (options & UIViewAnimationOptions.repeat.rawValue == UIViewAnimationOptions.repeat.rawValue) ? Float.infinity : 0 360 | } 361 | 362 | //easing 363 | var timingFunctionName = kCAMediaTimingFunctionEaseInEaseOut 364 | 365 | if options & UIViewAnimationOptions.curveLinear.rawValue == UIViewAnimationOptions.curveLinear.rawValue { 366 | //first check for linear (it's this way to take up only 2 bits) 367 | timingFunctionName = kCAMediaTimingFunctionLinear 368 | } else if options & UIViewAnimationOptions.curveEaseIn.rawValue == UIViewAnimationOptions.curveEaseIn.rawValue { 369 | timingFunctionName = kCAMediaTimingFunctionEaseIn 370 | } else if options & UIViewAnimationOptions.curveEaseOut.rawValue == UIViewAnimationOptions.curveEaseOut.rawValue { 371 | timingFunctionName = kCAMediaTimingFunctionEaseOut 372 | } 373 | 374 | anim.timingFunction = CAMediaTimingFunction(name: timingFunctionName) 375 | 376 | //fill mode 377 | if options & UIViewAnimationOptions.fillModeBoth.rawValue == UIViewAnimationOptions.fillModeBoth.rawValue { 378 | //both 379 | anim.fillMode = kCAFillModeBoth 380 | } else if options & UIViewAnimationOptions.fillModeForwards.rawValue == UIViewAnimationOptions.fillModeForwards.rawValue { 381 | //forward 382 | anim.fillMode = (anim.fillMode == kCAFillModeBackwards) ? kCAFillModeBoth : kCAFillModeForwards 383 | } else if options & UIViewAnimationOptions.fillModeBackwards.rawValue == UIViewAnimationOptions.fillModeBackwards.rawValue { 384 | //backwards 385 | anim.fillMode = kCAFillModeBackwards 386 | } 387 | 388 | //is removed on completion 389 | if options & UIViewAnimationOptions.isNotRemovedOnCompletion.rawValue == UIViewAnimationOptions.isNotRemovedOnCompletion.rawValue { 390 | anim.isRemovedOnCompletion = false 391 | } else { 392 | anim.isRemovedOnCompletion = true 393 | } 394 | } 395 | 396 | return anim 397 | } 398 | 399 | // MARK: chain animations 400 | 401 | /** 402 | Creates and runs an animation which allows other animations to be chained to it and to each other. 403 | 404 | :param: duration The animation duration in seconds 405 | :param: delay The delay before the animation starts 406 | :param: options A UIViewAnimationOptions bitmask (check UIView.animationWithDuration:delay:options:animations:completion: for more info) 407 | :param: animations Animation closure 408 | :param: completion Completion closure of type (Bool)->Void 409 | 410 | :returns: The created request. 411 | */ 412 | public class func animateAndChain(withDuration duration: TimeInterval, delay: TimeInterval, options: UIViewAnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 413 | 414 | let currentAnimation = EAAnimationFuture() 415 | currentAnimation.duration = duration 416 | currentAnimation.delay = delay 417 | currentAnimation.options = options 418 | currentAnimation.animations = animations 419 | currentAnimation.completion = completion 420 | 421 | currentAnimation.nextDelayedAnimation = EAAnimationFuture() 422 | currentAnimation.nextDelayedAnimation!.prevDelayedAnimation = currentAnimation 423 | currentAnimation.run() 424 | 425 | EAAnimationFuture.animations.append(currentAnimation) 426 | 427 | return currentAnimation.nextDelayedAnimation! 428 | } 429 | 430 | /** 431 | Creates and runs an animation which allows other animations to be chained to it and to each other. 432 | 433 | :param: duration The animation duration in seconds 434 | :param: delay The delay before the animation starts 435 | :param: usingSpringWithDamping the spring damping 436 | :param: initialSpringVelocity initial velocity of the animation 437 | :param: options A UIViewAnimationOptions bitmask (check UIView.animationWithDuration:delay:options:animations:completion: for more info) 438 | :param: animations Animation closure 439 | :param: completion Completion closure of type (Bool)->Void 440 | 441 | :returns: The created request. 442 | */ 443 | public class func animateAndChain(withDuration duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping dampingRatio: CGFloat, initialSpringVelocity velocity: CGFloat, options: UIViewAnimationOptions, animations: @escaping () -> Void, completion: ((Bool) -> Void)?) -> EAAnimationFuture { 444 | 445 | let currentAnimation = EAAnimationFuture() 446 | currentAnimation.duration = duration 447 | currentAnimation.delay = delay 448 | currentAnimation.options = options 449 | currentAnimation.animations = animations 450 | currentAnimation.completion = completion 451 | currentAnimation.springDamping = dampingRatio 452 | currentAnimation.springVelocity = velocity 453 | 454 | currentAnimation.nextDelayedAnimation = EAAnimationFuture() 455 | currentAnimation.nextDelayedAnimation!.prevDelayedAnimation = currentAnimation 456 | currentAnimation.run() 457 | 458 | EAAnimationFuture.animations.append(currentAnimation) 459 | 460 | return currentAnimation.nextDelayedAnimation! 461 | } 462 | } 463 | 464 | extension CALayer { 465 | // MARK: CALayer animations 466 | 467 | fileprivate static func replaceAnimationMethods() { 468 | //replace actionForKey 469 | method_exchangeImplementations( 470 | class_getInstanceMethod(self, #selector(CALayer.action(forKey:))), 471 | class_getInstanceMethod(self, #selector(CALayer.EA_action(forKey:)))) 472 | } 473 | 474 | public func EA_action(forKey key: String!) -> CAAction! { 475 | 476 | //check if the layer has a view-delegate 477 | if let _ = delegate as? UIView { 478 | return EA_action(forKey: key) // -> this passes the ball to UIView.actionForLayer:forKey: 479 | } 480 | 481 | //create a custom easy animation and add it to the animation stack 482 | if let activeContext = EasyAnimation.activeAnimationContexts.last, 483 | vanillaLayerKeys.contains(key) || 484 | (specializedLayerKeys[self.classForCoder.description()] != nil && 485 | specializedLayerKeys[self.classForCoder.description()]!.contains(key)) { 486 | 487 | var currentKeyValue = value(forKey: key) 488 | 489 | //exceptions 490 | if currentKeyValue == nil && key.hasSuffix("Color") { 491 | currentKeyValue = UIColor.clear.cgColor 492 | } 493 | 494 | //found an animatable property - add the pending animation 495 | if let currentKeyValue = currentKeyValue { 496 | activeContext.pendingAnimations.append( 497 | PendingAnimation(layer: self, keyPath: key, fromValue: currentKeyValue 498 | ) 499 | ) 500 | } 501 | } 502 | 503 | return nil 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Sources/RBBBlockBasedArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBBSpringAnimation.h 3 | // RBBAnimation 4 | // 5 | // Created by Robert Böhnke on 10/14/13. 6 | // Copyright (c) 2013 Robert Böhnke. All rights reserved. 7 | // 8 | 9 | // 10 | // RBBBlockBasedArray.swift 11 | // 12 | // Swift intepretation of the Objective-C original by Marin Todorov 13 | // Copyright (c) 2015-2016 Underplot ltd. All rights reserved. 14 | // 15 | 16 | import Foundation 17 | 18 | typealias RBBBlockBasedArrayBlock = (Int) -> Any 19 | 20 | class RBBBlockBasedArray: NSArray { 21 | 22 | private var countBlockBased: Int = 0 23 | private var block: RBBBlockBasedArrayBlock? = nil 24 | 25 | //can't do custom init because it's declared in an NSArray extension originally 26 | //and can't override it from here in Swift 1.2; need to do initialization from an ordinary method 27 | 28 | func setCount(_ count: Int, block: @escaping RBBBlockBasedArrayBlock) { 29 | self.countBlockBased = count; 30 | self.block = block 31 | } 32 | 33 | override var count: Int { 34 | return countBlockBased 35 | } 36 | 37 | //will crash if block is not set 38 | 39 | override func object(at index: Int) -> Any { 40 | return block!(index) 41 | } 42 | 43 | func asAnys() -> [Any] { 44 | return map {$0 as Any} 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Sources/RBBLinearInterpolation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBBLinearInterpolation.h 3 | // RBBAnimation 4 | // 5 | // Created by Robert Böhnke on 10/25/13. 6 | // Copyright (c) 2013 Robert Böhnke. All rights reserved. 7 | // 8 | 9 | // 10 | // RBBLinearInterpolation.swift 11 | // 12 | // Swift intepretation of the Objective-C original by Marin Todorov 13 | // Copyright (c) 2015-2016 Underplot ltd. All rights reserved. 14 | // 15 | 16 | import UIKit 17 | 18 | typealias RBBLinearInterpolation = (_ fraction: CGFloat) -> Any 19 | 20 | class RBBInterpolator 21 | { 22 | // MARK: public interpolation methods 23 | static func interpolate(_ from: Any, to: Any) -> RBBLinearInterpolation 24 | { 25 | //Int 26 | if let from = from as? Int, let to = to as? Int { 27 | return self.RBBInterpolateCGFloat(CGFloat(from), to: CGFloat(to)) 28 | } 29 | 30 | //Double 31 | if let from = from as? Double, let to = to as? Double { 32 | return self.RBBInterpolateCGFloat(CGFloat(from), to: CGFloat(to)) 33 | } 34 | 35 | //CGFloat 36 | if let from = from as? CGFloat, let to = to as? CGFloat { 37 | return self.RBBInterpolateCGFloat(from, to: to) 38 | } 39 | 40 | //UIColor 41 | if let from = from as? UIColor, let to = to as? UIColor { 42 | return self.RBBInterpolateUIColor(from, to: to) 43 | } 44 | 45 | //CGColorRef 46 | 47 | //NSValue 48 | if let from = from as? NSValue, let to = to as? NSValue { 49 | let type = String(cString: from.objCType) //should check to's type too? 50 | 51 | //CGPoint 52 | if type.hasPrefix("{CGPoint") { 53 | return self.RBBInterpolateCGPoint(from.cgPointValue, to: to.cgPointValue) 54 | } 55 | //CGSize 56 | if type.hasPrefix("{CGSize") { 57 | return self.RBBInterpolateCGSize(from.cgSizeValue, to: to.cgSizeValue) 58 | } 59 | //CGRect 60 | if type.hasPrefix("{CGRect") { 61 | return self.RBBInterpolateCGRect(from.cgRectValue, to: to.cgRectValue) 62 | } 63 | //CATransform3D 64 | if type.hasPrefix("{CATransform3D") { 65 | return self.RBBInterpolateCATransform3D(from.caTransform3DValue, to: to.caTransform3DValue) 66 | } 67 | } 68 | 69 | //other type? 70 | let _: RBBLinearInterpolation = {fraction in 71 | return ((fraction < 0.5) ? from : to) as AnyObject 72 | } 73 | 74 | //core foundation 75 | let fromRefType = CFGetTypeID(from as CFTypeRef) 76 | let toRefType = CFGetTypeID(to as CFTypeRef) 77 | 78 | if fromRefType == CGColor.typeID && toRefType == CGColor.typeID { 79 | //CGColor 80 | let fromCGColor = from as! CGColor 81 | let toCGColor = to as! CGColor 82 | 83 | return self.RBBInterpolateUIColor(UIColor(cgColor: fromCGColor), to: UIColor(cgColor: toCGColor)) 84 | } 85 | 86 | fatalError("Unknown type of animated property") 87 | } 88 | 89 | // MARK: private interpolation methods 90 | private static func RBBInterpolateCATransform3D(_ from: CATransform3D, to: CATransform3D) -> RBBLinearInterpolation 91 | { 92 | let delta = CATransform3D( 93 | m11: to.m11 - from.m11, 94 | m12: to.m12 - from.m12, 95 | m13: to.m13 - from.m13, 96 | m14: to.m14 - from.m14, 97 | m21: to.m21 - from.m21, 98 | m22: to.m22 - from.m22, 99 | m23: to.m23 - from.m23, 100 | m24: to.m24 - from.m24, 101 | m31: to.m31 - from.m31, 102 | m32: to.m32 - from.m32, 103 | m33: to.m33 - from.m33, 104 | m34: to.m34 - from.m34, 105 | m41: to.m41 - from.m41, 106 | m42: to.m42 - from.m42, 107 | m43: to.m43 - from.m43, 108 | m44: to.m44 - from.m44 109 | ) 110 | 111 | let result: RBBLinearInterpolation = {fraction in 112 | let transform = CATransform3D( 113 | m11: from.m11 + fraction * delta.m11, 114 | m12: from.m12 + fraction * delta.m12, 115 | m13: from.m13 + fraction * delta.m13, 116 | m14: from.m14 + fraction * delta.m14, 117 | m21: from.m21 + fraction * delta.m21, 118 | m22: from.m22 + fraction * delta.m22, 119 | m23: from.m23 + fraction * delta.m23, 120 | m24: from.m24 + fraction * delta.m24, 121 | m31: from.m31 + fraction * delta.m31, 122 | m32: from.m32 + fraction * delta.m32, 123 | m33: from.m33 + fraction * delta.m33, 124 | m34: from.m34 + fraction * delta.m34, 125 | m41: from.m41 + fraction * delta.m41, 126 | m42: from.m42 + fraction * delta.m42, 127 | m43: from.m43 + fraction * delta.m43, 128 | m44: from.m44 + fraction * delta.m44) 129 | 130 | return NSValue(caTransform3D: transform) 131 | } 132 | 133 | return result 134 | } 135 | 136 | private static func RBBInterpolateCGRect(_ from: CGRect, to: CGRect) -> RBBLinearInterpolation 137 | { 138 | let deltaX = to.origin.x - from.origin.x 139 | let deltaY = to.origin.y - from.origin.y 140 | let deltaWidth = to.size.width - from.size.width 141 | let deltaHeight = to.size.height - from.size.height 142 | 143 | let result: RBBLinearInterpolation = {fraction in 144 | let rect = CGRect( 145 | x: from.origin.x + fraction * deltaX, 146 | y: from.origin.y + fraction * deltaY, 147 | width: from.size.width + fraction * deltaWidth, 148 | height: from.size.height + fraction * deltaHeight) 149 | 150 | return NSValue(cgRect: rect) 151 | } 152 | 153 | return result 154 | } 155 | 156 | private static func RBBInterpolateCGPoint(_ from: CGPoint, to: CGPoint) -> RBBLinearInterpolation 157 | { 158 | let deltaX = to.x - from.x 159 | let deltaY = to.y - from.y 160 | 161 | let result: RBBLinearInterpolation = {fraction in 162 | let point = CGPoint( 163 | x: from.x + fraction * deltaX, 164 | y: from.y + fraction * deltaY 165 | ) 166 | 167 | return NSValue(cgPoint: point) 168 | } 169 | 170 | return result 171 | } 172 | 173 | private static func RBBInterpolateCGSize(_ from: CGSize, to: CGSize) -> RBBLinearInterpolation 174 | { 175 | let deltaWidth = to.width - from.width 176 | let deltaHeight = to.height - from.height 177 | 178 | let result: RBBLinearInterpolation = {fraction in 179 | let size = CGSize( 180 | width: from.width + fraction * deltaWidth, 181 | height: from.height + fraction * deltaHeight 182 | ) 183 | 184 | return NSValue(cgSize: size) 185 | } 186 | 187 | return result 188 | } 189 | 190 | private static func RBBInterpolateCGFloat(_ from: CGFloat, to: CGFloat) -> RBBLinearInterpolation 191 | { 192 | let delta = to - from 193 | 194 | let result: RBBLinearInterpolation = {fraction in 195 | return (from + fraction * delta) as AnyObject 196 | } 197 | 198 | return result 199 | } 200 | 201 | private static func RBBInterpolateUIColor(_ from: UIColor, to: UIColor) -> RBBLinearInterpolation 202 | { 203 | var fromHue: CGFloat = 0.0 204 | var fromSaturation: CGFloat = 0.0 205 | var fromBrightness: CGFloat = 0.0 206 | var fromAlpha: CGFloat = 0.0 207 | 208 | from.getHue(&fromHue, saturation: &fromSaturation, brightness: &fromBrightness, alpha: &fromAlpha) 209 | 210 | var toHue: CGFloat = 0.0 211 | var toSaturation: CGFloat = 0.0 212 | var toBrightness: CGFloat = 0.0 213 | var toAlpha: CGFloat = 0.0 214 | 215 | to.getHue(&toHue, saturation: &toSaturation, brightness: &toBrightness, alpha: &toAlpha) 216 | 217 | let deltaHue = toHue - fromHue 218 | let deltaSaturation = toSaturation - fromSaturation 219 | let deltaBrightness = toBrightness - fromBrightness 220 | let deltaAlpha = toAlpha - fromAlpha 221 | 222 | let result: RBBLinearInterpolation = {fraction in 223 | let hue: CGFloat = fromHue + fraction * deltaHue 224 | let saturation: CGFloat = fromSaturation + fraction * deltaSaturation 225 | let brightness: CGFloat = fromBrightness + fraction * deltaBrightness 226 | let alpha: CGFloat = fromAlpha + fraction * deltaAlpha 227 | 228 | return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha).cgColor 229 | } 230 | 231 | return result 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/Sources/RBBSpringAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RBBSpringAnimation.h 3 | // RBBAnimation 4 | // 5 | // Created by Robert Böhnke on 10/14/13. 6 | // Copyright (c) 2013 Robert Böhnke. All rights reserved. 7 | // 8 | 9 | // 10 | // RBBSpringAnimation.swift 11 | // 12 | // Swift intepretation of the Objective-C original by Marin Todorov 13 | // Copyright (c) 2015-2016 Underplot ltd. All rights reserved. 14 | // 15 | 16 | import UIKit 17 | 18 | class RBBSpringAnimation: CAKeyframeAnimation { 19 | 20 | var damping: Double = 0.01 21 | var velocity: Double = 0.0 22 | 23 | var from: Any? 24 | var to: Any? 25 | 26 | var allowsOverdamping: Bool = true 27 | 28 | typealias RBBAnimationBlock = (CGFloat, CGFloat) -> Any //(t, duration) 29 | 30 | var mass: Double = 1.0 31 | var stiffness: Double = 0.0 32 | 33 | private func durationForEpsilon(_ epsilon: Double) -> CFTimeInterval { 34 | let beta = damping / (2 * mass) 35 | var duration: CFTimeInterval = 0 36 | 37 | while (exp(-beta * duration) >= epsilon) { 38 | duration += 0.1 39 | } 40 | 41 | return duration 42 | } 43 | 44 | private lazy var blockArrayValues: RBBBlockBasedArray = { 45 | var result = RBBBlockBasedArray() 46 | let block: RBBBlockBasedArrayBlock = {index in 47 | return self.animationBlock(CGFloat(index) / 60.0, CGFloat(self.duration)) 48 | } 49 | result.setCount(Int(self.duration * 60), block: block) 50 | return result 51 | }() 52 | 53 | override var values: [Any]! { 54 | get { 55 | return blockArrayValues.asAnys() 56 | } 57 | set { 58 | //no storage for this property 59 | } 60 | } 61 | 62 | override init() { 63 | super.init() 64 | 65 | damping = 10 66 | mass = 1 67 | stiffness = 100 68 | 69 | calculationMode = kCAAnimationDiscrete 70 | } 71 | 72 | required init(coder aDecoder: NSCoder) { 73 | fatalError("init(coder:) has not been implemented") 74 | } 75 | 76 | // MARK: RBBAnimation 77 | private var animationBlock: RBBAnimationBlock { 78 | 79 | let b = CGFloat(damping) 80 | let m = CGFloat(mass) 81 | let k = CGFloat(stiffness) 82 | let v0 = CGFloat(velocity) 83 | 84 | if b <= 0.0 || k <= 0.0 || b <= 0.0 { 85 | fatalError("Incorrect animation values") 86 | } 87 | 88 | var beta: CGFloat = b / (2 * m) 89 | let omega0: CGFloat = sqrt(k / m) 90 | let omega1: CGFloat = sqrt((omega0 * omega0) - (beta * beta)) 91 | let omega2: CGFloat = sqrt((beta * beta) - (omega0 * omega0)) 92 | 93 | let x0: CGFloat = -1 94 | 95 | if allowsOverdamping && beta > omega0 { 96 | beta = omega0 97 | } 98 | 99 | var oscillation: (CGFloat)->CGFloat 100 | 101 | if beta < omega0 { 102 | // Underdamped 103 | oscillation = {t in 104 | let envelope: CGFloat = exp(-beta * t) 105 | 106 | let part2: CGFloat = x0 * cos(omega1 * t) 107 | let part3: CGFloat = ((beta * x0 + v0) / omega1) * sin(omega1 * t) 108 | return -x0 + envelope * (part2 + part3) 109 | }; 110 | } else if beta == omega0 { 111 | // Critically damped 112 | oscillation = {t in 113 | let envelope: CGFloat = exp(-beta * t) 114 | return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 115 | }; 116 | } else { 117 | // Overdamped 118 | oscillation = {t in 119 | let envelope: CGFloat = exp(-beta * t) 120 | let part2: CGFloat = x0 * cosh(omega2 * t) 121 | let part3: CGFloat = ((beta * x0 + v0) / omega2) * sinh(omega2 * t) 122 | return -x0 + envelope * (part2 + part3); 123 | }; 124 | } 125 | 126 | let lerp = RBBInterpolator.interpolate(self.from!, to: self.to!) 127 | let result: RBBAnimationBlock = {t, _ in 128 | return lerp(oscillation(t)) 129 | } 130 | return result 131 | } 132 | 133 | 134 | override func copy(with zone: NSZone?) -> Any { 135 | let anim = super.copy(with: zone) as! RBBSpringAnimation 136 | 137 | anim.damping = self.damping 138 | anim.velocity = self.velocity 139 | anim.duration = self.duration 140 | 141 | anim.from = self.from 142 | anim.to = self.to 143 | 144 | anim.mass = self.mass 145 | anim.stiffness = self.stiffness 146 | 147 | anim.allowsOverdamping = self.allowsOverdamping 148 | 149 | return anim 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /EasyAnimationDemo.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Marin Todorov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](etc/EA.png) 2 | 3 | #### ver 2.0 4 | 5 | **NB!** _Breaking changes_ in 2.0 - due to a lot of requests EasyAnimation does NOT automatically install itself when imported. You **need** to enable it by calling `EasyAnimation.enable()` somewhere in your code (AppDelegate is a good idea). 6 | 7 | _The library doesn't use any private APIs - apps using it should be fine for release on the App Store._ 8 | 9 | 10 | 11 | 18 | 24 | 25 |
12 | Intro
13 | Layer Animations
14 | Spring Layer Animations
15 | Chain Animations
16 | Cancel Chain Animations
17 |
19 | Installation
20 | Credit
21 | License
22 | Version History 23 |
26 | 27 | 28 | Intro 29 | ======== 30 | 31 | `UIView.animateWithDuration:animations:` is really easy to use and you're so familiar with its syntax that you often want it to do just a bit more for you automatically. But it doesn't and you need to import *Bloated.framework* by *Beginner Ninja Coder* in order to make a bit more advanced animations than what `animateWithDuration:animations:` allows you to. 32 | 33 | **EasyAnimation** extends what UIKit offers in terms of animations and makes your life much easier because you can do much more without learning some perky new syntax. 34 | 35 | In version 2.0 and higher to enable all EasyAnimation features you need to run **once** this code - AppDelegate is a good place since it'll enable EasyAnimation as soon as your app starts. 36 | 37 |
 38 |     EasyAnimation.enable()
 39 | 
40 | 41 | In your other classes you don't need to import EasyAnimation or do anything else. Once you call `enable()` afterwards you use the normal UIKit APIs like usual. 42 | 43 | 44 | Easy Layer Animations 45 | ======== 46 | 47 | **EasyAnimation** allows you to animate your layers straight from `animate(duration:animations:...)`. No more `CABasicAnimation` code for you. Just adjust the properties of your layers from within the `animations` block and **EasyAnimation** will take care of the rest: 48 | 49 | 50 | 51 | 60 | 61 |
CoreAnimation (before)
52 |
 53 |     let anim = CABasicAnimation(keyPath: "position.x")
 54 |     anim.fromValue = 100.0
 55 |     anim.toValue = 200.0
 56 |     anim.duration = 2.0
 57 |     view.layer.addAnimation(anim, forKey: nil)
 58 | 
59 |
62 | 63 | 64 | 65 | 72 | 73 |
EasyAnimation (after)
66 |
 67 |     UIView.animate(duration: 2.0, animations: {
 68 |         self.view.layer.position.x = 200.0
 69 |     })
 70 | 
71 |
74 | 75 | ![](etc/moveX.gif) 76 | 77 | (OK, this example actually works fine also without EasyAnimation but I still keep it here for illustrative purpose) 78 | 79 | Or if you need to specify delay, animation options and/or animation curve: 80 | 81 | 82 | 83 | 84 | 99 | 100 |
CoreAnimation (before)
85 |
 86 |     let anim = CABasicAnimation(keyPath: "position.x")
 87 |     anim.fromValue = 100.0
 88 |     anim.toValue = 200.0
 89 |     anim.duration = 2.0
 90 |     anim.fillMode = kCAFillModeBackwards
 91 |     anim.beginTime = CACurrentMediaTime() + 2.0
 92 |     anim.timingFunction = CAMediaTimingFunction(name: 
 93 |         kCAMediaTimingFunctionEaseOut)
 94 |     anim.repeatCount = Float.infinity
 95 |     anim.autoreverses = true
 96 |     view.layer.addAnimation(anim, forKey: nil)
 97 | 
98 |
101 | 102 | 103 | 104 | 118 | 119 |
EasyAnimation (after)
105 |
106 |     UIView.animate(duration: 2.0, delay: 2.0, 
107 |         options: [.repeat, .autoreverse, .curveEaseOut], 
108 |         animations: {
109 |         self.view.layer.position.x += 200.0
110 | 
111 |         // let's add more animations 
112 |         // to make it more interesting!
113 |         self.view.layer.cornerRadius = 20.0
114 |         self.view.layer.borderWidth = 5.0
115 |     }, completion: nil)
116 | 
117 |
120 | 121 | ![](etc/corners.gif) 122 | 123 | And if you want to execute a piece of code after the animation is completed - good luck setting up your animation delegate and writing the delegate methods. 124 | 125 | With **EasyAnimation** you just put your code as the `completion` parameter value and **EasyAnimation** executes it for you when your animation completes. 126 | 127 | 128 | 129 | Spring Layer Animations 130 | ======== 131 | One thing I really missed since iOS9 when using CoreAnimation and `CABasicAnimation` was that there was no easy way to create spring animations. Luckily a handy library called `RBBAnimation` provides an excellent implementation of spring animations for layers - I translated the code to Swift and included `RBBSpringAnimation` into `EasyAnimation`. 132 | 133 | Easy Animation takes care to use the new in iOS9 spring animation class `CASpringAnimation` when your app runs on iOS9 or higher and falls back to `RBBSpringAnimation` when your app runs on iOS8. 134 | 135 | Here's how the code to create a spring animation for the layer position, transform and corner radius looks like: 136 | 137 | 138 | 139 | 140 | 153 | 154 |
EasyAnimation
141 |
142 |     UIView.animate(duration: 2.0, delay: 0.0, 
143 |       usingSpringWithDamping: 0.25, 
144 |       initialSpringVelocity: 0.0, 
145 |       options: [], 
146 |       animations: {
147 |         self.view.layer.position.x += 200.0
148 |         self.view.layer.cornerRadius = 50.0
149 |         self.view.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1.0)
150 |     }, completion: nil)
151 | 
152 |
155 | 156 | ![](etc/spring.gif) 157 | 158 | [Sam Davies](https://github.com/sammyd) collaborated on the spring animations code. Thanks a ton - I couldn't have figured this one on my own! 159 | 160 | 161 | 162 | Chain Animations 163 | ======== 164 | 165 | `animate(duration:animations:..)` is really handy but chaining one animation after another is a major pain (especially if we are talking about more than 2 animations). 166 | 167 | **EasyAnimation** allows you to use a method to just chain two or more animations together. Call `animateAndChain(duration:delay:options:animations:completion:)` and then chain to it more animations. Use `animate(duration:animations...)` or any other method to create chained animations. 168 | 169 | 170 | 171 | 172 | 186 | 187 |
EasyAnimation
173 |
174 |     UIView.animateAndChain(duration: 1.0, delay: 0.0, 
175 |       options: [], animations: {
176 |         self.view.center.y += 100
177 |     }, completion: nil).animate(duration: 1.0, animations: {
178 |         self.view.center.x += 100
179 |     }).animate(duration: 1.0, animations: {
180 |         self.view.center.y -= 100
181 |     }).animate(duration: 1.0, animations: {
182 |         self.view.center.x -= 100
183 |     })
184 | 
185 |
188 | 189 | ![](etc/chain.gif) 190 | 191 | *Yes - that works, give it a try in your project :]* 192 | 193 | This code will animate the view along a rectangular path - first downwards, then to the right, then up, then to the initial point where the animation started. 194 | 195 | What a perfect oportunity to repeat the animation and make the animation run continuosly! Add `options` parameter to the last `animate(duration:...` in the chain and turn on the `.repeat` option. 196 | 197 | This will make the whole chain (e.g. the 4 animations) repeat continuously. 198 | 199 | If you want to pause between any two animations in the chain - just use the `delay` parameter and it will all just work. 200 | 201 | **Note**: `animateAndChain` does not create a serial queue to which you could add animations at any time. You schedule your animations once with one call like the example above and it runs on its own, you can't add or remove animations to and from the sequence. 202 | 203 | 204 | 205 | Cancel Chain Animations 206 | ======== 207 | 208 | If you have a repeating (or a normal) chain animation on screen you can cancel it at any time. Just grab hold of the animation object and call `cancelAnimationChain` on it any time you want. 209 | 210 |
211 | let chain = UIView.animateAndChain(duration: 1.0, delay: 0.0,
212 |     options: [], animations: {
213 |         self.square.center.y += 100
214 |     }, completion: nil).animate(duration: 1.0, animations: {
215 |   [... the rest of the animations in the chain]
216 | 
217 | 218 |
219 | chain.cancelAnimationChain()
220 | 
221 | 222 | If you want to do some cleanup after the animation chain is cancelled provide a block of code to the `cancelAnimationChain` method: 223 | 224 |
225 | chain.cancelAnimationChain({
226 |   self.myView.center = initialPosition
227 |   //etc. etc.
228 | })
229 | 
230 | 231 | 232 | The animation will not stop immediately but once it completes the current step of the chain it will stop and cancel all scheduled animations in this chain. 233 | 234 | 235 | 236 | Installation 237 | ======== 238 | 239 | * __CocoaPods__: Add to your project's **Podfile**: 240 | 241 | `pod 'EasyAnimation'` 242 | 243 | * __Carthage__: If you can help with Cartage support let me know. 244 | 245 | * __Source code__: To install with the source code - clone this repo or download the source code as a zip file. Include all files within the `EasyAnimation` folder into your project. 246 | 247 | 248 | 249 | Credit 250 | ======== 251 | 252 | Author: **Marin Todorov** 253 | 254 | * [http://www.underplot.com](http://www.underplot.com) 255 | * [https://twitter.com/icanzilb](https://twitter.com/icanzilb) 256 | 257 | More about Marin: 258 | 259 | 260 | 261 | 265 | 269 | 270 |
262 |
263 | iOS Animations by Tutorials, Author
264 |
266 |
267 | iOS Animations by Emails Newsletter, Author
268 |
271 | 272 | Includes parts of [RBBAnimation](https://github.com/robb/RBBAnimation) by [Robert Böhnke](https://github.com/robb). The code is translated from Objective-C to Swift by Marin Todorov. 273 | 274 | Collaborator on the spring animation integration: [Sam Davies](https://github.com/sammyd). 275 | 276 | 277 | 278 | License 279 | ======== 280 | `EasyAnimation` is available under the MIT license. See the LICENSE file for more info. 281 | 282 | `RBBAnimation` license: [https://github.com/robb/RBBAnimation/blob/master/LICENSE](https://github.com/robb/RBBAnimation/blob/master/LICENSE) 283 | 284 | 285 | 286 | To Do 287 | ========= 288 | 289 | * `.autoreverse` for chain animations (if possible) 290 | * add support for keyframe animation along the path via a custom property 291 | 292 | Version History 293 | ======== 294 | 295 | * 2.0 - `initialize()` is deprecated so in need of manual initialization 296 | * 1.1 - Xcode 8 297 | * 1.0.5 - Xcode 7.3 compatibility 298 | * 1.0.4 - Swift 3 compatibility changes 299 | * 1.0.2 - Fixes openGL view crashes for everyone 300 | * 1.0.1 - Bug fixes 301 | * 1.0 - Swift 2.0 and iOS9 302 | * 0.7 - round of bug fixes and a number of improvements 303 | * 0.6 - first beta version 304 | -------------------------------------------------------------------------------- /etc/EA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icanzilb/EasyAnimation/4ed72aebe9dc610b92d4d607d398f381b7451a1e/etc/EA.png -------------------------------------------------------------------------------- /etc/chain.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icanzilb/EasyAnimation/4ed72aebe9dc610b92d4d607d398f381b7451a1e/etc/chain.gif -------------------------------------------------------------------------------- /etc/corners.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icanzilb/EasyAnimation/4ed72aebe9dc610b92d4d607d398f381b7451a1e/etc/corners.gif -------------------------------------------------------------------------------- /etc/moveX.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icanzilb/EasyAnimation/4ed72aebe9dc610b92d4d607d398f381b7451a1e/etc/moveX.gif -------------------------------------------------------------------------------- /etc/spring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icanzilb/EasyAnimation/4ed72aebe9dc610b92d4d607d398f381b7451a1e/etc/spring.gif --------------------------------------------------------------------------------