├── .gitignore ├── KeyframeViewAnimations.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── KeyframeViewAnimations ├── AppDelegate.h ├── AppDelegate.m ├── Base.lproj │ └── Main.storyboard ├── CALayer+setUIColor.h ├── CALayer+setUIColor.m ├── Constants.h ├── DebugLayer │ ├── DebugImageView.h │ ├── DebugImageView.m │ ├── DebugLayer.h │ └── DebugLayer.m ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── wareto icon iPad.png │ │ └── wareto icon iPad@2x.png │ ├── LaunchImage.launchimage │ │ └── Contents.json │ ├── WareTo icon w center dot.imageset │ │ ├── Contents.json │ │ ├── Wareto icon w center dot 76x100.png │ │ └── Wareto icon w center dot 76x100@2x.png │ └── WareTo icon.imageset │ │ ├── Contents.json │ │ ├── Wareto icon 76x100.png │ │ └── Wareto icon 76x100@2x.png ├── KeyframeViewAnimations-Info.plist ├── KeyframeViewAnimations-Prefix.pch ├── UIBezierPath-Points.h ├── UIBezierPath-Points.m ├── UIBezierPath-Smoothing.h ├── UIBezierPath-Smoothing.m ├── ViewController.h ├── ViewController.m ├── en.lproj │ └── InfoPlist.strings └── main.m ├── KeyframeViewAnimationsTests ├── KeyframeViewAnimationsTests-Info.plist ├── KeyframeViewAnimationsTests.m └── en.lproj │ └── InfoPlist.strings ├── README.md └── Sleuthing UIView animations.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 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 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | *.xccheckout 19 | 20 | #CocoaPods 21 | Pods 22 | -------------------------------------------------------------------------------- /KeyframeViewAnimations.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AE07F25E18BCE5220007350A /* DebugLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = AE07F25D18BCE5220007350A /* DebugLayer.m */; }; 11 | AE07F26118BCE54D0007350A /* DebugImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = AE07F26018BCE54D0007350A /* DebugImageView.m */; }; 12 | AE12142718B6DE1D00AA9441 /* CALayer+setUIColor.m in Sources */ = {isa = PBXBuildFile; fileRef = AE12142618B6DE1D00AA9441 /* CALayer+setUIColor.m */; }; 13 | AE581CBE18A1477E003EA317 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE581CBD18A1477E003EA317 /* Foundation.framework */; }; 14 | AE581CC018A1477E003EA317 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE581CBF18A1477E003EA317 /* CoreGraphics.framework */; }; 15 | AE581CC218A1477E003EA317 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE581CC118A1477E003EA317 /* UIKit.framework */; }; 16 | AE581CC818A1477E003EA317 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = AE581CC618A1477E003EA317 /* InfoPlist.strings */; }; 17 | AE581CCA18A1477E003EA317 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AE581CC918A1477E003EA317 /* main.m */; }; 18 | AE581CCE18A1477E003EA317 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AE581CCD18A1477E003EA317 /* AppDelegate.m */; }; 19 | AE581CD118A1477E003EA317 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AE581CCF18A1477E003EA317 /* Main.storyboard */; }; 20 | AE581CD418A1477E003EA317 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE581CD318A1477E003EA317 /* ViewController.m */; }; 21 | AE581CD618A1477E003EA317 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE581CD518A1477E003EA317 /* Images.xcassets */; }; 22 | AE581CDD18A1477E003EA317 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE581CDC18A1477E003EA317 /* XCTest.framework */; }; 23 | AE581CDE18A1477E003EA317 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE581CBD18A1477E003EA317 /* Foundation.framework */; }; 24 | AE581CDF18A1477E003EA317 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE581CC118A1477E003EA317 /* UIKit.framework */; }; 25 | AE581CE718A1477E003EA317 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = AE581CE518A1477E003EA317 /* InfoPlist.strings */; }; 26 | AE581CE918A1477E003EA317 /* KeyframeViewAnimationsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AE581CE818A1477E003EA317 /* KeyframeViewAnimationsTests.m */; }; 27 | AEE4928E18C6235500D43663 /* UIBezierPath-Points.m in Sources */ = {isa = PBXBuildFile; fileRef = AEE4928B18C6235500D43663 /* UIBezierPath-Points.m */; }; 28 | AEE4928F18C6235500D43663 /* UIBezierPath-Smoothing.m in Sources */ = {isa = PBXBuildFile; fileRef = AEE4928D18C6235500D43663 /* UIBezierPath-Smoothing.m */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXContainerItemProxy section */ 32 | AE581CE018A1477E003EA317 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = AE581CB218A1477E003EA317 /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = AE581CB918A1477E003EA317; 37 | remoteInfo = KeyframeViewAnimations; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | AE07F25C18BCE5220007350A /* DebugLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DebugLayer.h; path = DebugLayer/DebugLayer.h; sourceTree = ""; }; 43 | AE07F25D18BCE5220007350A /* DebugLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DebugLayer.m; path = DebugLayer/DebugLayer.m; sourceTree = ""; }; 44 | AE07F25F18BCE54D0007350A /* DebugImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DebugImageView.h; path = DebugLayer/DebugImageView.h; sourceTree = ""; }; 45 | AE07F26018BCE54D0007350A /* DebugImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DebugImageView.m; path = DebugLayer/DebugImageView.m; sourceTree = ""; }; 46 | AE07F26218BCEBEB0007350A /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; 47 | AE12142518B6DE1D00AA9441 /* CALayer+setUIColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CALayer+setUIColor.h"; sourceTree = ""; }; 48 | AE12142618B6DE1D00AA9441 /* CALayer+setUIColor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CALayer+setUIColor.m"; sourceTree = ""; }; 49 | AE581CBA18A1477E003EA317 /* KeyframeViewAnimations.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeyframeViewAnimations.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | AE581CBD18A1477E003EA317 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 51 | AE581CBF18A1477E003EA317 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 52 | AE581CC118A1477E003EA317 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 53 | AE581CC518A1477E003EA317 /* KeyframeViewAnimations-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "KeyframeViewAnimations-Info.plist"; sourceTree = ""; }; 54 | AE581CC718A1477E003EA317 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 55 | AE581CC918A1477E003EA317 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 56 | AE581CCB18A1477E003EA317 /* KeyframeViewAnimations-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "KeyframeViewAnimations-Prefix.pch"; sourceTree = ""; }; 57 | AE581CCC18A1477E003EA317 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 58 | AE581CCD18A1477E003EA317 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 59 | AE581CD018A1477E003EA317 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 60 | AE581CD218A1477E003EA317 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 61 | AE581CD318A1477E003EA317 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 62 | AE581CD518A1477E003EA317 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 63 | AE581CDB18A1477E003EA317 /* KeyframeViewAnimationsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KeyframeViewAnimationsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | AE581CDC18A1477E003EA317 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 65 | AE581CE418A1477E003EA317 /* KeyframeViewAnimationsTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "KeyframeViewAnimationsTests-Info.plist"; sourceTree = ""; }; 66 | AE581CE618A1477E003EA317 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 67 | AE581CE818A1477E003EA317 /* KeyframeViewAnimationsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KeyframeViewAnimationsTests.m; sourceTree = ""; }; 68 | AE5A498218D8C42E008F1B9E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.md; sourceTree = SOURCE_ROOT; }; 69 | AE5A498318D8C42E008F1B9E /* Sleuthing UIView animations.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Sleuthing UIView animations.md"; sourceTree = SOURCE_ROOT; }; 70 | AEE4928A18C6235500D43663 /* UIBezierPath-Points.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIBezierPath-Points.h"; sourceTree = ""; }; 71 | AEE4928B18C6235500D43663 /* UIBezierPath-Points.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIBezierPath-Points.m"; sourceTree = ""; }; 72 | AEE4928C18C6235500D43663 /* UIBezierPath-Smoothing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIBezierPath-Smoothing.h"; sourceTree = ""; }; 73 | AEE4928D18C6235500D43663 /* UIBezierPath-Smoothing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIBezierPath-Smoothing.m"; sourceTree = ""; }; 74 | /* End PBXFileReference section */ 75 | 76 | /* Begin PBXFrameworksBuildPhase section */ 77 | AE581CB718A1477E003EA317 /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 2147483647; 80 | files = ( 81 | AE581CC018A1477E003EA317 /* CoreGraphics.framework in Frameworks */, 82 | AE581CC218A1477E003EA317 /* UIKit.framework in Frameworks */, 83 | AE581CBE18A1477E003EA317 /* Foundation.framework in Frameworks */, 84 | ); 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | AE581CD818A1477E003EA317 /* Frameworks */ = { 88 | isa = PBXFrameworksBuildPhase; 89 | buildActionMask = 2147483647; 90 | files = ( 91 | AE581CDD18A1477E003EA317 /* XCTest.framework in Frameworks */, 92 | AE581CDF18A1477E003EA317 /* UIKit.framework in Frameworks */, 93 | AE581CDE18A1477E003EA317 /* Foundation.framework in Frameworks */, 94 | ); 95 | runOnlyForDeploymentPostprocessing = 0; 96 | }; 97 | /* End PBXFrameworksBuildPhase section */ 98 | 99 | /* Begin PBXGroup section */ 100 | AE07F25B18BCE4FC0007350A /* DebugLayer */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | AE07F25F18BCE54D0007350A /* DebugImageView.h */, 104 | AE07F26018BCE54D0007350A /* DebugImageView.m */, 105 | AE07F25C18BCE5220007350A /* DebugLayer.h */, 106 | AE07F25D18BCE5220007350A /* DebugLayer.m */, 107 | ); 108 | name = DebugLayer; 109 | sourceTree = ""; 110 | }; 111 | AE581CB118A1477E003EA317 = { 112 | isa = PBXGroup; 113 | children = ( 114 | AE581CC318A1477E003EA317 /* KeyframeViewAnimations */, 115 | AE581CE218A1477E003EA317 /* KeyframeViewAnimationsTests */, 116 | AE581CBC18A1477E003EA317 /* Frameworks */, 117 | AE581CBB18A1477E003EA317 /* Products */, 118 | ); 119 | sourceTree = ""; 120 | }; 121 | AE581CBB18A1477E003EA317 /* Products */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | AE581CBA18A1477E003EA317 /* KeyframeViewAnimations.app */, 125 | AE581CDB18A1477E003EA317 /* KeyframeViewAnimationsTests.xctest */, 126 | ); 127 | name = Products; 128 | sourceTree = ""; 129 | }; 130 | AE581CBC18A1477E003EA317 /* Frameworks */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | AE581CBD18A1477E003EA317 /* Foundation.framework */, 134 | AE581CBF18A1477E003EA317 /* CoreGraphics.framework */, 135 | AE581CC118A1477E003EA317 /* UIKit.framework */, 136 | AE581CDC18A1477E003EA317 /* XCTest.framework */, 137 | ); 138 | name = Frameworks; 139 | sourceTree = ""; 140 | }; 141 | AE581CC318A1477E003EA317 /* KeyframeViewAnimations */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | AE07F26218BCEBEB0007350A /* Constants.h */, 145 | AE07F25B18BCE4FC0007350A /* DebugLayer */, 146 | AE581CCC18A1477E003EA317 /* AppDelegate.h */, 147 | AE581CCD18A1477E003EA317 /* AppDelegate.m */, 148 | AE581CCF18A1477E003EA317 /* Main.storyboard */, 149 | AE12142518B6DE1D00AA9441 /* CALayer+setUIColor.h */, 150 | AE12142618B6DE1D00AA9441 /* CALayer+setUIColor.m */, 151 | AEE4928A18C6235500D43663 /* UIBezierPath-Points.h */, 152 | AEE4928B18C6235500D43663 /* UIBezierPath-Points.m */, 153 | AEE4928C18C6235500D43663 /* UIBezierPath-Smoothing.h */, 154 | AEE4928D18C6235500D43663 /* UIBezierPath-Smoothing.m */, 155 | AE581CD218A1477E003EA317 /* ViewController.h */, 156 | AE581CD318A1477E003EA317 /* ViewController.m */, 157 | AE581CD518A1477E003EA317 /* Images.xcassets */, 158 | AE581CC418A1477E003EA317 /* Supporting Files */, 159 | ); 160 | path = KeyframeViewAnimations; 161 | sourceTree = ""; 162 | }; 163 | AE581CC418A1477E003EA317 /* Supporting Files */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | AE5A498118D8C3CD008F1B9E /* Documentation */, 167 | AE581CC518A1477E003EA317 /* KeyframeViewAnimations-Info.plist */, 168 | AE581CC618A1477E003EA317 /* InfoPlist.strings */, 169 | AE581CC918A1477E003EA317 /* main.m */, 170 | AE581CCB18A1477E003EA317 /* KeyframeViewAnimations-Prefix.pch */, 171 | ); 172 | name = "Supporting Files"; 173 | sourceTree = ""; 174 | }; 175 | AE581CE218A1477E003EA317 /* KeyframeViewAnimationsTests */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | AE581CE818A1477E003EA317 /* KeyframeViewAnimationsTests.m */, 179 | AE581CE318A1477E003EA317 /* Supporting Files */, 180 | ); 181 | path = KeyframeViewAnimationsTests; 182 | sourceTree = ""; 183 | }; 184 | AE581CE318A1477E003EA317 /* Supporting Files */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | AE581CE418A1477E003EA317 /* KeyframeViewAnimationsTests-Info.plist */, 188 | AE581CE518A1477E003EA317 /* InfoPlist.strings */, 189 | ); 190 | name = "Supporting Files"; 191 | sourceTree = ""; 192 | }; 193 | AE5A498118D8C3CD008F1B9E /* Documentation */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | AE5A498218D8C42E008F1B9E /* README.md */, 197 | AE5A498318D8C42E008F1B9E /* Sleuthing UIView animations.md */, 198 | ); 199 | name = Documentation; 200 | sourceTree = ""; 201 | }; 202 | /* End PBXGroup section */ 203 | 204 | /* Begin PBXNativeTarget section */ 205 | AE581CB918A1477E003EA317 /* KeyframeViewAnimations */ = { 206 | isa = PBXNativeTarget; 207 | buildConfigurationList = AE581CEC18A1477E003EA317 /* Build configuration list for PBXNativeTarget "KeyframeViewAnimations" */; 208 | buildPhases = ( 209 | AE581CB618A1477E003EA317 /* Sources */, 210 | AE581CB718A1477E003EA317 /* Frameworks */, 211 | AE581CB818A1477E003EA317 /* Resources */, 212 | ); 213 | buildRules = ( 214 | ); 215 | dependencies = ( 216 | ); 217 | name = KeyframeViewAnimations; 218 | productName = KeyframeViewAnimations; 219 | productReference = AE581CBA18A1477E003EA317 /* KeyframeViewAnimations.app */; 220 | productType = "com.apple.product-type.application"; 221 | }; 222 | AE581CDA18A1477E003EA317 /* KeyframeViewAnimationsTests */ = { 223 | isa = PBXNativeTarget; 224 | buildConfigurationList = AE581CEF18A1477E003EA317 /* Build configuration list for PBXNativeTarget "KeyframeViewAnimationsTests" */; 225 | buildPhases = ( 226 | AE581CD718A1477E003EA317 /* Sources */, 227 | AE581CD818A1477E003EA317 /* Frameworks */, 228 | AE581CD918A1477E003EA317 /* Resources */, 229 | ); 230 | buildRules = ( 231 | ); 232 | dependencies = ( 233 | AE581CE118A1477E003EA317 /* PBXTargetDependency */, 234 | ); 235 | name = KeyframeViewAnimationsTests; 236 | productName = KeyframeViewAnimationsTests; 237 | productReference = AE581CDB18A1477E003EA317 /* KeyframeViewAnimationsTests.xctest */; 238 | productType = "com.apple.product-type.bundle.unit-test"; 239 | }; 240 | /* End PBXNativeTarget section */ 241 | 242 | /* Begin PBXProject section */ 243 | AE581CB218A1477E003EA317 /* Project object */ = { 244 | isa = PBXProject; 245 | attributes = { 246 | LastUpgradeCheck = 0500; 247 | ORGANIZATIONNAME = WareTo; 248 | TargetAttributes = { 249 | AE581CDA18A1477E003EA317 = { 250 | TestTargetID = AE581CB918A1477E003EA317; 251 | }; 252 | }; 253 | }; 254 | buildConfigurationList = AE581CB518A1477E003EA317 /* Build configuration list for PBXProject "KeyframeViewAnimations" */; 255 | compatibilityVersion = "Xcode 3.2"; 256 | developmentRegion = English; 257 | hasScannedForEncodings = 0; 258 | knownRegions = ( 259 | en, 260 | Base, 261 | ); 262 | mainGroup = AE581CB118A1477E003EA317; 263 | productRefGroup = AE581CBB18A1477E003EA317 /* Products */; 264 | projectDirPath = ""; 265 | projectRoot = ""; 266 | targets = ( 267 | AE581CB918A1477E003EA317 /* KeyframeViewAnimations */, 268 | AE581CDA18A1477E003EA317 /* KeyframeViewAnimationsTests */, 269 | ); 270 | }; 271 | /* End PBXProject section */ 272 | 273 | /* Begin PBXResourcesBuildPhase section */ 274 | AE581CB818A1477E003EA317 /* Resources */ = { 275 | isa = PBXResourcesBuildPhase; 276 | buildActionMask = 2147483647; 277 | files = ( 278 | AE581CD618A1477E003EA317 /* Images.xcassets in Resources */, 279 | AE581CC818A1477E003EA317 /* InfoPlist.strings in Resources */, 280 | AE581CD118A1477E003EA317 /* Main.storyboard in Resources */, 281 | ); 282 | runOnlyForDeploymentPostprocessing = 0; 283 | }; 284 | AE581CD918A1477E003EA317 /* Resources */ = { 285 | isa = PBXResourcesBuildPhase; 286 | buildActionMask = 2147483647; 287 | files = ( 288 | AE581CE718A1477E003EA317 /* InfoPlist.strings in Resources */, 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | }; 292 | /* End PBXResourcesBuildPhase section */ 293 | 294 | /* Begin PBXSourcesBuildPhase section */ 295 | AE581CB618A1477E003EA317 /* Sources */ = { 296 | isa = PBXSourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | AE07F26118BCE54D0007350A /* DebugImageView.m in Sources */, 300 | AEE4928E18C6235500D43663 /* UIBezierPath-Points.m in Sources */, 301 | AE12142718B6DE1D00AA9441 /* CALayer+setUIColor.m in Sources */, 302 | AE581CD418A1477E003EA317 /* ViewController.m in Sources */, 303 | AEE4928F18C6235500D43663 /* UIBezierPath-Smoothing.m in Sources */, 304 | AE581CCE18A1477E003EA317 /* AppDelegate.m in Sources */, 305 | AE581CCA18A1477E003EA317 /* main.m in Sources */, 306 | AE07F25E18BCE5220007350A /* DebugLayer.m in Sources */, 307 | ); 308 | runOnlyForDeploymentPostprocessing = 0; 309 | }; 310 | AE581CD718A1477E003EA317 /* Sources */ = { 311 | isa = PBXSourcesBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | AE581CE918A1477E003EA317 /* KeyframeViewAnimationsTests.m in Sources */, 315 | ); 316 | runOnlyForDeploymentPostprocessing = 0; 317 | }; 318 | /* End PBXSourcesBuildPhase section */ 319 | 320 | /* Begin PBXTargetDependency section */ 321 | AE581CE118A1477E003EA317 /* PBXTargetDependency */ = { 322 | isa = PBXTargetDependency; 323 | target = AE581CB918A1477E003EA317 /* KeyframeViewAnimations */; 324 | targetProxy = AE581CE018A1477E003EA317 /* PBXContainerItemProxy */; 325 | }; 326 | /* End PBXTargetDependency section */ 327 | 328 | /* Begin PBXVariantGroup section */ 329 | AE581CC618A1477E003EA317 /* InfoPlist.strings */ = { 330 | isa = PBXVariantGroup; 331 | children = ( 332 | AE581CC718A1477E003EA317 /* en */, 333 | ); 334 | name = InfoPlist.strings; 335 | sourceTree = ""; 336 | }; 337 | AE581CCF18A1477E003EA317 /* Main.storyboard */ = { 338 | isa = PBXVariantGroup; 339 | children = ( 340 | AE581CD018A1477E003EA317 /* Base */, 341 | ); 342 | name = Main.storyboard; 343 | sourceTree = ""; 344 | }; 345 | AE581CE518A1477E003EA317 /* InfoPlist.strings */ = { 346 | isa = PBXVariantGroup; 347 | children = ( 348 | AE581CE618A1477E003EA317 /* en */, 349 | ); 350 | name = InfoPlist.strings; 351 | sourceTree = ""; 352 | }; 353 | /* End PBXVariantGroup section */ 354 | 355 | /* Begin XCBuildConfiguration section */ 356 | AE581CEA18A1477E003EA317 /* Debug */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | ALWAYS_SEARCH_USER_PATHS = NO; 360 | ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; 361 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 362 | CLANG_CXX_LIBRARY = "libc++"; 363 | CLANG_ENABLE_MODULES = YES; 364 | CLANG_ENABLE_OBJC_ARC = YES; 365 | CLANG_WARN_BOOL_CONVERSION = YES; 366 | CLANG_WARN_CONSTANT_CONVERSION = YES; 367 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 368 | CLANG_WARN_EMPTY_BODY = YES; 369 | CLANG_WARN_ENUM_CONVERSION = YES; 370 | CLANG_WARN_INT_CONVERSION = YES; 371 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 373 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 374 | COPY_PHASE_STRIP = NO; 375 | GCC_C_LANGUAGE_STANDARD = gnu99; 376 | GCC_DYNAMIC_NO_PIC = NO; 377 | GCC_OPTIMIZATION_LEVEL = 0; 378 | GCC_PREPROCESSOR_DEFINITIONS = ( 379 | "DEBUG=1", 380 | "$(inherited)", 381 | ); 382 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 383 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 384 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 385 | GCC_WARN_UNDECLARED_SELECTOR = YES; 386 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 387 | GCC_WARN_UNUSED_FUNCTION = YES; 388 | GCC_WARN_UNUSED_VARIABLE = YES; 389 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 390 | ONLY_ACTIVE_ARCH = YES; 391 | SDKROOT = iphoneos; 392 | TARGETED_DEVICE_FAMILY = 2; 393 | }; 394 | name = Debug; 395 | }; 396 | AE581CEB18A1477E003EA317 /* Release */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | ALWAYS_SEARCH_USER_PATHS = NO; 400 | ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; 401 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 402 | CLANG_CXX_LIBRARY = "libc++"; 403 | CLANG_ENABLE_MODULES = YES; 404 | CLANG_ENABLE_OBJC_ARC = YES; 405 | CLANG_WARN_BOOL_CONVERSION = YES; 406 | CLANG_WARN_CONSTANT_CONVERSION = YES; 407 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 408 | CLANG_WARN_EMPTY_BODY = YES; 409 | CLANG_WARN_ENUM_CONVERSION = YES; 410 | CLANG_WARN_INT_CONVERSION = YES; 411 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 412 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 413 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 414 | COPY_PHASE_STRIP = YES; 415 | ENABLE_NS_ASSERTIONS = NO; 416 | GCC_C_LANGUAGE_STANDARD = gnu99; 417 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 418 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 419 | GCC_WARN_UNDECLARED_SELECTOR = YES; 420 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 421 | GCC_WARN_UNUSED_FUNCTION = YES; 422 | GCC_WARN_UNUSED_VARIABLE = YES; 423 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 424 | SDKROOT = iphoneos; 425 | TARGETED_DEVICE_FAMILY = 2; 426 | VALIDATE_PRODUCT = YES; 427 | }; 428 | name = Release; 429 | }; 430 | AE581CED18A1477E003EA317 /* Debug */ = { 431 | isa = XCBuildConfiguration; 432 | buildSettings = { 433 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 434 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 435 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 436 | GCC_PREFIX_HEADER = "KeyframeViewAnimations/KeyframeViewAnimations-Prefix.pch"; 437 | INFOPLIST_FILE = "KeyframeViewAnimations/KeyframeViewAnimations-Info.plist"; 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | WRAPPER_EXTENSION = app; 440 | }; 441 | name = Debug; 442 | }; 443 | AE581CEE18A1477E003EA317 /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 447 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 448 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 449 | GCC_PREFIX_HEADER = "KeyframeViewAnimations/KeyframeViewAnimations-Prefix.pch"; 450 | INFOPLIST_FILE = "KeyframeViewAnimations/KeyframeViewAnimations-Info.plist"; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | WRAPPER_EXTENSION = app; 453 | }; 454 | name = Release; 455 | }; 456 | AE581CF018A1477E003EA317 /* Debug */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; 460 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/KeyframeViewAnimations.app/KeyframeViewAnimations"; 461 | FRAMEWORK_SEARCH_PATHS = ( 462 | "$(SDKROOT)/Developer/Library/Frameworks", 463 | "$(inherited)", 464 | "$(DEVELOPER_FRAMEWORKS_DIR)", 465 | ); 466 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 467 | GCC_PREFIX_HEADER = "KeyframeViewAnimations/KeyframeViewAnimations-Prefix.pch"; 468 | GCC_PREPROCESSOR_DEFINITIONS = ( 469 | "DEBUG=1", 470 | "$(inherited)", 471 | ); 472 | INFOPLIST_FILE = "KeyframeViewAnimationsTests/KeyframeViewAnimationsTests-Info.plist"; 473 | PRODUCT_NAME = "$(TARGET_NAME)"; 474 | TEST_HOST = "$(BUNDLE_LOADER)"; 475 | WRAPPER_EXTENSION = xctest; 476 | }; 477 | name = Debug; 478 | }; 479 | AE581CF118A1477E003EA317 /* Release */ = { 480 | isa = XCBuildConfiguration; 481 | buildSettings = { 482 | ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; 483 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/KeyframeViewAnimations.app/KeyframeViewAnimations"; 484 | FRAMEWORK_SEARCH_PATHS = ( 485 | "$(SDKROOT)/Developer/Library/Frameworks", 486 | "$(inherited)", 487 | "$(DEVELOPER_FRAMEWORKS_DIR)", 488 | ); 489 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 490 | GCC_PREFIX_HEADER = "KeyframeViewAnimations/KeyframeViewAnimations-Prefix.pch"; 491 | INFOPLIST_FILE = "KeyframeViewAnimationsTests/KeyframeViewAnimationsTests-Info.plist"; 492 | PRODUCT_NAME = "$(TARGET_NAME)"; 493 | TEST_HOST = "$(BUNDLE_LOADER)"; 494 | WRAPPER_EXTENSION = xctest; 495 | }; 496 | name = Release; 497 | }; 498 | /* End XCBuildConfiguration section */ 499 | 500 | /* Begin XCConfigurationList section */ 501 | AE581CB518A1477E003EA317 /* Build configuration list for PBXProject "KeyframeViewAnimations" */ = { 502 | isa = XCConfigurationList; 503 | buildConfigurations = ( 504 | AE581CEA18A1477E003EA317 /* Debug */, 505 | AE581CEB18A1477E003EA317 /* Release */, 506 | ); 507 | defaultConfigurationIsVisible = 0; 508 | defaultConfigurationName = Release; 509 | }; 510 | AE581CEC18A1477E003EA317 /* Build configuration list for PBXNativeTarget "KeyframeViewAnimations" */ = { 511 | isa = XCConfigurationList; 512 | buildConfigurations = ( 513 | AE581CED18A1477E003EA317 /* Debug */, 514 | AE581CEE18A1477E003EA317 /* Release */, 515 | ); 516 | defaultConfigurationIsVisible = 0; 517 | defaultConfigurationName = Release; 518 | }; 519 | AE581CEF18A1477E003EA317 /* Build configuration list for PBXNativeTarget "KeyframeViewAnimationsTests" */ = { 520 | isa = XCConfigurationList; 521 | buildConfigurations = ( 522 | AE581CF018A1477E003EA317 /* Debug */, 523 | AE581CF118A1477E003EA317 /* Release */, 524 | ); 525 | defaultConfigurationIsVisible = 0; 526 | defaultConfigurationName = Release; 527 | }; 528 | /* End XCConfigurationList section */ 529 | }; 530 | rootObject = AE581CB218A1477E003EA317 /* Project object */; 531 | } 532 | -------------------------------------------------------------------------------- /KeyframeViewAnimations.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/4/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/4/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @implementation AppDelegate 12 | 13 | 14 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 15 | { 16 | // Override point for customization after application launch. 17 | return YES; 18 | } 19 | 20 | - (void)applicationWillResignActive:(UIApplication *)application 21 | { 22 | // 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. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | - (void)applicationDidEnterBackground:(UIApplication *)application 27 | { 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 | - (void)applicationWillEnterForeground:(UIApplication *)application 33 | { 34 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 35 | } 36 | 37 | - (void)applicationDidBecomeActive:(UIApplication *)application 38 | { 39 | // 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. 40 | } 41 | 42 | - (void)applicationWillTerminate:(UIApplication *)application 43 | { 44 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/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 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 62 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 89 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/CALayer+setUIColor.h: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+setUIColor.h 3 | // 4 | // Created by Duncan Champney on 12/6/12. 5 | // Copyright (c) 2013 WareTo. All rights reserved. 6 | // 7 | 8 | /* 9 | This category adds new set-only property borderUIColor to CALayer. 10 | With this category in your project, you can set the border color on a layer 11 | from the "User Defined Runtime Attributes" settings in the identity inspector. 12 | You would add a key "layer.borderUIColor" 13 | and a value of type Color (which is a UIColor in the iOS SDK) 14 | The setBorderUIColor method sets the layer's borderColor with the result of converting the 15 | UIColor to a CGColor 16 | */ 17 | 18 | #import 19 | 20 | @interface CALayer (setUIColor) 21 | 22 | - (void) setBorderUIColor: (UIColor *) newBorderColor; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/CALayer+setUIColor.m: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+setUIColor.m 3 | // 4 | // Created by Duncan Champney on 12/6/12. 5 | // 6 | // 7 | 8 | #import "CALayer+setUIColor.h" 9 | 10 | @implementation CALayer (setUIColor) 11 | 12 | - (void) setBorderUIColor: (UIColor *) newBorderColor; 13 | { 14 | self.borderColor = [newBorderColor CGColor]; 15 | } 16 | @end 17 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/Constants.h: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.h 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/25/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #ifndef KeyframeViewAnimations_Constants_h 10 | #define KeyframeViewAnimations_Constants_h 11 | 12 | #define K_USE_CUBIC_PACING 1 13 | #define K_ROTATE 1 14 | #define K_KEYFRAME_STEPS 6 15 | #define K_FIX_ANIMATION 1 16 | #define K_LOG_KEYFRAME_STEPS 0 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/DebugLayer/DebugImageView.h: -------------------------------------------------------------------------------- 1 | // 2 | // DebugImageView.h 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/25/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DebugImageView : UIImageView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/DebugLayer/DebugImageView.m: -------------------------------------------------------------------------------- 1 | // 2 | // DebugImageView.m 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/25/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import "DebugImageView.h" 10 | #import "DebugLayer.h" 11 | 12 | 13 | @implementation DebugImageView 14 | 15 | + (Class)layerClass 16 | { 17 | return [DebugLayer class]; 18 | } 19 | 20 | - (id)initWithFrame:(CGRect)frame 21 | { 22 | self = [super initWithFrame:frame]; 23 | if (self) { 24 | // Initialization code 25 | } 26 | return self; 27 | } 28 | 29 | /* 30 | // Only override drawRect: if you perform custom drawing. 31 | // An empty implementation adversely affects performance during animation. 32 | - (void)drawRect:(CGRect)rect 33 | { 34 | // Drawing code 35 | } 36 | */ 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/DebugLayer/DebugLayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // DebugLayer.h 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/25/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DebugLayer : CALayer 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/DebugLayer/DebugLayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // DebugLayer.m 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/25/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import "DebugLayer.h" 10 | #import "Constants.h" 11 | 12 | @implementation DebugLayer 13 | 14 | - (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key 15 | { 16 | 17 | #if K_LOG_KEYFRAME_STEPS 18 | printf("\n"); 19 | NSLog(@"Adding animation for key \"%@\". Animation = %@", key, anim); 20 | #endif 21 | if ( [anim isMemberOfClass: [CAKeyframeAnimation class]]) 22 | { 23 | #if K_LOG_KEYFRAME_STEPS || K_FIX_ANIMATION 24 | NSValue *previousValue = nil; 25 | NSNumber *previousTime = nil; 26 | BOOL duplicates_found = NO; 27 | CAKeyframeAnimation *keyframe = (CAKeyframeAnimation *) anim; 28 | #endif 29 | 30 | #if K_LOG_KEYFRAME_STEPS || K_FIX_ANIMATION 31 | NSString *dupeString; 32 | for (int index = 0; index--->> Removing extra indexes from values and keyTimes <<---"); 69 | 70 | previousValue = nil; 71 | previousTime = nil; 72 | #endif 73 | 74 | #if K_LOG_KEYFRAME_STEPS || K_FIX_ANIMATION 75 | 76 | for (int index = 0; index= index+1) 109 | aTime = keyframe.keyTimes[index]; 110 | #if K_LOG_KEYFRAME_STEPS 111 | NSValue *aValue = keyframe.values[index]; 112 | const char *aValueType = [aValue objCType]; 113 | if (strstr(aValueType, "Point") != NULL) 114 | NSLog(@" Key %d, value = %@,\ttime = %.2f", index, aValue, aTime.floatValue); 115 | else 116 | NSLog(@" Key %d, value = %s,\ttime = %.2f", index, aValueType, aTime.floatValue); 117 | #endif 118 | } 119 | #endif 120 | 121 | #if K_FIX_ANIMATION 122 | } 123 | #endif 124 | } 125 | [super addAnimation: anim 126 | forKey: key]; 127 | } 128 | @end 129 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "ipad", 5 | "size" : "29x29", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "ipad", 10 | "size" : "29x29", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "ipad", 15 | "size" : "40x40", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "40x40", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "size" : "76x76", 25 | "idiom" : "ipad", 26 | "filename" : "wareto icon iPad.png", 27 | "scale" : "1x" 28 | }, 29 | { 30 | "size" : "76x76", 31 | "idiom" : "ipad", 32 | "filename" : "wareto icon iPad@2x.png", 33 | "scale" : "2x" 34 | } 35 | ], 36 | "info" : { 37 | "version" : 1, 38 | "author" : "xcode" 39 | } 40 | } -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/AppIcon.appiconset/wareto icon iPad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DuncanMC/KeyframeViewAnimations/e1fd88d9359664120231ee5669c709a775e1f553/KeyframeViewAnimations/Images.xcassets/AppIcon.appiconset/wareto icon iPad.png -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/AppIcon.appiconset/wareto icon iPad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DuncanMC/KeyframeViewAnimations/e1fd88d9359664120231ee5669c709a775e1f553/KeyframeViewAnimations/Images.xcassets/AppIcon.appiconset/wareto icon iPad@2x.png -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "ipad", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "1x" 9 | }, 10 | { 11 | "orientation" : "landscape", 12 | "idiom" : "ipad", 13 | "extent" : "full-screen", 14 | "minimum-system-version" : "7.0", 15 | "scale" : "1x" 16 | }, 17 | { 18 | "orientation" : "portrait", 19 | "idiom" : "ipad", 20 | "extent" : "full-screen", 21 | "minimum-system-version" : "7.0", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "orientation" : "landscape", 26 | "idiom" : "ipad", 27 | "extent" : "full-screen", 28 | "minimum-system-version" : "7.0", 29 | "scale" : "2x" 30 | } 31 | ], 32 | "info" : { 33 | "version" : 1, 34 | "author" : "xcode" 35 | } 36 | } -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/WareTo icon w center dot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "Wareto icon w center dot 76x100.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "Wareto icon w center dot 76x100@2x.png" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/WareTo icon w center dot.imageset/Wareto icon w center dot 76x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DuncanMC/KeyframeViewAnimations/e1fd88d9359664120231ee5669c709a775e1f553/KeyframeViewAnimations/Images.xcassets/WareTo icon w center dot.imageset/Wareto icon w center dot 76x100.png -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/WareTo icon w center dot.imageset/Wareto icon w center dot 76x100@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DuncanMC/KeyframeViewAnimations/e1fd88d9359664120231ee5669c709a775e1f553/KeyframeViewAnimations/Images.xcassets/WareTo icon w center dot.imageset/Wareto icon w center dot 76x100@2x.png -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/WareTo icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "Wareto icon 76x100.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "Wareto icon 76x100@2x.png" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/WareTo icon.imageset/Wareto icon 76x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DuncanMC/KeyframeViewAnimations/e1fd88d9359664120231ee5669c709a775e1f553/KeyframeViewAnimations/Images.xcassets/WareTo icon.imageset/Wareto icon 76x100.png -------------------------------------------------------------------------------- /KeyframeViewAnimations/Images.xcassets/WareTo icon.imageset/Wareto icon 76x100@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DuncanMC/KeyframeViewAnimations/e1fd88d9359664120231ee5669c709a775e1f553/KeyframeViewAnimations/Images.xcassets/WareTo icon.imageset/Wareto icon 76x100@2x.png -------------------------------------------------------------------------------- /KeyframeViewAnimations/KeyframeViewAnimations-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | com.wareto.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations~ipad 34 | 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/KeyframeViewAnimations-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_5_0 10 | #warning "This project uses features only available in iOS SDK 5.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | #endif 17 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/UIBezierPath-Points.h: -------------------------------------------------------------------------------- 1 | /* 2 | Erica Sadun, http://ericasadun.com 3 | iPhone Developer's Cookbook, 6.x Edition 4 | BSD License, Use at your own risk 5 | */ 6 | 7 | #import 8 | 9 | @interface UIBezierPath (Points) 10 | - (NSArray *) points; 11 | @end 12 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/UIBezierPath-Points.m: -------------------------------------------------------------------------------- 1 | /* 2 | Erica Sadun, http://ericasadun.com 3 | iPhone Developer's Cookbook, 6.x Edition 4 | BSD License, Use at your own risk 5 | */ 6 | 7 | #import "UIBezierPath-Points.h" 8 | 9 | #define VALUE(_INDEX_) [NSValue valueWithCGPoint:points[_INDEX_]] 10 | 11 | @implementation UIBezierPath (Points) 12 | void getPointsFromBezier(void *info, const CGPathElement *element) 13 | { 14 | NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info; 15 | 16 | // Retrieve the path element type and its points 17 | CGPathElementType type = element->type; 18 | CGPoint *points = element->points; 19 | 20 | // Add the points if they're available (per type) 21 | if (type != kCGPathElementCloseSubpath) 22 | { 23 | [bezierPoints addObject:VALUE(0)]; 24 | if ((type != kCGPathElementAddLineToPoint) && 25 | (type != kCGPathElementMoveToPoint)) 26 | [bezierPoints addObject:VALUE(1)]; 27 | } 28 | if (type == kCGPathElementAddCurveToPoint) 29 | [bezierPoints addObject:VALUE(2)]; 30 | } 31 | 32 | - (NSArray *)points 33 | { 34 | NSMutableArray *points = [NSMutableArray array]; 35 | CGPathApply(self.CGPath, (__bridge void *)points, getPointsFromBezier); 36 | return points; 37 | } 38 | @end 39 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/UIBezierPath-Smoothing.h: -------------------------------------------------------------------------------- 1 | /* 2 | Erica Sadun, http://ericasadun.com 3 | iPhone Developer's Cookbook, 6.x Edition 4 | BSD License, Use at your own risk 5 | */ 6 | 7 | #import 8 | 9 | @interface UIBezierPath (Smoothing) 10 | - (UIBezierPath *) smoothedPath: (int) granularity; 11 | @end 12 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/UIBezierPath-Smoothing.m: -------------------------------------------------------------------------------- 1 | /* 2 | Erica Sadun, http://ericasadun.com 3 | iPhone Developer's Cookbook, 6.x Edition 4 | BSD License, Use at your own risk 5 | */ 6 | 7 | #import "UIBezierPath-Smoothing.h" 8 | #import "UIBezierPath-Points.h" 9 | 10 | #define K_BUG_FIX 1 11 | 12 | #define POINT(_INDEX_) [(NSValue *)[points objectAtIndex:_INDEX_] CGPointValue] 13 | 14 | @implementation UIBezierPath (Smoothing) 15 | - (UIBezierPath *) smoothedPath: (int) granularity 16 | { 17 | NSMutableArray *points = [self.points mutableCopy]; 18 | if (points.count < 4) return [self copy]; 19 | 20 | // Add control points to make the math make sense 21 | // Via Josh Weinberg 22 | [points insertObject:[points objectAtIndex:0] atIndex:0]; 23 | [points addObject:[points lastObject]]; 24 | 25 | UIBezierPath *smoothedPath = [UIBezierPath bezierPath]; 26 | 27 | // Copy traits 28 | smoothedPath.lineWidth = self.lineWidth; 29 | 30 | // Draw out the first 3 points (0..2) 31 | [smoothedPath moveToPoint:POINT(0)]; 32 | 33 | int line_points = 3; 34 | #if K_BUG_FIX 35 | line_points = 2; 36 | #endif 37 | 38 | for (int index = 1; index < line_points; index++) //change "for (int index = 1; index < 3; index++)" to "for (int index = 1; index < 2; index++)". 39 | [smoothedPath addLineToPoint:POINT(index)]; 40 | 41 | for (int index = line_points+1; index < points.count; index++) //Change "for (int index = 4; index < points.count; index++)" to "for (int index = 3; index < points.count; index++)" 42 | { 43 | CGPoint p0 = POINT(index - 3); 44 | CGPoint p1 = POINT(index - 2); 45 | CGPoint p2 = POINT(index - 1); 46 | CGPoint p3 = POINT(index); 47 | 48 | // now add n points starting at p1 + dx/dy up until p2 using Catmull-Rom splines 49 | for (int i = 1; i < granularity; i++) 50 | { 51 | float t = (float) i * (1.0f / (float) granularity); 52 | float tt = t * t; 53 | float ttt = tt * t; 54 | 55 | CGPoint pi; // intermediate point 56 | pi.x = 0.5 * (2*p1.x+(p2.x-p0.x)*t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*tt + (3*p1.x-p0.x-3*p2.x+p3.x)*ttt); 57 | pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t + (2*p0.y-5*p1.y+4*p2.y-p3.y)*tt + (3*p1.y-p0.y-3*p2.y+p3.y)*ttt); 58 | [smoothedPath addLineToPoint:pi]; 59 | } 60 | 61 | // Now add p2 62 | [smoothedPath addLineToPoint:p2]; 63 | } 64 | 65 | // finish by adding the last point 66 | [smoothedPath addLineToPoint:POINT(points.count - 1)]; 67 | 68 | return smoothedPath; 69 | } 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/4/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #define kAnimationCompletionBlock @"animationCompletionBlock" 12 | typedef void (^animationCompletionBlock)(void); 13 | 14 | typedef enum 15 | { 16 | keyframeAnimation, 17 | rotationAnimation 18 | } animationType; 19 | 20 | @interface ViewController : UIViewController 21 | { 22 | __weak IBOutlet UIImageView *imageViewToAnimate; 23 | __weak IBOutlet UIView *animationView; 24 | __weak IBOutlet UIButton *animateButton; 25 | __weak IBOutlet UIButton *animateCAButton; 26 | __weak IBOutlet UIButton *rotateButton; 27 | 28 | __weak IBOutlet UIButton *pauseButton; 29 | __weak IBOutlet UIButton *stopButton; 30 | __weak IBOutlet UISlider *animationSlider; 31 | 32 | __weak IBOutlet UIView *keyframeAnimationPlaceholder; 33 | CGPoint startingCenter; 34 | CGFloat angle; 35 | CFTimeInterval animationStartTime; 36 | CFTimeInterval totalAnimationTime; 37 | CGFloat animationProgress; 38 | 39 | CAShapeLayer *pathLayer; 40 | __weak NSTimer *sliderTimer; 41 | } 42 | 43 | @property (nonatomic, assign) BOOL animationIsPaused; 44 | 45 | - (IBAction)handleAnimateButton:(UIButton *)sender; 46 | - (IBAction)handleRotateButton:(UIButton *)sender; 47 | - (IBAction)handlePauseButton:(UIButton *)sender; 48 | - (IBAction)handleAnimationSlider:(UISlider *)sender; 49 | - (IBAction)handleStopButton:(UIButton *)sender; 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/4/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "Constants.h" 11 | #import "UIBezierPath-Smoothing.h" 12 | 13 | 14 | @interface ViewController () 15 | 16 | @end 17 | 18 | @implementation ViewController 19 | 20 | //------------------------------------------------------------------------------------------------------ 21 | #pragma mark - property methods 22 | //------------------------------------------------------------------------------------------------------ 23 | /* 24 | This property method either pauses or unpauses the current animation. 25 | */ 26 | -(void) setAnimationIsPaused:(BOOL)animationIsPaused; 27 | { 28 | _animationIsPaused = animationIsPaused; 29 | 30 | [self setPauseButtonTitle]; 31 | 32 | CALayer *theLayer = imageViewToAnimate.layer; 33 | 34 | if (animationIsPaused) 35 | //Pause the current animation on the image view's layer. 36 | { 37 | //Stop the slider timer 38 | [sliderTimer invalidate]; 39 | 40 | //Setting the layer's speed to zero freezes the animmation. 41 | theLayer.speed = 0; 42 | 43 | //Calculate how far we into the animation based on current media time minus the media time when 44 | //The animation started. 45 | CFTimeInterval mediaTime = CACurrentMediaTime(); 46 | animationProgress = mediaTime - animationStartTime; 47 | 48 | CFTimeInterval pausedTime = mediaTime; 49 | 50 | //Shift the layer's timing so it appears at the current time. 51 | theLayer.timeOffset = pausedTime-theLayer.beginTime; 52 | animationStartTime -= theLayer.beginTime; 53 | theLayer.beginTime = 0; 54 | } 55 | else //else un-pause the animation 56 | { 57 | //Get the layer's current time offset. 58 | CFTimeInterval pausedTime = [theLayer timeOffset]; 59 | 60 | //Now reset the time offset and beginTime to zero. 61 | theLayer.timeOffset = 0.0; 62 | theLayer.beginTime = 0.0; 63 | 64 | CFTimeInterval mediaTime = CACurrentMediaTime(); 65 | 66 | //Figure out how much time has elapsed since the animation was paused. 67 | CFTimeInterval timeSincePause = mediaTime - pausedTime; 68 | 69 | //Set the layer's beginTime to that time-since-pause 70 | theLayer.beginTime = timeSincePause; 71 | 72 | //Figure out when the animation would have started in order to be this far along in its progress. 73 | animationStartTime = CACurrentMediaTime() -animationProgress; 74 | 75 | //Start the animation running again 76 | theLayer.speed = 1.0; 77 | 78 | //Start the slider timer so the slider updates. 79 | [self startSliderTimer]; 80 | } 81 | } 82 | 83 | //------------------------------------------------------------------------------------------------------ 84 | #pragma mark - view lifecycle methods 85 | //------------------------------------------------------------------------------------------------------ 86 | 87 | - (void) viewDidAppear:(BOOL)animated 88 | { 89 | //Remember where our image vew is positioned at first. 90 | startingCenter = imageViewToAnimate.center; 91 | } 92 | 93 | //------------------------------------------------------------------------------------------------------ 94 | 95 | - (void)didReceiveMemoryWarning 96 | { 97 | [super didReceiveMemoryWarning]; 98 | // Dispose of any resources that can be recreated. 99 | } 100 | 101 | 102 | //------------------------------------------------------------------------------------------------------ 103 | #pragma mark - custom instance methods 104 | //------------------------------------------------------------------------------------------------------ 105 | 106 | /* 107 | This method uses a CABasicAnimation to animate a 720 degree rotation on our image view's layer. 108 | There are a couple of things that are noteworthy. 109 | 110 | 1. normally you can't control the rotation direction (clockwise/counterclockwize) of rotations of 1/2 111 | turn or more, since the system simple rotates the object in the direction that requires the smallest 112 | rotation. 113 | 114 | To solve this, we're going to use the valueFunction property of the animation, 115 | with a value of kCAValueFunctionRotateZ. This tells the system that we want to apply a rotation 116 | around the Z axis, and the value(s) we're providing in fromValue and toValue are angles instead 117 | of a full transformation matrix. This makes it possible to animate a rotation of an arbitrary amount, 118 | including multiple full rotations - something that's normally not possible with a single animation. 119 | 120 | 2. Normally a CAAnimation will always invoke a single delegate method, animationDidStop:finished:, 121 | once the animation is completed. If you are managing multiple CAAnimation objects that need 122 | custom completion code, this quickly becomes a mess. 123 | 124 | What we've done here is to take advantage of an interesting behavior of CAAnimation objects: 125 | 126 | They support the methods setValue:forKey and valueForKey:. You can attach an arbitrary object 127 | to an animation, and that object will stay attached. We take advantage of this by creating a custom 128 | block type, animationCompletionBlock, and attaching an animationCompletionBlock to our animation, 129 | using the key kAnimationCompletionBlock 130 | (which is just a #define for @"animationCompletionBlock"). 131 | 132 | Our animationDidStop:finished method simply uses valueForKey:kAnimationCompletionBlock to look 133 | for a code block attached to the aniamtion. If it exists, the completion method executes it. 134 | 135 | Using this approach, you can specify completion code when you define your animation, instead of 136 | having to maintain a big switch statement in your global animationDidStop:finished method. 137 | */ 138 | 139 | - (void) handleRotate; 140 | { 141 | #define full_rotation M_PI*2 142 | #define rotation_count 2 143 | 144 | animationCompletionBlock completionBlock; 145 | 146 | static float change =-full_rotation* rotation_count; 147 | 148 | change *= -1; //Have our rotation alternate between clockwise and counter-clockwise. 149 | 150 | //Create a CABasicAnimation object to manage our rotation. 151 | CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:@"transform"]; 152 | totalAnimationTime = rotation_count; 153 | 154 | //Make sure the begin time on the animation is 0, 155 | //in case we left at a different value on the last animation. 156 | imageViewToAnimate.layer.beginTime = 0; 157 | 158 | rotation.duration = totalAnimationTime; 159 | 160 | //Start the animation at the previous value of angle 161 | rotation.fromValue = @(angle); 162 | 163 | //Add change (which will be a change of +/- 2pi*rotation_count 164 | angle += change; 165 | 166 | //Set the ending value of the rotation to the new angle. 167 | rotation.toValue = @(angle); 168 | 169 | //Have the rotation use linear timing. 170 | rotation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; 171 | 172 | /* 173 | 174 | This is the magic bit. We add a CAValueFunction that tells the CAAnimation we are modifying 175 | the transform's rotation around the Z axis. 176 | Without this, we would supply a transform as the fromValue and toValue, and for rotations 177 | > a half-turn, we could not control the rotation direction. 178 | 179 | By using a value function, we can specify arbitrary rotation amounts and directions, and even 180 | Rotations greater than 360 degrees. 181 | */ 182 | 183 | rotation.valueFunction = [CAValueFunction functionWithName: kCAValueFunctionRotateZ]; 184 | 185 | //Make ourselves the animation's delegate, so we get called when it's finished. 186 | rotation.delegate = self; 187 | animationStartTime = CACurrentMediaTime(); 188 | 189 | 190 | //Create a block of code to be executed once our animation finishes. 191 | completionBlock = ^void(void) 192 | { 193 | animateButton.enabled = YES; 194 | animateCAButton.enabled = YES; 195 | rotateButton.enabled = YES; 196 | pauseButton.enabled = NO; 197 | stopButton.enabled = NO; 198 | 199 | animationSlider.enabled = NO; 200 | animationSlider.value = 0; 201 | 202 | imageViewToAnimate.layer.transform = CATransform3DIdentity; 203 | [sliderTimer invalidate]; 204 | _animationIsPaused = NO; 205 | imageViewToAnimate.layer.timeOffset = 0; 206 | [self setPauseButtonTitle]; 207 | }; 208 | 209 | /* 210 | Attach the completion block to the animation using the key kAnimationCompletionBlock. 211 | Our animationDidStop:finished: delegate method will execute this block when the animation completes. 212 | 213 | Unlike most objects, CAAnimation and CALayer objects allow you 214 | to attach any arbitrary key/value pair to them. 215 | */ 216 | 217 | [rotation setValue: completionBlock forKey: kAnimationCompletionBlock]; 218 | 219 | /* 220 | Set the layer's transform to it's final state before submitting the animation, so it is in it's 221 | final state once the animation completes. 222 | */ 223 | imageViewToAnimate.layer.transform = CATransform3DRotate(imageViewToAnimate.layer.transform, angle, 0, 0, 1.0); 224 | 225 | //Now actually add the animation to the layer. 226 | [self startSliderTimer]; 227 | [imageViewToAnimate.layer addAnimation:rotation forKey:@"transform.rotation.z"]; 228 | } 229 | 230 | //------------------------------------------------------------------------------------------------------ 231 | /* 232 | This method does a keyframe animation that is visually identical to the doKeyFrameViewAnimation method, 233 | But it uses CAKeyframeAnimation objects instead of using the new iOS 7 view-based keframe animations. 234 | 235 | */ 236 | 237 | 238 | - (void) doKeyframeCAAnimation; 239 | { 240 | CGSize imageSize = imageViewToAnimate.bounds.size; 241 | 242 | //Create a rectangle to contain the center-point of our animations 243 | //(inset by 20 pixels + the height & width of the image view 244 | CGRect animationBounds = CGRectIntegral( 245 | CGRectInset( 246 | animationView.bounds, 20 + imageSize.width/2, 247 | 20+imageSize.height/2)); 248 | totalAnimationTime = 8.0; 249 | 250 | //Remember the time when we start the animation (we'll use that value in calculating the slider 251 | //Poistion as well as in figuring out how to pause the animation. 252 | animationStartTime = CACurrentMediaTime(); 253 | 254 | //Make sure the animation is set to begin at the start of the animation duration. 255 | imageViewToAnimate.layer.beginTime = 0; 256 | 257 | animationProgress = 0; 258 | 259 | [self startSliderTimer]; 260 | 261 | if (!pathLayer) 262 | [self createPathLayer]; 263 | 264 | pathLayer.opacity = 1.0; 265 | 266 | __block CGFloat animationSteps = K_KEYFRAME_STEPS; 267 | 268 | CGFloat stepDistance = round(animationBounds.size.width / animationSteps); 269 | 270 | int stepCount; 271 | 272 | animationSlider.enabled = YES; 273 | pauseButton.enabled = YES; 274 | stopButton.enabled = YES; 275 | 276 | animationCompletionBlock completionBlock; 277 | 278 | //----------------------------------- 279 | 280 | 281 | //Create a keyframe CAAnimation that moves the view's layer using cubic calculations 282 | CAKeyframeAnimation* keyframeMove = nil; 283 | keyframeMove= [CAKeyframeAnimation animationWithKeyPath: @"position"]; 284 | keyframeMove.duration = totalAnimationTime; 285 | keyframeMove.beginTime = 0; 286 | keyframeMove.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; 287 | #if K_USE_CUBIC_PACING 288 | keyframeMove.calculationMode = kCAAnimationCubic; 289 | #else 290 | keyframeMove.calculationMode = kCAAnimationLinear; 291 | #endif 292 | 293 | keyframeMove.removedOnCompletion = FALSE; 294 | keyframeMove.fillMode = kCAFillModeBackwards; 295 | 296 | NSMutableArray *pathArray = [NSMutableArray arrayWithCapacity:animationSteps]; 297 | 298 | //Add the starting point 299 | [pathArray addObject: [NSValue valueWithCGPoint: keyframeAnimationPlaceholder.layer.position]]; 300 | for (stepCount = 1; stepCount <= animationSteps; stepCount++) 301 | { 302 | CGPoint stepCenter; 303 | CGFloat newY; 304 | CGFloat newX; 305 | 306 | //Alternate between the top and bottom of the animation view 307 | if (stepCount %2 == 0) //even step (0, 2, 4, 6). Position along the bottom 308 | newY = floorf(animationBounds.origin.y +animationBounds.size.height); 309 | else 310 | newY =animationBounds.origin.y; 311 | 312 | newX = truncf(animationBounds.origin.x + stepDistance * stepCount); 313 | stepCenter = CGPointMake(newX, newY ); 314 | 315 | [pathArray addObject: [NSValue valueWithCGPoint: stepCenter]]; 316 | } 317 | 318 | keyframeMove.values = pathArray; 319 | 320 | //Create a block of code to be executed once our animation finishes. 321 | completionBlock = ^void(void) 322 | { 323 | pauseButton.enabled = NO; 324 | stopButton.enabled = NO; 325 | 326 | animationSlider.enabled = NO; 327 | animationSlider.value = 0; 328 | 329 | imageViewToAnimate.layer.transform = CATransform3DIdentity; 330 | [sliderTimer invalidate]; 331 | _animationIsPaused = NO; 332 | imageViewToAnimate.layer.timeOffset = 0; 333 | 334 | //After a pause, Animate the image view back to it's starting point. 335 | _animationIsPaused = NO; 336 | imageViewToAnimate.layer.speed = 1.0; 337 | pathLayer.opacity = 0; 338 | 339 | [UIView animateWithDuration: .2 340 | delay: .5 341 | options: 0 342 | animations: 343 | ^{ 344 | imageViewToAnimate.center = startingCenter; 345 | imageViewToAnimate.transform = CGAffineTransformIdentity; 346 | } 347 | //And in the completion block for THIS animation, re-enable the animation buttons. 348 | completion: ^(BOOL finished) 349 | { 350 | animateButton.enabled = YES; 351 | animateCAButton.enabled = YES; 352 | rotateButton.enabled = YES; 353 | 354 | [self setPauseButtonTitle]; 355 | }]; 356 | }; 357 | 358 | [keyframeMove setValue: completionBlock forKey: kAnimationCompletionBlock]; 359 | keyframeMove.delegate = self; 360 | 361 | [imageViewToAnimate.layer addAnimation: keyframeMove forKey: @"CALayerKeyframes"]; 362 | imageViewToAnimate.layer.position = [[pathArray lastObject] CGPointValue]; 363 | 364 | //----------------------------------- 365 | //Create a keyframe CAAnimation that rotates the view's layer as it moves 366 | 367 | 368 | 369 | #if K_ROTATE 370 | CAKeyframeAnimation* keyframeRotate; 371 | angle = 0; 372 | animationSteps = 8; 373 | keyframeRotate= [CAKeyframeAnimation animationWithKeyPath: @"transform"]; 374 | keyframeRotate.removedOnCompletion = FALSE; 375 | keyframeRotate.duration = totalAnimationTime; 376 | keyframeRotate.beginTime = 0; 377 | keyframeRotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; 378 | 379 | keyframeRotate.valueFunction = [CAValueFunction functionWithName: kCAValueFunctionRotateZ]; 380 | keyframeRotate.delegate = self; 381 | 382 | //Build an array of angles for rotating the view 383 | NSMutableArray *anglesArray = [NSMutableArray arrayWithCapacity:animationSteps]; 384 | //Add the starting angle 385 | [anglesArray addObject: @0]; 386 | 387 | for (stepCount = 1; stepCount <= animationSteps; stepCount++) 388 | { 389 | angle = M_PI * 4 * stepCount/animationSteps; 390 | [anglesArray addObject: @(angle)]; 391 | } 392 | keyframeRotate.values = anglesArray; 393 | [imageViewToAnimate.layer addAnimation: keyframeRotate forKey: @"CALayerRotateKeyframes"]; 394 | #endif 395 | } 396 | //------------------------------------------------------------------------------------------------------ 397 | - (void) createPathLayer; 398 | { 399 | CGSize imageSize = imageViewToAnimate.bounds.size; 400 | 401 | CGRect pathBounds = CGRectIntegral(CGRectInset( 402 | animationView.layer.bounds, 403 | 20 + imageSize.width/2, 404 | 20+imageSize.height/2)); 405 | 406 | UIBezierPath *path = [UIBezierPath new]; 407 | 408 | pathLayer = [CAShapeLayer layer]; 409 | 410 | pathLayer.frame = animationView.layer.bounds; 411 | CGFloat animationSteps = K_KEYFRAME_STEPS; 412 | CGFloat stepDistance = round(pathBounds.size.width / animationSteps); 413 | 414 | int stepCount = 1; 415 | 416 | CGPoint stepCenter; 417 | CGFloat newY; 418 | CGFloat newX; 419 | [path moveToPoint: keyframeAnimationPlaceholder.layer.position]; 420 | for (stepCount = 1; stepCount <= animationSteps; stepCount++) 421 | { //Alternate between the top and bottom of the animation view 422 | if (stepCount %2 == 0) //even step (0, 2, 4, 6). Position along the bottom 423 | newY = floorf(pathBounds.origin.y +pathBounds.size.height); 424 | else 425 | newY =pathBounds.origin.y; 426 | 427 | newX = truncf(pathBounds.origin.x + stepDistance * stepCount); 428 | stepCenter = CGPointMake(newX, newY ); 429 | [path addLineToPoint: stepCenter]; 430 | } 431 | 432 | #if K_USE_CUBIC_PACING 433 | path = [path smoothedPath: 16]; 434 | #endif 435 | pathLayer.path = path.CGPath; 436 | pathLayer.strokeColor = [UIColor colorWithRed: 0 437 | green: 0 438 | blue: 0 439 | alpha: .5].CGColor; 440 | // pathLayer.strokeColor = [UIColor blackColor].CGColor; 441 | pathLayer.fillColor = [UIColor clearColor].CGColor; 442 | pathLayer.lineWidth = 1.0; 443 | pathLayer.lineDashPattern = [NSArray arrayWithObjects: 444 | [NSNumber numberWithInt: 5], 445 | [NSNumber numberWithInt: 9], 446 | nil]; 447 | 448 | [animationView.layer addSublayer: pathLayer]; 449 | 450 | } 451 | 452 | //------------------------------------------------------------------------------------------------------ 453 | /* 454 | This method uses the new iOS 7 UIView class method 455 | animateKeyframesWithDuration:delay:options:animations:completion: 456 | to run a multi-step keyframe animation on our image view "imageViewToAnimate". 457 | */ 458 | 459 | - (void) doKeyFrameViewAnimation; 460 | { 461 | pathLayer.opacity = 1.0; 462 | CGSize imageSize = imageViewToAnimate.bounds.size; 463 | 464 | //Create a rectangle to contain the center-point of our animations 465 | //(inset by 20 pixels + the height & width of the image view 466 | CGRect animationBounds = CGRectIntegral( 467 | CGRectInset( 468 | animationView.bounds, 20 + imageSize.width/2, 469 | 20+imageSize.height/2)); 470 | totalAnimationTime = 8.0; 471 | 472 | if (!pathLayer) 473 | [self createPathLayer]; 474 | 475 | 476 | //Remember the time when we start the animation (we'll use that value in calculating the slider 477 | //Poistion as well as in figuring out how to pause the animation. 478 | animationStartTime = CACurrentMediaTime(); 479 | 480 | //Make sure the animation is set to begin at the start of the animation duration. 481 | imageViewToAnimate.layer.beginTime = 0; 482 | 483 | animationProgress = 0; 484 | 485 | [self startSliderTimer]; 486 | 487 | __block CGFloat animationSteps = K_KEYFRAME_STEPS; 488 | 489 | CGFloat stepDistance = round(animationBounds.size.width / animationSteps); 490 | 491 | __block int stepCount = 1; 492 | 493 | /* 494 | First we define a block of code that we will use in a call to 495 | addKeyframeWithRelativeStartTime:relativeDuration:animations: 496 | This block of code simply alternates the image view from top to bottom of the screen 497 | as it moves it from left to right. 498 | */ 499 | 500 | void (^animationStepBlock)() = 501 | ^{ 502 | CGPoint stepCenter; 503 | CGFloat newY; 504 | CGFloat newX; 505 | 506 | //Alternate between the top and bottom of the animation view 507 | if (stepCount %2 == 0) //even step (0, 2, 4, 6). Position along the bottom 508 | newY = floorf(animationBounds.origin.y +animationBounds.size.height); 509 | else 510 | newY =animationBounds.origin.y; 511 | 512 | newX = truncf(animationBounds.origin.x + stepDistance * stepCount); 513 | stepCenter = CGPointMake(newX, newY ); 514 | #if K_LOG_KEYFRAME_STEPS 515 | NSLog(@" Animation center = %@", NSStringFromCGPoint(stepCenter)); 516 | #endif 517 | imageViewToAnimate.center = stepCenter; 518 | }; 519 | 520 | //-------------- 521 | /* 522 | The method animateKeyframesWithDuration:delay:options:animations:completion: lets you trigger 523 | a sequence of animations in a block. Each animation in the sequence is specified 524 | with a call to addKeyframeWithRelativeStartTime:relativeDuration:animations: 525 | The start time is in the range 0..1 526 | (0= first instant of the keyframe sequence, and 1= the last instanct of the keyframe sequence.) 527 | 528 | Likewise the relativeDuration parameter ranges from 0 to 1, where 0 means it takes no time at all, and 1 529 | means it uses the duration of the entire keyframe sequence. 530 | 531 | you can submit your individual keyframe steps in any order, and you can even have mulitple 532 | keframes running at the same time. 533 | 534 | Furthermore, these keyframe animation steps can operate on mulitple different view objects. 535 | */ 536 | 537 | animationSlider.enabled = YES; 538 | pauseButton.enabled = YES; 539 | stopButton.enabled = YES; 540 | 541 | printf("\n"); 542 | #if K_LOG_KEYFRAME_STEPS 543 | NSLog(@"Building keyframe view animation"); 544 | #endif 545 | 546 | int options; 547 | 548 | #if K_USE_CUBIC_PACING 549 | options = UIViewKeyframeAnimationOptionCalculationModeCubic + UIViewAnimationOptionCurveLinear; 550 | #else 551 | options = UIViewKeyframeAnimationOptionCalculationModeLinear + UIViewAnimationOptionCurveLinear; 552 | 553 | #endif 554 | [UIView animateKeyframesWithDuration: totalAnimationTime 555 | delay:0.0 556 | options: options 557 | animations: 558 | ^{ 559 | /* 560 | In a for loop, make repeated calls to addKeyframeWithRelativeStartTime:relativeDuration:animations: to 561 | add multiple animation steps to the keyframe sequence. We pass in the block we defined above. 562 | */ 563 | 564 | for (stepCount = 1; stepCount <= animationSteps; stepCount++) 565 | { 566 | CGFloat startTime = (stepCount-1)/animationSteps; 567 | CGFloat relDuration = 1/animationSteps; 568 | #if K_LOG_KEYFRAME_STEPS 569 | NSLog(@" Adding animation step %d, start time = %.3f, duration = %.3f", stepCount, startTime, relDuration); 570 | #endif 571 | 572 | [UIView addKeyframeWithRelativeStartTime: startTime 573 | relativeDuration: relDuration 574 | animations: animationStepBlock 575 | ]; 576 | } 577 | printf("\n"); 578 | 579 | #if K_ROTATE 580 | 581 | /* 582 | Also animate a change to the rotation of the view's layer. We run this animation in 6 steps to show that 583 | the 2 sets of keyframes are run independently and concurrently. 584 | */ 585 | 586 | animationSteps = 8; 587 | for (stepCount = 1; stepCount <= animationSteps; stepCount++) 588 | { 589 | CGFloat startTime = (stepCount-1)/animationSteps; 590 | CGFloat relDuration = 1/animationSteps; 591 | 592 | [UIView addKeyframeWithRelativeStartTime: startTime 593 | relativeDuration: relDuration 594 | animations: 595 | ^{ 596 | //Make the rotation change go through a full 4 pi angle change (2 full rotations) during the sequence 597 | CGFloat viewAngle =M_PI * 4 * stepCount/animationSteps; 598 | CGAffineTransform transform = CGAffineTransformMakeRotation(viewAngle); 599 | imageViewToAnimate.transform = transform; 600 | } 601 | ]; 602 | } 603 | #endif 604 | } 605 | //Provide a completion block for the entire keyframe sequnce. 606 | completion: ^(BOOL finished) 607 | { 608 | //Stop the slider animation 609 | [sliderTimer invalidate]; 610 | 611 | //Disable the slider, pause/resume button, and stop button 612 | animationSlider.enabled = NO; 613 | pauseButton.enabled = NO; 614 | stopButton.enabled = NO; 615 | pathLayer.opacity = 0; 616 | 617 | //Reset the slider value to zero. 618 | animationSlider.value = 0; 619 | 620 | 621 | //After a pause, Animate the image view back to it's starting point. 622 | _animationIsPaused = NO; 623 | [UIView animateWithDuration: .2 624 | delay: .5 625 | options: 0 626 | animations: 627 | ^{ 628 | imageViewToAnimate.center = startingCenter; 629 | imageViewToAnimate.transform = CGAffineTransformIdentity; 630 | } 631 | //And in the completion block for THIS animation, re-enable the animation buttons. 632 | completion: ^(BOOL finished) 633 | { 634 | animateButton.enabled = YES; 635 | animateCAButton.enabled = YES; 636 | rotateButton.enabled = YES; 637 | 638 | [self setPauseButtonTitle]; 639 | }]; 640 | }]; 641 | } 642 | 643 | //----------------------------------------------------------------------------------------------------------- 644 | 645 | //This is the delegate method for a CAAnimation. Instead of putting custom code in this method, we check 646 | //To see if the animation has a code block attached to it 647 | 648 | - (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag 649 | { 650 | //Is there a completion block attached to this CAAnimation? 651 | animationCompletionBlock theBlock = [theAnimation valueForKey: kAnimationCompletionBlock]; 652 | 653 | //If yes, execute it. 654 | if (theBlock) 655 | theBlock(); 656 | } 657 | 658 | //----------------------------------------------------------------------------------------------------------- 659 | 660 | - (void) startSliderTimer; 661 | { 662 | sliderTimer = [NSTimer scheduledTimerWithTimeInterval: 1/30.0 663 | target: self selector: @selector(handleSliderTimer:) 664 | userInfo: nil 665 | repeats: YES]; 666 | } 667 | 668 | //----------------------------------------------------------------------------------------------------------- 669 | //This timer method simply figures out how far we are into the current animation 670 | //and updates the slider position. 671 | 672 | - (void) handleSliderTimer: (NSTimer *) timer; 673 | { 674 | animationProgress = CACurrentMediaTime() - animationStartTime; 675 | CGFloat sliderValue = animationProgress/totalAnimationTime; 676 | animationSlider.value = sliderValue; 677 | } 678 | 679 | //----------------------------------------------------------------------------------------------------------- 680 | //Set the title of the pause/resume button based on the state of _animationIsPaused 681 | 682 | - (void) setPauseButtonTitle; 683 | { 684 | NSString *buttonTitle; 685 | 686 | //Create localized version of the strings "Pause" & "Continue" 687 | NSString *pauseString = NSLocalizedString(@"Pause", nil); 688 | NSString *continueString = NSLocalizedString(@"Continue", nil); 689 | 690 | buttonTitle = _animationIsPaused ? continueString: pauseString; 691 | [pauseButton setTitle: buttonTitle forState: UIControlStateNormal]; 692 | } 693 | 694 | //----------------------------------------------------------------------------------------------------------- 695 | #pragma mark - IBAction methods 696 | //----------------------------------------------------------------------------------------------------------- 697 | 698 | - (IBAction)handleAnimateButton:(UIButton *)sender 699 | { 700 | NSInteger tag = sender.tag; 701 | //Before we start, disable the animation buttons so the user can't trigger an animation until we're done 702 | animateButton.enabled = NO; 703 | animateCAButton.enabled = NO; 704 | rotateButton.enabled = NO; 705 | 706 | //Don't enable the pause/continue button or stop button until after we animate the image view to its 707 | //Starting position. 708 | 709 | animationProgress = 0; 710 | 711 | imageViewToAnimate.layer.timeOffset = 0; 712 | 713 | 714 | //First move the image view to it's starting position in the lower left corner of the screen 715 | [UIView animateWithDuration: .2 716 | animations: 717 | ^{ 718 | imageViewToAnimate.center = keyframeAnimationPlaceholder.center; 719 | } 720 | completion:^(BOOL finished) 721 | { 722 | //Once that animation is done, trigger the keyframe animation after a brief pause. 723 | if (tag == 1) 724 | [self performSelector: @selector(doKeyFrameViewAnimation) withObject: nil afterDelay: .75]; 725 | else 726 | [self performSelector: @selector(doKeyframeCAAnimation) withObject: nil afterDelay: .75]; 727 | } 728 | ]; 729 | } 730 | 731 | //----------------------------------------------------------------------------------------------------------- 732 | 733 | - (IBAction)handleRotateButton:(UIButton *)sender 734 | { 735 | //Before we start, disable the animation buttons so the user can't trigger an animation until we're done 736 | animateButton.enabled = NO; 737 | animateCAButton.enabled = NO; 738 | rotateButton.enabled = NO; 739 | 740 | //enable the pause/resume and stop buttons 741 | pauseButton.enabled =YES; 742 | stopButton.enabled = YES; 743 | animationSlider.enabled = YES; 744 | 745 | //Remember the time when we start the animation (we'll use that value in calculating the slider 746 | //Poistion as well as in figuring out how to pause the animation.) 747 | animationStartTime = CACurrentMediaTime(); 748 | 749 | animationProgress = 0; 750 | 751 | [self handleRotate]; 752 | } 753 | //----------------------------------------------------------------------------------------------------------- 754 | 755 | - (IBAction)handlePauseButton:(UIButton *)sender 756 | { 757 | self.animationIsPaused = !self.animationIsPaused; 758 | } 759 | 760 | //----------------------------------------------------------------------------------------------------------- 761 | 762 | - (IBAction)handleAnimationSlider:(UISlider *)sender 763 | { 764 | CGFloat sliderValue = sender.value; 765 | 766 | //Calculate how far we should be into the total animation, in seconds. 767 | CGFloat temp = sliderValue * totalAnimationTime; 768 | 769 | //Calculate an offset into the animation based on the previous value of animationProgress. 770 | //Do this before pausing the animation, because pausing the animation changes animationProgress. 771 | CFTimeInterval offset = animationStartTime + animationProgress; 772 | 773 | if (!self.animationIsPaused) 774 | self.animationIsPaused = YES; 775 | 776 | imageViewToAnimate.layer.timeOffset = offset; 777 | animationProgress = temp; 778 | } 779 | 780 | 781 | //----------------------------------------------------------------------------------------------------------- 782 | 783 | - (IBAction)handleStopButton:(UIButton *)sender 784 | { 785 | //Stop the slider timer 786 | [sliderTimer invalidate]; 787 | 788 | //Remove all CAAnimations running on our image view layer (this works both for UIView animations and for 789 | //Explicit CAAnimations, because UIView animations create CACAnimations "under the covers" 790 | [imageViewToAnimate.layer removeAllAnimations]; 791 | imageViewToAnimate.layer.speed = 1.0; 792 | } 793 | 794 | @end 795 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /KeyframeViewAnimations/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // KeyframeViewAnimations 4 | // 5 | // Created by Duncan Champney on 2/4/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "AppDelegate.h" 12 | 13 | int main(int argc, char * argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /KeyframeViewAnimationsTests/KeyframeViewAnimationsTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.wareto.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /KeyframeViewAnimationsTests/KeyframeViewAnimationsTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // KeyframeViewAnimationsTests.m 3 | // KeyframeViewAnimationsTests 4 | // 5 | // Created by Duncan Champney on 2/4/14. 6 | // Copyright (c) 2014 WareTo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface KeyframeViewAnimationsTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation KeyframeViewAnimationsTests 16 | 17 | - (void)setUp 18 | { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown 24 | { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | [super tearDown]; 27 | } 28 | 29 | - (void)testExample 30 | { 31 | XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /KeyframeViewAnimationsTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | KeyframeViewAnimations 2 | ====================== 3 | 4 | A project that demonstrates a number of iOS animation techniques: 5 | 6 | 7 | 8 | ##Animating a view/layer along a curved path using 2 different types of keyframe animation: 9 | 10 | 11 | 12 | 13 | 1. Using the new UIView animation method `animateKeyframesWithDuration:delay:options:animations:completion:` 14 | 15 | 2. Using a CAKeyframeAnimation with an array of position values. 16 | 17 | 18 |
19 | 20 | ##Rotating a view/layer > 180º 21 | 22 | animating the view's transform using a `CAValueFunction` of type "kCAValueFunctionRotateZ". 23 | 24 | 25 |
26 | 27 | ##Pausing and resuming an "in flight" animation on a layer. 28 | 29 | All of the animations in this demo can be paused by clicking a pause/continue button, stopped with a stop button, or "scrubbed" back and forth along their timeline by dragging on a slider. 30 | 31 | This works for `UIView` animations as well as animations you create using `CAAnimation` objects. It works because `UIView` aniamtions create `CAAnimation` objects "under the covers" to perform the requested animation. 32 | 33 | This is done by manipulating the layer's `speed`, `beginTime`, and `timeoffset` properties. 34 | 35 |
36 | ##Sleuthing the animations created by UIView animation methods 37 | 38 | It's possible to watch the animations that iOS creates when you use `UIView` animation methods like `animateWithDuration:animations:` or `animateKeyframesWithDuration:delay:options:animations:completion:`. Take a look at [this article on sleuthing UIView animations](Sleuthing UIView animations.md). -------------------------------------------------------------------------------- /Sleuthing UIView animations.md: -------------------------------------------------------------------------------- 1 | ##Tutorial: Sleuthing UIView animations: 2 | 3 | 4 | With early versions of iOS, UIView animations were pretty limited. Prior to iOS 4, you had to use `beginAnimations:context:` … `commitAnimations` to create `UIView` animations. 5 | 6 | For many types of animation, you had to resort to writing your own `CAAnimation` code and manipulating `CALayer`s instead of views. 7 | 8 | Apple has been gradually adding better and more powerful animation methods to the UIKit classes like `UIView`. 9 | Beginning with iOS 4, Apple added animations that took block parameters, in the form `animateWithDuration:animations:` (and variations) 10 | 11 | Since then, Apple has added view and view controller transitions, keyframe view animations, animations with spring dynamics, UIKit dynamics, and lots of other cool tricks. These higher-level animation methods are usually simpler to use and easier to understand than their Core Animation counterparts. 12 | 13 | Behind the scenes, though, these higher level animation methods are still creating `CAAnimation` objects and attaching them to your view's layers, along with custom code to make everything work correctly. 14 | 15 | Sometimes it's useful to see what's going on and how the UIKit makes the animations work. 16 | 17 | It is also possible to manipulate the under-the-covers `CAAnimation`s. You can pause or restart an animation that was created using `UIView` animation calls, or "scrub" it (a video editing term) backwards or forwards by manipulating the underlying `CAAnimation` objects and their settings. As it turns out you can also fix bugs in the animations that the system creates on your behalf (see below) before submitting them. 18 | 19 | It's actually pretty easy to make the animations the system creates visible. Credit for this technique goes to David Rönnqvist on Stack Overflow. 20 | 21 | Here is the technique I came up with, based on his idea: 22 | 23 | Create a custom subclass of the view class that you're animating. (e.g. if it's a `UIImageView`, create a custom subclass of `UIImageView`. If it's some other view type, make the base class that other type, or you can even subclass `UIView` directly.) 24 | 25 | In my example I'll use a view class of **DebugImageView** and a CALayer subclass of **DebugLayer**. You might want to add your company's initials (or your initials if you are an individual developer) as a prefix to the class name to avoid possible name conflicts. (e.g. for a company called "MyCompany", you might use the prefix "MC", making the custom UIImageView class name "**MCDebugImageView**" and your custom `CALayer` subclass "**MCDebugLayer**." I skip that step here for simplicity. 26 | 27 | In the .m for your custom UIView subclass, add the following method: 28 | 29 | ```Objective-C 30 | + (Class)layerClass 31 | { 32 | return [DebugLayer class]; 33 | } 34 | ``` 35 | 36 | This one method causes your custom view to use a custom layer type of DebugLayer as the backing layer for the view instead of a generic CALayer object. So any time you create a view of your custom type, it's layer will by a DebugLayer rather than a normal CALayer. 37 | 38 | 39 | In the examples below we'll be working with animations on a `UIImageView`, so that's the type of view for which we create a custom subclass. If you're debugging animations on other types of views, you may find that your view type uses a different type of backing layer than a CALayer. To figure out what type of layer a view takes, you would create a custom subclass of the view you were trying to animate, and then add a `+layerClass` class method like this to your custom `UIView` subclass: 40 | 41 | ```Objective-C 42 | + (Class)layerClass 43 | { 44 | Class myLayerClass = [super layerClass]; 45 | NSLog(@"The UIView subclass %@ uses a backing layer of class %@", [self superclass], myLayerClass); 46 | return myLayerClass; 47 | } 48 | ``` 49 | 50 | Then create a new class, DebugLayer (or MCDebugLayer), that inherits from CALayer (or the layer class used by your particular UIView, if you're animating a view type that uses a different kind of layer.) 51 | 52 | The DebugLayer header is very simple: 53 | 54 | 55 | ```Objective-C 56 | #import 57 | 58 | @interface DebugLayer : CALayer 59 | 60 | @end 61 | ``` 62 | 63 | In your **DebugLayer**'s .m file, add the following method: 64 | 65 | 66 | ```Objective-C 67 | - (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key 68 | { 69 | NSLog(@"Adding animation for key \"%@\". Animation = %@", key, anim); 70 | [super addAnimation: anim forKey: key]; 71 | } 72 | ``` 73 | 74 | Now, in your project's XIB file or Storyboard, anywhere you have a `UIImageView` object that you want to animate, select the view object in IB, display the identity inspector, and switch the object's class from it's base class to your custom subclass. In the demo below, we're working with animations added to a UIImageView, so we created a custom subclass of `UIImageView` called **DebugImageView**. 75 | 76 | Once you've added the classes **DebugImageView** class and **DebugLayer** class to your project, and changed the class of a UIImageView object in your XIB/Storyboard to DebugImageView, you will see a simple entry in the debug console for every animation that is added to your image view object. 77 | 78 | I originally did this because I was trying to track down an odd behavior with the new iOS 7 keyframe view animation method `animateKeyframesWithDuration:delay:options:animations:completion:`. That method uses a series of other method calls to the UIView class method `addKeyframeWithRelativeStartTime:relativeDuration:animations:` to add one or more keyframe animations to one or more views. 79 | 80 | Keyframe `UIView` animations end up creating one or more corresponding `CAKeyframeAnimation` objects and adding them to your view's layer, so when you run a keyframe view animation on a **DebugImageView**, you'll see an entry in the debug console listing one or more `CAKeyframeAnimation` objects being added to your view's layer. 81 | 82 | `CAKeyframeAnimation` animations can either take an array of key values or a `CGPath` (which is the Core Graphics object that backs a `UIBezierPath`. `CAKeyframeAnimation` is based on Core Foundation rather than `NSObject`.) If you create a `UIView` keyframe animation using `animateKeyframesWithDuration:delay:options:animations:completion:`, you will find that the system creates one or more `CAKeyframeAnimation` objects with an `NSArray` of `NSValue` objects in it's values property that correspond to the key values you specify, plus an array of time values in the `keyTimes` property. 83 | 84 | I first started sleuthing UIView keyframe animations when I was animating the position of a `UIImageView` using cubic calculation mode, which causes the image view to follow a curved path through the list of positions that I specified. I was getting some odd behavior in the path my animations followed. It looked like there was a bug in the resulting animations. For some of the points in the animation, my image view would bounce off the point rather than following a smooth curve through it. Exactly which points, and how many points, showed the odd behavior varied with the number of keyframes in the animation. 85 | 86 | I thus developed a **DebugLayer** class that logged all the keyframe values for every `CAKeyframeAnimation` that was submitted to the layer. 87 | 88 | For a keyframe `UIView` animation, the system creates a `CAKeyframeAnimation` that has an `NSArray` of `CGPoint` coordinates. The `CGPoint` values are packaged as `NSValue` objects so they can be contained in an `NSArray`. 89 | 90 | I found that for the keyframe values that showed the odd behavior, the system was submitting a duplicate entry in the values array. (Both the coordinates of the point in the values array and the value of the time entry in the `keyTimes` array were duplicated, although the floating point value for the entry in `keyTimes` was different by a very tiny amount, so I needed to make the logic check for keyTime values that were "really close" to the other value (I settled on checking for key time values that were within .0001 of each other.) 91 | 92 | I then went on to write code that detected the duplicate values in values & keyTimes and remove the duplicates. (I did that by creating mutable copies of the values and keyTimes arrays, and copying all the non-duplicate entries to the new array, then installing the "condensed" arrays back into the animation object before submitting to the system with a call to `[super addAnimation: anim forKey: key];`) 93 | 94 | Sure enough, when I remove duplicate keyframe entries from the values and keyTimes arrays, the odd bouncing effect goes away from position animations. 95 | 96 | I ended up with a custom **DebugLayer** class that will optionally log each animation that is added to it, and also optionally detect and/or remove duplicate keyframe/keyTime values. The code is driven by a set of compiler switches that lets me turn logging and fixes on or off at will. I also added compiler switches that let me turn cubic calculation mode on or off, turn off a rotation animation that also submitted at the same time, vary the number of steps in the keyframe animation. The switch definitions look like this: 97 | 98 | 99 | ```Objective-C 100 | #define K_USE_CUBIC_PACING 1 101 | #define K_ROTATE 1 102 | #define K_KEYFRAME_STEPS 6 103 | #define K_FIX_ANIMATION 1 104 | #define K_LOG_KEYFRAME_STEPS 0 105 | ``` 106 | 107 | The code for my custom **DebugLayer's** `addAnimation:forKey:` method has gotten pretty convoluted in order to handle the different compiler flags, but the concept is pretty straightforward. It would be easy to add code to log lots of different animation settings for the types of animations you are analyzing. 108 | 109 | 110 | --------------------------------------------------------------------------------