├── .gitignore ├── CoreAnimationDemo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CoreAnimationDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Boat.imageset │ │ ├── Boat.png │ │ ├── Boat@2x.png │ │ ├── Boat@3x.png │ │ └── Contents.json │ ├── Contents.json │ ├── Heart_blue.imageset │ │ ├── Contents.json │ │ ├── Heart_blue.png │ │ ├── Heart_blue@2x.png │ │ └── Heart_blue@3x.png │ └── Heart_red.imageset │ │ ├── Contents.json │ │ ├── Heart_red.png │ │ ├── Heart_red@2x.png │ │ └── Heart_red@3x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Controller │ ├── EmitterVC.swift │ ├── PulsatorVC.swift │ ├── TableViewController.swift │ └── WaveVC.swift ├── Info.plist └── View │ ├── BoatWaveView.swift │ └── PulsatorLayer.swift ├── README.md └── README_resources ├── Emitter.gif ├── Pulsator.gif └── Wave.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /CoreAnimationDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4A9159CA1EE18392004051BF /* BoatWaveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A9159C91EE18392004051BF /* BoatWaveView.swift */; }; 11 | 4AB2672A1ED9273B00DC5806 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB267291ED9273B00DC5806 /* AppDelegate.swift */; }; 12 | 4AB2672F1ED9273B00DC5806 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4AB2672D1ED9273B00DC5806 /* Main.storyboard */; }; 13 | 4AB267311ED9273B00DC5806 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4AB267301ED9273B00DC5806 /* Assets.xcassets */; }; 14 | 4AB267341ED9273B00DC5806 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4AB267321ED9273B00DC5806 /* LaunchScreen.storyboard */; }; 15 | 4AB2673D1ED927B600DC5806 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB2673C1ED927B600DC5806 /* TableViewController.swift */; }; 16 | 4AB2673F1ED928EF00DC5806 /* PulsatorVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB2673E1ED928EF00DC5806 /* PulsatorVC.swift */; }; 17 | 4AB267421ED92A8F00DC5806 /* PulsatorLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB267411ED92A8F00DC5806 /* PulsatorLayer.swift */; }; 18 | 4AB267441EDFCC1E00DC5806 /* EmitterVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB267431EDFCC1E00DC5806 /* EmitterVC.swift */; }; 19 | 4AB267461EE0373100DC5806 /* WaveVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB267451EE0373100DC5806 /* WaveVC.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 4A9159C91EE18392004051BF /* BoatWaveView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoatWaveView.swift; sourceTree = ""; }; 24 | 4AB267261ED9273B00DC5806 /* CoreAnimationDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CoreAnimationDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 4AB267291ED9273B00DC5806 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | 4AB2672E1ED9273B00DC5806 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | 4AB267301ED9273B00DC5806 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | 4AB267331ED9273B00DC5806 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 29 | 4AB267351ED9273B00DC5806 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | 4AB2673C1ED927B600DC5806 /* TableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 31 | 4AB2673E1ED928EF00DC5806 /* PulsatorVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PulsatorVC.swift; sourceTree = ""; }; 32 | 4AB267411ED92A8F00DC5806 /* PulsatorLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PulsatorLayer.swift; sourceTree = ""; }; 33 | 4AB267431EDFCC1E00DC5806 /* EmitterVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmitterVC.swift; sourceTree = ""; }; 34 | 4AB267451EE0373100DC5806 /* WaveVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaveVC.swift; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 4AB267231ED9273B00DC5806 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 4AB2671D1ED9273B00DC5806 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 4AB267281ED9273B00DC5806 /* CoreAnimationDemo */, 52 | 4AB267271ED9273B00DC5806 /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | 4AB267271ED9273B00DC5806 /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 4AB267261ED9273B00DC5806 /* CoreAnimationDemo.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | 4AB267281ED9273B00DC5806 /* CoreAnimationDemo */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 4AB267401ED92A0400DC5806 /* View */, 68 | 4AB2673B1ED927A600DC5806 /* Controller */, 69 | 4AB267291ED9273B00DC5806 /* AppDelegate.swift */, 70 | 4AB2672D1ED9273B00DC5806 /* Main.storyboard */, 71 | 4AB267301ED9273B00DC5806 /* Assets.xcassets */, 72 | 4AB267321ED9273B00DC5806 /* LaunchScreen.storyboard */, 73 | 4AB267351ED9273B00DC5806 /* Info.plist */, 74 | ); 75 | path = CoreAnimationDemo; 76 | sourceTree = ""; 77 | }; 78 | 4AB2673B1ED927A600DC5806 /* Controller */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 4AB2673C1ED927B600DC5806 /* TableViewController.swift */, 82 | 4AB2673E1ED928EF00DC5806 /* PulsatorVC.swift */, 83 | 4AB267431EDFCC1E00DC5806 /* EmitterVC.swift */, 84 | 4AB267451EE0373100DC5806 /* WaveVC.swift */, 85 | ); 86 | path = Controller; 87 | sourceTree = ""; 88 | }; 89 | 4AB267401ED92A0400DC5806 /* View */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 4AB267411ED92A8F00DC5806 /* PulsatorLayer.swift */, 93 | 4A9159C91EE18392004051BF /* BoatWaveView.swift */, 94 | ); 95 | path = View; 96 | sourceTree = ""; 97 | }; 98 | /* End PBXGroup section */ 99 | 100 | /* Begin PBXNativeTarget section */ 101 | 4AB267251ED9273B00DC5806 /* CoreAnimationDemo */ = { 102 | isa = PBXNativeTarget; 103 | buildConfigurationList = 4AB267381ED9273B00DC5806 /* Build configuration list for PBXNativeTarget "CoreAnimationDemo" */; 104 | buildPhases = ( 105 | 4AB267221ED9273B00DC5806 /* Sources */, 106 | 4AB267231ED9273B00DC5806 /* Frameworks */, 107 | 4AB267241ED9273B00DC5806 /* Resources */, 108 | ); 109 | buildRules = ( 110 | ); 111 | dependencies = ( 112 | ); 113 | name = CoreAnimationDemo; 114 | productName = CoreAnimationDemo; 115 | productReference = 4AB267261ED9273B00DC5806 /* CoreAnimationDemo.app */; 116 | productType = "com.apple.product-type.application"; 117 | }; 118 | /* End PBXNativeTarget section */ 119 | 120 | /* Begin PBXProject section */ 121 | 4AB2671E1ED9273B00DC5806 /* Project object */ = { 122 | isa = PBXProject; 123 | attributes = { 124 | LastSwiftUpdateCheck = 0830; 125 | LastUpgradeCheck = 0830; 126 | ORGANIZATIONNAME = "Kaibo Lu"; 127 | TargetAttributes = { 128 | 4AB267251ED9273B00DC5806 = { 129 | CreatedOnToolsVersion = 8.3.2; 130 | DevelopmentTeam = 9LJHCTN42A; 131 | ProvisioningStyle = Automatic; 132 | }; 133 | }; 134 | }; 135 | buildConfigurationList = 4AB267211ED9273B00DC5806 /* Build configuration list for PBXProject "CoreAnimationDemo" */; 136 | compatibilityVersion = "Xcode 3.2"; 137 | developmentRegion = English; 138 | hasScannedForEncodings = 0; 139 | knownRegions = ( 140 | en, 141 | Base, 142 | ); 143 | mainGroup = 4AB2671D1ED9273B00DC5806; 144 | productRefGroup = 4AB267271ED9273B00DC5806 /* Products */; 145 | projectDirPath = ""; 146 | projectRoot = ""; 147 | targets = ( 148 | 4AB267251ED9273B00DC5806 /* CoreAnimationDemo */, 149 | ); 150 | }; 151 | /* End PBXProject section */ 152 | 153 | /* Begin PBXResourcesBuildPhase section */ 154 | 4AB267241ED9273B00DC5806 /* Resources */ = { 155 | isa = PBXResourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | 4AB267341ED9273B00DC5806 /* LaunchScreen.storyboard in Resources */, 159 | 4AB267311ED9273B00DC5806 /* Assets.xcassets in Resources */, 160 | 4AB2672F1ED9273B00DC5806 /* Main.storyboard in Resources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXResourcesBuildPhase section */ 165 | 166 | /* Begin PBXSourcesBuildPhase section */ 167 | 4AB267221ED9273B00DC5806 /* Sources */ = { 168 | isa = PBXSourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | 4AB2672A1ED9273B00DC5806 /* AppDelegate.swift in Sources */, 172 | 4A9159CA1EE18392004051BF /* BoatWaveView.swift in Sources */, 173 | 4AB267421ED92A8F00DC5806 /* PulsatorLayer.swift in Sources */, 174 | 4AB2673F1ED928EF00DC5806 /* PulsatorVC.swift in Sources */, 175 | 4AB2673D1ED927B600DC5806 /* TableViewController.swift in Sources */, 176 | 4AB267441EDFCC1E00DC5806 /* EmitterVC.swift in Sources */, 177 | 4AB267461EE0373100DC5806 /* WaveVC.swift in Sources */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | /* End PBXSourcesBuildPhase section */ 182 | 183 | /* Begin PBXVariantGroup section */ 184 | 4AB2672D1ED9273B00DC5806 /* Main.storyboard */ = { 185 | isa = PBXVariantGroup; 186 | children = ( 187 | 4AB2672E1ED9273B00DC5806 /* Base */, 188 | ); 189 | name = Main.storyboard; 190 | sourceTree = ""; 191 | }; 192 | 4AB267321ED9273B00DC5806 /* LaunchScreen.storyboard */ = { 193 | isa = PBXVariantGroup; 194 | children = ( 195 | 4AB267331ED9273B00DC5806 /* Base */, 196 | ); 197 | name = LaunchScreen.storyboard; 198 | sourceTree = ""; 199 | }; 200 | /* End PBXVariantGroup section */ 201 | 202 | /* Begin XCBuildConfiguration section */ 203 | 4AB267361ED9273B00DC5806 /* Debug */ = { 204 | isa = XCBuildConfiguration; 205 | buildSettings = { 206 | ALWAYS_SEARCH_USER_PATHS = NO; 207 | CLANG_ANALYZER_NONNULL = YES; 208 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 209 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 210 | CLANG_CXX_LIBRARY = "libc++"; 211 | CLANG_ENABLE_MODULES = YES; 212 | CLANG_ENABLE_OBJC_ARC = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_CONSTANT_CONVERSION = YES; 215 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 216 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 217 | CLANG_WARN_EMPTY_BODY = YES; 218 | CLANG_WARN_ENUM_CONVERSION = YES; 219 | CLANG_WARN_INFINITE_RECURSION = YES; 220 | CLANG_WARN_INT_CONVERSION = YES; 221 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 222 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 223 | CLANG_WARN_UNREACHABLE_CODE = YES; 224 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 225 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 226 | COPY_PHASE_STRIP = NO; 227 | DEBUG_INFORMATION_FORMAT = dwarf; 228 | ENABLE_STRICT_OBJC_MSGSEND = YES; 229 | ENABLE_TESTABILITY = YES; 230 | GCC_C_LANGUAGE_STANDARD = gnu99; 231 | GCC_DYNAMIC_NO_PIC = NO; 232 | GCC_NO_COMMON_BLOCKS = YES; 233 | GCC_OPTIMIZATION_LEVEL = 0; 234 | GCC_PREPROCESSOR_DEFINITIONS = ( 235 | "DEBUG=1", 236 | "$(inherited)", 237 | ); 238 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 239 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 240 | GCC_WARN_UNDECLARED_SELECTOR = YES; 241 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 242 | GCC_WARN_UNUSED_FUNCTION = YES; 243 | GCC_WARN_UNUSED_VARIABLE = YES; 244 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 245 | MTL_ENABLE_DEBUG_INFO = YES; 246 | ONLY_ACTIVE_ARCH = YES; 247 | SDKROOT = iphoneos; 248 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 249 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 250 | TARGETED_DEVICE_FAMILY = "1,2"; 251 | }; 252 | name = Debug; 253 | }; 254 | 4AB267371ED9273B00DC5806 /* Release */ = { 255 | isa = XCBuildConfiguration; 256 | buildSettings = { 257 | ALWAYS_SEARCH_USER_PATHS = NO; 258 | CLANG_ANALYZER_NONNULL = YES; 259 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 260 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 261 | CLANG_CXX_LIBRARY = "libc++"; 262 | CLANG_ENABLE_MODULES = YES; 263 | CLANG_ENABLE_OBJC_ARC = YES; 264 | CLANG_WARN_BOOL_CONVERSION = YES; 265 | CLANG_WARN_CONSTANT_CONVERSION = YES; 266 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 267 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 268 | CLANG_WARN_EMPTY_BODY = YES; 269 | CLANG_WARN_ENUM_CONVERSION = YES; 270 | CLANG_WARN_INFINITE_RECURSION = YES; 271 | CLANG_WARN_INT_CONVERSION = YES; 272 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 273 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 274 | CLANG_WARN_UNREACHABLE_CODE = YES; 275 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 276 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 277 | COPY_PHASE_STRIP = NO; 278 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 279 | ENABLE_NS_ASSERTIONS = NO; 280 | ENABLE_STRICT_OBJC_MSGSEND = YES; 281 | GCC_C_LANGUAGE_STANDARD = gnu99; 282 | GCC_NO_COMMON_BLOCKS = YES; 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 = 10.3; 290 | MTL_ENABLE_DEBUG_INFO = NO; 291 | SDKROOT = iphoneos; 292 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 293 | TARGETED_DEVICE_FAMILY = "1,2"; 294 | VALIDATE_PRODUCT = YES; 295 | }; 296 | name = Release; 297 | }; 298 | 4AB267391ED9273B00DC5806 /* Debug */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 302 | DEVELOPMENT_TEAM = 9LJHCTN42A; 303 | INFOPLIST_FILE = CoreAnimationDemo/Info.plist; 304 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 305 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 306 | PRODUCT_BUNDLE_IDENTIFIER = Kaibo.CoreAnimationDemo; 307 | PRODUCT_NAME = "$(TARGET_NAME)"; 308 | SWIFT_VERSION = 3.0; 309 | }; 310 | name = Debug; 311 | }; 312 | 4AB2673A1ED9273B00DC5806 /* Release */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 316 | DEVELOPMENT_TEAM = 9LJHCTN42A; 317 | INFOPLIST_FILE = CoreAnimationDemo/Info.plist; 318 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 319 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 320 | PRODUCT_BUNDLE_IDENTIFIER = Kaibo.CoreAnimationDemo; 321 | PRODUCT_NAME = "$(TARGET_NAME)"; 322 | SWIFT_VERSION = 3.0; 323 | }; 324 | name = Release; 325 | }; 326 | /* End XCBuildConfiguration section */ 327 | 328 | /* Begin XCConfigurationList section */ 329 | 4AB267211ED9273B00DC5806 /* Build configuration list for PBXProject "CoreAnimationDemo" */ = { 330 | isa = XCConfigurationList; 331 | buildConfigurations = ( 332 | 4AB267361ED9273B00DC5806 /* Debug */, 333 | 4AB267371ED9273B00DC5806 /* Release */, 334 | ); 335 | defaultConfigurationIsVisible = 0; 336 | defaultConfigurationName = Release; 337 | }; 338 | 4AB267381ED9273B00DC5806 /* Build configuration list for PBXNativeTarget "CoreAnimationDemo" */ = { 339 | isa = XCConfigurationList; 340 | buildConfigurations = ( 341 | 4AB267391ED9273B00DC5806 /* Debug */, 342 | 4AB2673A1ED9273B00DC5806 /* Release */, 343 | ); 344 | defaultConfigurationIsVisible = 0; 345 | defaultConfigurationName = Release; 346 | }; 347 | /* End XCConfigurationList section */ 348 | }; 349 | rootObject = 4AB2671E1ED9273B00DC5806 /* Project object */; 350 | } 351 | -------------------------------------------------------------------------------- /CoreAnimationDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CoreAnimationDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CoreAnimationDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CoreAnimationDemo 4 | // 5 | // Created by Kaibo Lu on 2017/5/27. 6 | // Copyright © 2017年 Kaibo Lu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Boat.imageset/Boat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/CoreAnimationDemo/Assets.xcassets/Boat.imageset/Boat.png -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Boat.imageset/Boat@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/CoreAnimationDemo/Assets.xcassets/Boat.imageset/Boat@2x.png -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Boat.imageset/Boat@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/CoreAnimationDemo/Assets.xcassets/Boat.imageset/Boat@3x.png -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Boat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Boat.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Boat@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Boat@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Heart_blue.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Heart_blue.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Heart_blue@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Heart_blue@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Heart_blue.imageset/Heart_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/CoreAnimationDemo/Assets.xcassets/Heart_blue.imageset/Heart_blue.png -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Heart_blue.imageset/Heart_blue@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/CoreAnimationDemo/Assets.xcassets/Heart_blue.imageset/Heart_blue@2x.png -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Heart_blue.imageset/Heart_blue@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/CoreAnimationDemo/Assets.xcassets/Heart_blue.imageset/Heart_blue@3x.png -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Heart_red.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Heart_red.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Heart_red@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Heart_red@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Heart_red.imageset/Heart_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/CoreAnimationDemo/Assets.xcassets/Heart_red.imageset/Heart_red.png -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Heart_red.imageset/Heart_red@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/CoreAnimationDemo/Assets.xcassets/Heart_red.imageset/Heart_red@2x.png -------------------------------------------------------------------------------- /CoreAnimationDemo/Assets.xcassets/Heart_red.imageset/Heart_red@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/CoreAnimationDemo/Assets.xcassets/Heart_red.imageset/Heart_red@3x.png -------------------------------------------------------------------------------- /CoreAnimationDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /CoreAnimationDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 95 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 113 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 131 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 150 | 157 | 164 | 171 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 275 | 282 | 289 | 296 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 345 | 352 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 374 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 395 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 416 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 437 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 458 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | -------------------------------------------------------------------------------- /CoreAnimationDemo/Controller/EmitterVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmitterVC.swift 3 | // CoreAnimationDemo 4 | // 5 | // Created by Kaibo Lu on 2017/6/1. 6 | // Copyright © 2017年 Kaibo Lu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EmitterVC: UIViewController { 12 | 13 | @IBOutlet weak var centerHeartButton: UIButton! 14 | 15 | private var rainLayer: CAEmitterLayer! 16 | 17 | private var centerHeartLayer: CAEmitterLayer! 18 | private var leftHeartLayer: CAEmitterLayer! 19 | private var rightHeartLayer: CAEmitterLayer! 20 | 21 | private var gravityLayer: CAEmitterLayer! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | title = "Emitter" 27 | 28 | setupRainLayer() 29 | setupCenterHeartLayer() 30 | setupLeftHeartLayer() 31 | setupRightHeartLayer() 32 | setupGravityLayer() 33 | } 34 | 35 | private func setupRainLayer() { 36 | rainLayer = CAEmitterLayer() 37 | rainLayer.emitterShape = kCAEmitterLayerLine // Default emit orientation is up 38 | rainLayer.emitterMode = kCAEmitterLayerOutline 39 | rainLayer.renderMode = kCAEmitterLayerOldestFirst 40 | rainLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: 0) 41 | rainLayer.emitterSize = CGSize(width: view.bounds.width, height: 0) 42 | rainLayer.birthRate = 0 // Stop animation by default 43 | 44 | let cell = CAEmitterCell() 45 | cell.contents = #imageLiteral(resourceName: "Heart_red").cgImage 46 | cell.scale = 0.1 47 | cell.lifetime = 5 48 | cell.birthRate = 1000 49 | cell.velocity = 500 50 | cell.emissionLongitude = CGFloat.pi 51 | 52 | rainLayer.emitterCells = [cell] 53 | view.layer.addSublayer(rainLayer) 54 | } 55 | 56 | private func setupCenterHeartLayer() { 57 | centerHeartLayer = CAEmitterLayer() 58 | centerHeartLayer.emitterShape = kCAEmitterLayerCircle 59 | centerHeartLayer.emitterMode = kCAEmitterLayerOutline 60 | centerHeartLayer.renderMode = kCAEmitterLayerOldestFirst 61 | centerHeartLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.midY) 62 | centerHeartLayer.emitterSize = centerHeartButton.frame.size 63 | centerHeartLayer.birthRate = 0 64 | 65 | let cell = CAEmitterCell() 66 | cell.contents = #imageLiteral(resourceName: "Heart_red").cgImage 67 | cell.lifetime = 1 68 | cell.birthRate = 2000 69 | cell.scale = 0.05 70 | cell.scaleSpeed = -0.02 71 | cell.alphaSpeed = -1 72 | cell.velocity = 30 73 | 74 | centerHeartLayer.emitterCells = [cell] 75 | view.layer.addSublayer(centerHeartLayer) 76 | } 77 | 78 | private func setupLeftHeartLayer() { 79 | leftHeartLayer = CAEmitterLayer() 80 | leftHeartLayer.emitterShape = kCAEmitterLayerPoint // default value, emit orientation is right 81 | leftHeartLayer.emitterMode = kCAEmitterLayerVolume // default value 82 | leftHeartLayer.renderMode = kCAEmitterLayerOldestFirst 83 | leftHeartLayer.emitterPosition = CGPoint(x: view.bounds.midX * 0.5, y: view.bounds.midY) 84 | leftHeartLayer.birthRate = 0 85 | 86 | let cell = CAEmitterCell() 87 | cell.contents = #imageLiteral(resourceName: "Heart_red").cgImage 88 | cell.scale = 0.5 89 | cell.lifetime = 1 90 | cell.birthRate = 1 91 | cell.alphaSpeed = -1 92 | cell.velocity = 50 93 | cell.emissionLongitude = -CGFloat.pi / 2 94 | 95 | leftHeartLayer.emitterCells = [cell] 96 | view.layer.addSublayer(leftHeartLayer) 97 | } 98 | 99 | private func setupRightHeartLayer() { 100 | rightHeartLayer = CAEmitterLayer() 101 | rightHeartLayer.renderMode = kCAEmitterLayerOldestFirst 102 | rightHeartLayer.emitterPosition = CGPoint(x: view.bounds.midX * 1.5, y: view.bounds.midY) 103 | rightHeartLayer.birthRate = 0 104 | 105 | let cell = CAEmitterCell() 106 | cell.contents = #imageLiteral(resourceName: "Heart_red").cgImage 107 | cell.scale = 0.5 108 | cell.lifetime = 1 109 | cell.birthRate = 5 110 | cell.alphaSpeed = -1 111 | cell.velocity = 50 112 | cell.emissionLongitude = -CGFloat.pi / 2 113 | cell.emissionRange = CGFloat.pi / 4 114 | 115 | rightHeartLayer.emitterCells = [cell] 116 | view.layer.addSublayer(rightHeartLayer) 117 | } 118 | 119 | private func setupGravityLayer() { 120 | gravityLayer = CAEmitterLayer() 121 | gravityLayer.renderMode = kCAEmitterLayerOldestFirst 122 | gravityLayer.emitterPosition = CGPoint(x: 0, y: view.bounds.maxY) 123 | gravityLayer.birthRate = 0 124 | 125 | let cell = CAEmitterCell() 126 | cell.contents = #imageLiteral(resourceName: "Heart_red").cgImage 127 | cell.scale = 0.5 128 | cell.lifetime = 10 129 | cell.alphaSpeed = -0.1 130 | cell.birthRate = 10 131 | cell.velocity = 100 132 | cell.yAcceleration = 20 133 | cell.emissionLongitude = -CGFloat.pi / 4 134 | cell.emissionRange = CGFloat.pi / 4 135 | cell.spin = 0 // default value 136 | cell.spinRange = CGFloat.pi * 2 137 | 138 | let cell2 = CAEmitterCell() 139 | cell2.contents = #imageLiteral(resourceName: "Heart_blue").cgImage 140 | cell2.scale = 0.3 141 | cell2.lifetime = 20 142 | cell2.alphaSpeed = -0.05 143 | cell2.birthRate = 5 144 | cell2.velocity = 135 145 | cell2.yAcceleration = 20 146 | cell2.emissionLongitude = -CGFloat.pi / 4 147 | cell2.emissionRange = CGFloat.pi / 4 148 | cell2.spin = 0 // default value 149 | cell2.spinRange = CGFloat.pi * 2 150 | 151 | gravityLayer.emitterCells = [cell, cell2] 152 | view.layer.addSublayer(gravityLayer) 153 | } 154 | 155 | @IBAction func rainButtonClicked(_ sender: UIButton) { 156 | sender.isUserInteractionEnabled = false 157 | let birthRateAnimation = CABasicAnimation(keyPath: "birthRate") 158 | birthRateAnimation.duration = 3 159 | if rainLayer.birthRate == 0 { 160 | birthRateAnimation.fromValue = 0 161 | birthRateAnimation.toValue = 1 162 | rainLayer.birthRate = 1 163 | } else { 164 | birthRateAnimation.fromValue = 1 165 | birthRateAnimation.toValue = 0 166 | rainLayer.birthRate = 0 167 | } 168 | rainLayer.add(birthRateAnimation, forKey: "birthRate") 169 | DispatchQueue.main.asyncAfter(deadline: .now() + birthRateAnimation.duration) { [weak self] in 170 | guard self != nil else { return } 171 | sender.isUserInteractionEnabled = true 172 | } 173 | } 174 | 175 | @IBAction func centerHeartButtonClicked(_ sender: UIButton) { 176 | sender.isUserInteractionEnabled = false 177 | centerHeartLayer.beginTime = CACurrentMediaTime() // There will be too many cell without setting begin time 178 | centerHeartLayer.birthRate = 1 179 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in 180 | guard let strongSelf = self else { return } 181 | strongSelf.centerHeartLayer.birthRate = 0 182 | } 183 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in 184 | guard self != nil else { return } 185 | sender.isUserInteractionEnabled = true 186 | } 187 | } 188 | 189 | @IBAction func leftHeartButtonClicked(_ sender: UIButton) { 190 | sender.isUserInteractionEnabled = false 191 | leftHeartLayer.beginTime = CACurrentMediaTime() - 1 192 | leftHeartLayer.birthRate = 1 193 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in 194 | guard let strongSelf = self else { return } 195 | strongSelf.leftHeartLayer.birthRate = 0 196 | } 197 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in 198 | guard self != nil else { return } 199 | sender.isUserInteractionEnabled = true 200 | } 201 | } 202 | 203 | @IBAction func rightHeartButtonClicked(_ sender: UIButton) { 204 | sender.isUserInteractionEnabled = false 205 | rightHeartLayer.beginTime = CACurrentMediaTime() - 0.2 206 | rightHeartLayer.birthRate = 1 207 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in 208 | guard let strongSelf = self else { return } 209 | strongSelf.rightHeartLayer.birthRate = 0 210 | } 211 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.6) { [weak self] in 212 | guard self != nil else { return } 213 | sender.isUserInteractionEnabled = true 214 | } 215 | } 216 | 217 | @IBAction func gravityButtonClicked(_ sender: UIButton) { 218 | if gravityLayer.birthRate == 0 { 219 | gravityLayer.beginTime = CACurrentMediaTime() 220 | gravityLayer.birthRate = 1 221 | } else { 222 | gravityLayer.birthRate = 0 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /CoreAnimationDemo/Controller/PulsatorVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PulsatorVC.swift 3 | // CoreAnimationDemo 4 | // 5 | // Created by Kaibo Lu on 2017/5/27. 6 | // Copyright © 2017年 Kaibo Lu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PulsatorVC: UIViewController { 12 | 13 | @IBOutlet weak var countLabel: UILabel! 14 | @IBOutlet weak var maxRadiusLabel: UILabel! 15 | @IBOutlet weak var durationLabel: UILabel! 16 | @IBOutlet weak var intervalLabel: UILabel! 17 | @IBOutlet weak var maxAlphaLabel: UILabel! 18 | 19 | var pulsatorLayer: PulsatorLayer! 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | title = "Pulsator" 25 | 26 | view.backgroundColor = .white 27 | pulsatorLayer = PulsatorLayer() 28 | pulsatorLayer.frame = CGRect(x: view.bounds.width / 2, y: 170, width: 0, height: 0) 29 | view.layer.addSublayer(pulsatorLayer) 30 | 31 | countLabel.text = "\(pulsatorLayer.pulseCount)" 32 | maxRadiusLabel.text = String(format: "%.2f", pulsatorLayer.maxRadius) 33 | durationLabel.text = String(format: "%.2f", pulsatorLayer.animationDuration) 34 | intervalLabel.text = String(format: "%.2f", pulsatorLayer.animationInterval) 35 | maxAlphaLabel.text = String(format: "%.2f", pulsatorLayer.maxAlpha) 36 | } 37 | 38 | @IBAction func countChanged(_ sender: UISlider) { 39 | pulsatorLayer.pulseCount = Int(sender.value) 40 | countLabel.text = "\(pulsatorLayer.pulseCount)" 41 | } 42 | 43 | @IBAction func maxRadiusChanged(_ sender: UISlider) { 44 | pulsatorLayer.maxRadius = CGFloat(sender.value) 45 | maxRadiusLabel.text = String(format: "%.2f", pulsatorLayer.maxRadius) 46 | } 47 | 48 | @IBAction func durationChanged(_ sender: UISlider) { 49 | pulsatorLayer.animationDuration = Double(sender.value) 50 | durationLabel.text = String(format: "%.2f", pulsatorLayer.animationDuration) 51 | } 52 | 53 | @IBAction func intervalChanged(_ sender: UISlider) { 54 | pulsatorLayer.animationInterval = Double(sender.value) 55 | intervalLabel.text = String(format: "%.2f", pulsatorLayer.animationInterval) 56 | } 57 | 58 | @IBAction func maxAlphaChanged(_ sender: UISlider) { 59 | pulsatorLayer.maxAlpha = CGFloat(sender.value) 60 | maxAlphaLabel.text = String(format: "%.2f", pulsatorLayer.maxAlpha) 61 | } 62 | 63 | @IBAction func blueToBlue(_ sender: UIButton) { 64 | pulsatorLayer.inColor = UIColor.blue.cgColor 65 | pulsatorLayer.outColor = UIColor.blue.cgColor 66 | } 67 | 68 | @IBAction func redToGreen(_ sender: UIButton) { 69 | pulsatorLayer.inColor = UIColor.red.cgColor 70 | pulsatorLayer.outColor = UIColor.green.cgColor 71 | } 72 | 73 | @IBAction func orientationChanged(_ sender: UIButton) { 74 | switch pulsatorLayer.pulseOrientation { 75 | case .out: 76 | pulsatorLayer.pulseOrientation = .in 77 | sender.setTitle("Out", for: .normal) 78 | default: 79 | pulsatorLayer.pulseOrientation = .out 80 | sender.setTitle("In", for: .normal) 81 | } 82 | } 83 | 84 | @IBAction func startOrStop(_ sender: UIButton) { 85 | if pulsatorLayer.isAnimating { 86 | pulsatorLayer.stop() 87 | sender.setTitle("Start", for: .normal) 88 | } else { 89 | pulsatorLayer.start() 90 | sender.setTitle("Stop", for: .normal) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /CoreAnimationDemo/Controller/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // CoreAnimationDemo 4 | // 5 | // Created by Kaibo Lu on 2017/5/27. 6 | // Copyright © 2017年 Kaibo Lu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TableViewController: UITableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | title = "Animations" 17 | } 18 | 19 | // MARK: - Table view data source 20 | 21 | private let vcs: [[String : String]] = [["title" : "Pulsator", "vc" : "PulsatorVC"], 22 | ["title" : "Emitter", "vc" : "EmitterVC"], 23 | ["title" : "Wave", "vc" : "WaveVC"]] 24 | 25 | override func numberOfSections(in tableView: UITableView) -> Int { 26 | return 1 27 | } 28 | 29 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 30 | return vcs.count 31 | } 32 | 33 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 34 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 35 | cell.textLabel?.text = vcs[indexPath.row]["title"] 36 | return cell 37 | } 38 | 39 | // MARK: - Table view delegate 40 | 41 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 42 | let identifier = vcs[indexPath.row]["vc"]! 43 | let vc: UIViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: identifier) 44 | navigationController?.pushViewController(vc, animated: true) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CoreAnimationDemo/Controller/WaveVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WaveVC.swift 3 | // CoreAnimationDemo 4 | // 5 | // Created by Kaibo Lu on 2017/6/1. 6 | // Copyright © 2017年 Kaibo Lu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WaveVC: UIViewController { 12 | 13 | private var boatWaveView: BoatWaveView! 14 | 15 | @IBOutlet weak var cycleCountLabel: UILabel! 16 | @IBOutlet weak var targetWaveHeightLabel: UILabel! 17 | @IBOutlet weak var horizontalStepLabel: UILabel! 18 | @IBOutlet weak var minWaterDepthLabel: UILabel! 19 | @IBOutlet weak var waveHeightStepLabel: UILabel! 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | title = "Wave" 25 | view.backgroundColor = .white 26 | 27 | boatWaveView = BoatWaveView(frame: CGRect(x: 0, y: 100, width: view.bounds.width, height: 100)) 28 | view.addSubview(boatWaveView) 29 | } 30 | 31 | @IBAction func cycleCountChanged(_ sender: UISlider) { 32 | boatWaveView.cycleCount = CGFloat(sender.value) 33 | cycleCountLabel.text = String(format: "%.2f", sender.value) 34 | } 35 | 36 | @IBAction func targetWaveHeightChanged(_ sender: UISlider) { 37 | boatWaveView.targetWaveHeight = CGFloat(sender.value) 38 | targetWaveHeightLabel.text = String(format: "%.2f", sender.value) 39 | } 40 | 41 | @IBAction func horizontalStepChanged(_ sender: UISlider) { 42 | boatWaveView.horizontalStep = CGFloat(sender.value) 43 | horizontalStepLabel.text = String(format: "%.2f", sender.value) 44 | } 45 | 46 | @IBAction func minWaterDepthChanged(_ sender: UISlider) { 47 | boatWaveView.minWaterDepth = CGFloat(sender.value) 48 | minWaterDepthLabel.text = String(format: "%.2f", sender.value) 49 | } 50 | 51 | @IBAction func waveHeightStepChanged(_ sender: UISlider) { 52 | boatWaveView.waveHeightStep = CGFloat(sender.value) 53 | waveHeightStepLabel.text = String(format: "%.2f", sender.value) 54 | } 55 | 56 | @IBAction func pause(_ sender: UIButton) { 57 | boatWaveView.pause() 58 | 59 | } 60 | 61 | @IBAction func stop(_ sender: UIButton) { 62 | boatWaveView.stop() 63 | } 64 | @IBAction func start(_ sender: UIButton) { 65 | boatWaveView.start() 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CoreAnimationDemo/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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /CoreAnimationDemo/View/BoatWaveView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoatWaveView.swift 3 | // CoreAnimationDemo 4 | // 5 | // Created by Kaibo Lu on 2017/6/2. 6 | // Copyright © 2017年 Kaibo Lu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private let kBoatImageViewSize: CGSize = CGSize(width: 20, height: 20) 12 | private let PI_Circle: CGFloat = CGFloat.pi * 2 13 | 14 | class BoatWaveView: UIView { 15 | 16 | /** 17 | Wave horizontal move step. 18 | A positive value specifies right movement, a negative value specifies left movement. 19 | This value should NOT be 0. 20 | */ 21 | var horizontalStep: CGFloat = 0.05 { 22 | didSet { 23 | assert(horizontalStep != 0, "Horizontal step (\(horizontalStep)) must != 0") 24 | } 25 | } 26 | 27 | /** 28 | Number of wave cycle the view can display each time. 29 | This value should be greater than or equal to 0. 30 | */ 31 | var cycleCount: CGFloat = 0.5 { 32 | didSet { 33 | assert(cycleCount >= 0, "Cycle count (\(cycleCount)) must >= 0") 34 | } 35 | } 36 | 37 | /* 38 | _ <- H 39 | / \ 40 | \_/ <- L 41 | 42 | The height between a wave highest point and lowest point (H - L). 43 | This value should be greater than or equal to 0. 44 | */ 45 | var waveHeight: CGFloat = 0 { 46 | didSet { 47 | assert(waveHeight >= 0 && waveHeight <= maxWaveHeight, "Wave height (\(waveHeight)) must >= 0 and <= \(maxWaveHeight)") 48 | } 49 | } 50 | 51 | var maxWaveHeight: CGFloat { 52 | return bounds.height - kBoatImageViewSize.height - minWaterDepth 53 | } 54 | 55 | /** 56 | Target wave height to change. 57 | Change this value will make wave height change by step (see waveHeightStep property) until wave height is equal to this value. 58 | */ 59 | var targetWaveHeight: CGFloat = 50 { 60 | didSet { 61 | assert(targetWaveHeight >= 0 && targetWaveHeight <= maxWaveHeight, 62 | "Target wave height (\(targetWaveHeight)) must >= 0 and <= \(maxWaveHeight)") 63 | if targetWaveHeight > 0 { 64 | lastPositiveTargetWaveHeight = targetWaveHeight 65 | } 66 | } 67 | } 68 | 69 | /** 70 | Use this value to start animation if target wave height is 0. 71 | */ 72 | private var lastPositiveTargetWaveHeight: CGFloat = 50 73 | 74 | /** 75 | Step of wave height change each time 76 | */ 77 | var waveHeightStep: CGFloat = 0.2 { 78 | didSet { 79 | assert(waveHeightStep > 0, "Wave height step (\(waveHeightStep)) must > 0") 80 | } 81 | } 82 | 83 | /** 84 | A wave lowest point height. See waveHeight property. 85 | This value should be greater than 0. 86 | */ 87 | var minWaterDepth: CGFloat = 20 { 88 | didSet { 89 | assert(minWaterDepth > 0, "Min water depth (\(minWaterDepth)) must > 0") 90 | updateBoatFrame() 91 | updateWaveWhenNoAnimation() 92 | } 93 | } 94 | 95 | private var boatImageView: UIImageView! 96 | private var skyLayer: CALayer! 97 | private var waveLayer: CAShapeLayer! 98 | private var underWaveLayer: CALayer! 99 | private var currentPhase: CGFloat = 0 100 | private var waveLink: CADisplayLink? 101 | 102 | var isAnimating: Bool { 103 | return waveLink?.isPaused == false 104 | } 105 | 106 | override init(frame: CGRect) { 107 | super.init(frame: frame) 108 | setup() 109 | } 110 | 111 | required init?(coder aDecoder: NSCoder) { 112 | fatalError("init(coder:) has not been implemented") 113 | } 114 | 115 | deinit { 116 | NotificationCenter.default.removeObserver(self) 117 | waveLink?.invalidate() 118 | } 119 | 120 | private func setup() { 121 | NotificationCenter.default.addObserver(self, selector: #selector(stop), name: Notification.Name.UIApplicationWillResignActive, object: nil) 122 | NotificationCenter.default.addObserver(self, selector: #selector(start), name: Notification.Name.UIApplicationDidBecomeActive, object: nil) 123 | 124 | let sky = CAGradientLayer() 125 | sky.frame = bounds 126 | sky.colors = [UIColor(red: 0.663, green: 0.812, blue: 0.961, alpha: 1).cgColor, 127 | UIColor.white.cgColor] 128 | layer.addSublayer(sky) 129 | skyLayer = sky 130 | 131 | boatImageView = UIImageView(image: #imageLiteral(resourceName: "Boat")) 132 | updateBoatFrame() 133 | addSubview(boatImageView) 134 | 135 | let underWave = CAGradientLayer() 136 | underWave.frame = bounds 137 | underWave.colors = [UIColor(red: 0.471, green: 0.784, blue: 1.000, alpha: 1).cgColor, 138 | UIColor(red: 0.122, green: 0.365, blue: 0.788, alpha: 1).cgColor] 139 | layer.addSublayer(underWave) 140 | underWaveLayer = underWave 141 | 142 | waveLayer = CAShapeLayer() 143 | updateWaveWhenNoAnimation() 144 | } 145 | 146 | private func updateWaveWhenNoAnimation() { 147 | guard !isAnimating else { return } 148 | let path = UIBezierPath(rect: CGRect(x: 0, 149 | y: bounds.maxY - minWaterDepth - waveHeight / 2, 150 | width: bounds.width, 151 | height: minWaterDepth + waveHeight / 2)) 152 | waveLayer.path = path.cgPath 153 | underWaveLayer.mask = waveLayer 154 | } 155 | 156 | private func updateBoatFrame() { 157 | let transform = boatImageView.transform 158 | boatImageView.transform = .identity 159 | boatImageView.frame = CGRect(origin: CGPoint(x: bounds.midX - kBoatImageViewSize.width / 2, 160 | y: bounds.maxY - minWaterDepth - waveHeight / 2 - kBoatImageViewSize.height), 161 | size: kBoatImageViewSize) 162 | boatImageView.transform = transform 163 | } 164 | 165 | @objc private func waveLinkRefresh() { 166 | defer { 167 | var needToUpdate = false 168 | if waveHeight < targetWaveHeight { 169 | let temp = waveHeight + waveHeightStep 170 | if temp > targetWaveHeight { 171 | waveHeight = targetWaveHeight 172 | } else { 173 | waveHeight = temp 174 | } 175 | needToUpdate = true 176 | } else if waveHeight > targetWaveHeight { 177 | let temp = waveHeight - waveHeightStep 178 | if temp < targetWaveHeight { 179 | waveHeight = targetWaveHeight 180 | } else { 181 | waveHeight = temp 182 | } 183 | needToUpdate = true 184 | } 185 | if needToUpdate { 186 | updateBoatFrame() 187 | } 188 | } 189 | guard waveHeight > 0 else { 190 | if targetWaveHeight == 0 { 191 | waveLink?.isPaused = true 192 | } 193 | return 194 | } 195 | let totalWidth: CGFloat = bounds.width 196 | assert(totalWidth > 0 && waveHeight <= maxWaveHeight, 197 | "Total width (\(totalWidth)) must > 0 and wave height (\(waveHeight)) must <= \(maxWaveHeight)") 198 | 199 | func angleInRadians(at x: CGFloat) -> CGFloat { 200 | return x / totalWidth * (PI_Circle * cycleCount) 201 | } 202 | 203 | func point(at i: Int) -> CGPoint { 204 | let x = CGFloat(i) 205 | let angle = angleInRadians(at: x) 206 | return CGPoint(x: x, y: bounds.height - minWaterDepth - (sin(angle + currentPhase) + 1) * waveHeight / 2) 207 | } 208 | 209 | // Draw wave 210 | 211 | UIGraphicsBeginImageContext(CGSize(width: totalWidth, height: waveHeight)) 212 | 213 | let path = UIBezierPath() 214 | path.move(to: point(at: 0)) 215 | for i in 1...Int(totalWidth) { 216 | path.addLine(to: point(at: i)) 217 | } 218 | path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY)) 219 | path.addLine(to: CGPoint(x: 0, y: bounds.maxY)) 220 | path.close() 221 | path.fill() 222 | waveLayer.path = path.cgPath 223 | underWaveLayer.mask = waveLayer 224 | 225 | UIGraphicsEndImageContext() 226 | 227 | // Move boat 228 | 229 | let centerX = totalWidth / 2 230 | let bottomCenter = point(at: Int(centerX)) 231 | // Identity translated y 232 | let transform = CGAffineTransform(a: 1, b: 0, 233 | c: 0, d: 1, 234 | tx: 0, ty: bottomCenter.y - (bounds.maxY - minWaterDepth - waveHeight / 2)) 235 | let angle = angleInRadians(at: centerX) 236 | let tanValue = -waveHeight / 2 * cos(angle + currentPhase) * angleInRadians(at: 1) // Derivative of y 237 | boatImageView.transform = transform.rotated(by: atan(tanValue)) 238 | 239 | currentPhase += horizontalStep 240 | if currentPhase > PI_Circle { 241 | currentPhase -= PI_Circle 242 | } else if currentPhase < PI_Circle { 243 | currentPhase += PI_Circle 244 | } 245 | } 246 | 247 | override func didMoveToSuperview() { 248 | if superview == nil { 249 | waveLink?.invalidate() 250 | } else { 251 | waveLink = CADisplayLink(target: self, selector: #selector(waveLinkRefresh)) 252 | waveLink?.isPaused = true 253 | waveLink?.add(to: .current, forMode: .defaultRunLoopMode) 254 | } 255 | } 256 | 257 | /** 258 | Start animation. If wave height is 0, use last positive wave height. 259 | */ 260 | func start() { 261 | if waveHeight == 0, targetWaveHeight == 0 { 262 | targetWaveHeight = lastPositiveTargetWaveHeight 263 | } 264 | waveLink?.isPaused = false 265 | } 266 | 267 | /** 268 | Pause animation. 269 | */ 270 | func pause() { 271 | waveLink?.isPaused = true 272 | } 273 | 274 | /** 275 | Stop animation by reducing wave height step by step until it is 0. 276 | */ 277 | func stop() { 278 | targetWaveHeight = 0 279 | } 280 | 281 | } 282 | -------------------------------------------------------------------------------- /CoreAnimationDemo/View/PulsatorLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PulsatorLayer.swift 3 | // CoreAnimationDemo 4 | // 5 | // Created by Kaibo Lu on 2017/5/27. 6 | // Copyright © 2017年 Kaibo Lu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private let kPulseAnimationKey: String = "PulseAnimationKey" 12 | 13 | class PulsatorLayer: CAReplicatorLayer { 14 | 15 | enum Orientation { 16 | case out 17 | case `in` 18 | } 19 | 20 | var pulseOrientation: Orientation = .out { 21 | didSet { 22 | if pulseOrientation != oldValue { 23 | restartIfNeeded() 24 | } 25 | } 26 | } 27 | 28 | var maxRadius: CGFloat = 50 { 29 | didSet { 30 | assert(maxRadius > minRadius, "Max radius (\(maxRadius)) must > min radius (\(minRadius))") 31 | pulseLayer.bounds.size = CGSize(width: maxRadius * 2, height: maxRadius * 2) 32 | pulseLayer.cornerRadius = maxRadius 33 | } 34 | } 35 | 36 | var minRadius: CGFloat = 0 { 37 | didSet { 38 | assert(minRadius < maxRadius && minRadius >= 0, "Min radius (\(minRadius)) must < max radius (\(maxRadius)) and >= 0") 39 | } 40 | } 41 | 42 | var maxAlpha: CGFloat = 0.5 { 43 | didSet { 44 | assert(maxAlpha >= minAlpha && maxAlpha > 0 && maxAlpha <= 1, "Max alpha (\(maxAlpha)) must >= min alpha (\(minAlpha)), > 0 and <= 1") 45 | if maxAlpha != oldValue { 46 | restartIfNeeded() 47 | } 48 | } 49 | } 50 | 51 | var minAlpha: CGFloat = 0 { 52 | didSet { 53 | assert(minAlpha <= maxAlpha && minAlpha >= 0, "Min alpha (\(minAlpha)) must <= max alpha (\(maxAlpha)) and >= 0") 54 | if minAlpha != oldValue { 55 | restartIfNeeded() 56 | } 57 | } 58 | } 59 | 60 | // Pulse color displayed when radius is max 61 | var outColor: CGColor = UIColor.blue.cgColor { 62 | didSet { 63 | if outColor != oldValue { 64 | restartIfNeeded() 65 | } 66 | } 67 | } 68 | 69 | // Pulse color displayed when radius is min 70 | var inColor: CGColor = UIColor.red.cgColor { 71 | didSet { 72 | if inColor != oldValue { 73 | restartIfNeeded() 74 | } 75 | } 76 | } 77 | 78 | // Duration for one pulse animation 79 | var animationDuration: Double = 3 { 80 | didSet { 81 | if animationDuration != oldValue { 82 | restartIfNeeded() 83 | } 84 | } 85 | } 86 | 87 | // Time interval between repeated pulse animations 88 | var animationInterval: Double = 1 { 89 | didSet { 90 | if animationInterval != oldValue { 91 | restartIfNeeded() 92 | } 93 | } 94 | } 95 | 96 | // Number of pulse to display in one pulse animation duration 97 | var pulseCount: Int = 5 { 98 | didSet { 99 | if pulseCount != oldValue { 100 | instanceCount = pulseCount 101 | restartIfNeeded() 102 | } 103 | } 104 | } 105 | 106 | private var pulseLayer: CALayer! 107 | 108 | private var isAnimatingBeforeLeaving: Bool = false 109 | 110 | var isAnimating: Bool { 111 | return pulseLayer.animation(forKey: kPulseAnimationKey) != nil 112 | } 113 | 114 | override init() { 115 | super.init() 116 | 117 | instanceCount = pulseCount 118 | repeatCount = MAXFLOAT 119 | 120 | pulseLayer = CALayer() 121 | pulseLayer.opacity = 0 122 | pulseLayer.backgroundColor = outColor 123 | pulseLayer.contentsScale = UIScreen.main.scale 124 | pulseLayer.bounds.size = CGSize(width: maxRadius * 2, height: maxRadius * 2) 125 | pulseLayer.cornerRadius = maxRadius 126 | addSublayer(pulseLayer) 127 | 128 | NotificationCenter.default.addObserver(self, selector: #selector(save), name: Notification.Name.UIApplicationWillResignActive, object: nil) 129 | NotificationCenter.default.addObserver(self, selector: #selector(resume), name: Notification.Name.UIApplicationDidBecomeActive, object: nil) 130 | } 131 | 132 | override init(layer: Any) { 133 | super.init(layer: layer) 134 | } 135 | 136 | required init?(coder aDecoder: NSCoder) { 137 | fatalError("init(coder:) has not been implemented") 138 | } 139 | 140 | deinit { 141 | NotificationCenter.default.removeObserver(self) 142 | } 143 | 144 | @objc private func save() { 145 | isAnimatingBeforeLeaving = isAnimating 146 | } 147 | 148 | @objc private func resume() { 149 | if isAnimatingBeforeLeaving { 150 | start() 151 | } 152 | } 153 | 154 | func restartIfNeeded() { 155 | if isAnimating { 156 | start() 157 | } 158 | } 159 | 160 | func start() { 161 | stop() 162 | 163 | instanceDelay = (animationDuration + animationInterval) / Double(pulseCount) 164 | 165 | let scaleAnimation = CABasicAnimation(keyPath: "transform.scale.xy") 166 | scaleAnimation.duration = animationDuration 167 | 168 | let opacityAnimation = CABasicAnimation(keyPath: "opacity") 169 | opacityAnimation.duration = animationDuration 170 | 171 | let colorAnimation = CABasicAnimation(keyPath: "backgroundColor") 172 | colorAnimation.duration = animationDuration 173 | 174 | switch pulseOrientation { 175 | case .out: 176 | scaleAnimation.fromValue = minRadius / maxRadius 177 | scaleAnimation.toValue = 1 178 | 179 | opacityAnimation.fromValue = maxAlpha 180 | opacityAnimation.toValue = minAlpha 181 | 182 | colorAnimation.fromValue = inColor 183 | colorAnimation.toValue = outColor 184 | 185 | case .in: 186 | scaleAnimation.fromValue = 1 187 | scaleAnimation.toValue = minRadius / maxRadius 188 | 189 | opacityAnimation.fromValue = minAlpha 190 | opacityAnimation.toValue = maxAlpha 191 | 192 | colorAnimation.fromValue = outColor 193 | colorAnimation.toValue = inColor 194 | } 195 | 196 | let animationGroup = CAAnimationGroup() 197 | animationGroup.duration = animationDuration + animationInterval 198 | animationGroup.animations = [scaleAnimation, opacityAnimation, colorAnimation] 199 | animationGroup.repeatCount = repeatCount 200 | pulseLayer.add(animationGroup, forKey: kPulseAnimationKey) 201 | } 202 | 203 | func stop() { 204 | pulseLayer.removeAnimation(forKey: kPulseAnimationKey) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoreAnimationDemo 2 | 3 | iOS core animation demo. 4 | 5 | ## Pulsator 6 | 7 | ![](README_resources/Pulsator.gif) 8 | 9 | Implimented with CAReplicatorLayer and CABasicAnimation. 10 | 11 | Blog: http://www.cnblogs.com/silence-cnblogs/p/6951948.html 12 | 13 | ## Emitter 14 | 15 | ![](README_resources/Emitter.gif) 16 | 17 | Implemented with CAEmitterLayer. 18 | 19 | Blog: http://www.cnblogs.com/silence-cnblogs/p/6971533.html 20 | 21 | ## Wave 22 | 23 | ![](README_resources/Wave.gif) 24 | 25 | Implemented with CAShapeLayer, CAGradientLayer and CADisplayLink. 26 | 27 | Blog: http://www.cnblogs.com/silence-cnblogs/p/6979418.html -------------------------------------------------------------------------------- /README_resources/Emitter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/README_resources/Emitter.gif -------------------------------------------------------------------------------- /README_resources/Pulsator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/README_resources/Pulsator.gif -------------------------------------------------------------------------------- /README_resources/Wave.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silence-GitHub/CoreAnimationDemo/2c311cf142912b82c8a8f847a0489fa1aec6f5ed/README_resources/Wave.gif --------------------------------------------------------------------------------