├── AnimationResult ├── AnimationTesting.xcodeproj │ └── project.pbxproj └── AnimationTesting │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Backup.swift │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift ├── Diff ├── Diff.xcodeproj │ └── project.pbxproj └── Diff │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Backup.swift │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── TimingFunctions.swift │ └── ViewController.swift ├── Duration ├── Duration.xcodeproj │ └── project.pbxproj └── Duration │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Backup.swift │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift └── UnitDuration ├── Duration.xcodeproj └── project.pbxproj └── Duration ├── AppDelegate.swift ├── Assets.xcassets └── AppIcon.appiconset │ └── Contents.json ├── Backup.swift ├── Base.lproj ├── LaunchScreen.storyboard └── Main.storyboard ├── Info.plist ├── TimingFunctions.swift └── ViewController.swift /AnimationResult/AnimationTesting.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 835262921F80FD5600039B50 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835262911F80FD5600039B50 /* AppDelegate.swift */; }; 11 | 835262941F80FD5600039B50 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835262931F80FD5600039B50 /* ViewController.swift */; }; 12 | 835262971F80FD5600039B50 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 835262951F80FD5600039B50 /* Main.storyboard */; }; 13 | 835262991F80FD5600039B50 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 835262981F80FD5600039B50 /* Assets.xcassets */; }; 14 | 8352629C1F80FD5600039B50 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */; }; 15 | 835262A41F8217D700039B50 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835262A31F8217D700039B50 /* Backup.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 8352628E1F80FD5600039B50 /* AnimationTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AnimationTesting.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 835262911F80FD5600039B50 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | 835262931F80FD5600039B50 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 22 | 835262961F80FD5600039B50 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23 | 835262981F80FD5600039B50 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | 8352629B1F80FD5600039B50 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 25 | 8352629D1F80FD5600039B50 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | 835262A31F8217D700039B50 /* Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backup.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 8352628B1F80FD5600039B50 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 835262851F80FD5600039B50 = { 41 | isa = PBXGroup; 42 | children = ( 43 | 835262901F80FD5600039B50 /* AnimationTesting */, 44 | 8352628F1F80FD5600039B50 /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 8352628F1F80FD5600039B50 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 8352628E1F80FD5600039B50 /* AnimationTesting.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 835262901F80FD5600039B50 /* AnimationTesting */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 835262911F80FD5600039B50 /* AppDelegate.swift */, 60 | 835262931F80FD5600039B50 /* ViewController.swift */, 61 | 835262951F80FD5600039B50 /* Main.storyboard */, 62 | 835262981F80FD5600039B50 /* Assets.xcassets */, 63 | 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */, 64 | 8352629D1F80FD5600039B50 /* Info.plist */, 65 | 835262A31F8217D700039B50 /* Backup.swift */, 66 | ); 67 | path = AnimationTesting; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | 8352628D1F80FD5600039B50 /* AnimationTesting */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = 835262A01F80FD5600039B50 /* Build configuration list for PBXNativeTarget "AnimationTesting" */; 76 | buildPhases = ( 77 | 8352628A1F80FD5600039B50 /* Sources */, 78 | 8352628B1F80FD5600039B50 /* Frameworks */, 79 | 8352628C1F80FD5600039B50 /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = AnimationTesting; 86 | productName = AnimationTesting; 87 | productReference = 8352628E1F80FD5600039B50 /* AnimationTesting.app */; 88 | productType = "com.apple.product-type.application"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 835262861F80FD5600039B50 /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastSwiftUpdateCheck = 0900; 97 | LastUpgradeCheck = 0900; 98 | ORGANIZATIONNAME = objc.io; 99 | TargetAttributes = { 100 | 8352628D1F80FD5600039B50 = { 101 | CreatedOnToolsVersion = 9.0; 102 | ProvisioningStyle = Automatic; 103 | }; 104 | }; 105 | }; 106 | buildConfigurationList = 835262891F80FD5600039B50 /* Build configuration list for PBXProject "AnimationTesting" */; 107 | compatibilityVersion = "Xcode 8.0"; 108 | developmentRegion = en; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | Base, 113 | ); 114 | mainGroup = 835262851F80FD5600039B50; 115 | productRefGroup = 8352628F1F80FD5600039B50 /* Products */; 116 | projectDirPath = ""; 117 | projectRoot = ""; 118 | targets = ( 119 | 8352628D1F80FD5600039B50 /* AnimationTesting */, 120 | ); 121 | }; 122 | /* End PBXProject section */ 123 | 124 | /* Begin PBXResourcesBuildPhase section */ 125 | 8352628C1F80FD5600039B50 /* Resources */ = { 126 | isa = PBXResourcesBuildPhase; 127 | buildActionMask = 2147483647; 128 | files = ( 129 | 8352629C1F80FD5600039B50 /* LaunchScreen.storyboard in Resources */, 130 | 835262991F80FD5600039B50 /* Assets.xcassets in Resources */, 131 | 835262971F80FD5600039B50 /* Main.storyboard in Resources */, 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXResourcesBuildPhase section */ 136 | 137 | /* Begin PBXSourcesBuildPhase section */ 138 | 8352628A1F80FD5600039B50 /* Sources */ = { 139 | isa = PBXSourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 835262A41F8217D700039B50 /* Backup.swift in Sources */, 143 | 835262941F80FD5600039B50 /* ViewController.swift in Sources */, 144 | 835262921F80FD5600039B50 /* AppDelegate.swift in Sources */, 145 | ); 146 | runOnlyForDeploymentPostprocessing = 0; 147 | }; 148 | /* End PBXSourcesBuildPhase section */ 149 | 150 | /* Begin PBXVariantGroup section */ 151 | 835262951F80FD5600039B50 /* Main.storyboard */ = { 152 | isa = PBXVariantGroup; 153 | children = ( 154 | 835262961F80FD5600039B50 /* Base */, 155 | ); 156 | name = Main.storyboard; 157 | sourceTree = ""; 158 | }; 159 | 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */ = { 160 | isa = PBXVariantGroup; 161 | children = ( 162 | 8352629B1F80FD5600039B50 /* Base */, 163 | ); 164 | name = LaunchScreen.storyboard; 165 | sourceTree = ""; 166 | }; 167 | /* End PBXVariantGroup section */ 168 | 169 | /* Begin XCBuildConfiguration section */ 170 | 8352629E1F80FD5600039B50 /* Debug */ = { 171 | isa = XCBuildConfiguration; 172 | buildSettings = { 173 | ALWAYS_SEARCH_USER_PATHS = NO; 174 | CLANG_ANALYZER_NONNULL = YES; 175 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 176 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 177 | CLANG_CXX_LIBRARY = "libc++"; 178 | CLANG_ENABLE_MODULES = YES; 179 | CLANG_ENABLE_OBJC_ARC = YES; 180 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_COMMA = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 185 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 186 | CLANG_WARN_EMPTY_BODY = YES; 187 | CLANG_WARN_ENUM_CONVERSION = YES; 188 | CLANG_WARN_INFINITE_RECURSION = YES; 189 | CLANG_WARN_INT_CONVERSION = YES; 190 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 191 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 193 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 194 | CLANG_WARN_STRICT_PROTOTYPES = YES; 195 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 196 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 197 | CLANG_WARN_UNREACHABLE_CODE = YES; 198 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 199 | CODE_SIGN_IDENTITY = "iPhone Developer"; 200 | COPY_PHASE_STRIP = NO; 201 | DEBUG_INFORMATION_FORMAT = dwarf; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | ENABLE_TESTABILITY = YES; 204 | GCC_C_LANGUAGE_STANDARD = gnu11; 205 | GCC_DYNAMIC_NO_PIC = NO; 206 | GCC_NO_COMMON_BLOCKS = YES; 207 | GCC_OPTIMIZATION_LEVEL = 0; 208 | GCC_PREPROCESSOR_DEFINITIONS = ( 209 | "DEBUG=1", 210 | "$(inherited)", 211 | ); 212 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 213 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 214 | GCC_WARN_UNDECLARED_SELECTOR = YES; 215 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 216 | GCC_WARN_UNUSED_FUNCTION = YES; 217 | GCC_WARN_UNUSED_VARIABLE = YES; 218 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 219 | MTL_ENABLE_DEBUG_INFO = YES; 220 | ONLY_ACTIVE_ARCH = YES; 221 | SDKROOT = iphoneos; 222 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 223 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 224 | }; 225 | name = Debug; 226 | }; 227 | 8352629F1F80FD5600039B50 /* Release */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | ALWAYS_SEARCH_USER_PATHS = NO; 231 | CLANG_ANALYZER_NONNULL = YES; 232 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 233 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 234 | CLANG_CXX_LIBRARY = "libc++"; 235 | CLANG_ENABLE_MODULES = YES; 236 | CLANG_ENABLE_OBJC_ARC = YES; 237 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 238 | CLANG_WARN_BOOL_CONVERSION = YES; 239 | CLANG_WARN_COMMA = YES; 240 | CLANG_WARN_CONSTANT_CONVERSION = YES; 241 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 242 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 243 | CLANG_WARN_EMPTY_BODY = YES; 244 | CLANG_WARN_ENUM_CONVERSION = YES; 245 | CLANG_WARN_INFINITE_RECURSION = YES; 246 | CLANG_WARN_INT_CONVERSION = YES; 247 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 251 | CLANG_WARN_STRICT_PROTOTYPES = YES; 252 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 254 | CLANG_WARN_UNREACHABLE_CODE = YES; 255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 256 | CODE_SIGN_IDENTITY = "iPhone Developer"; 257 | COPY_PHASE_STRIP = NO; 258 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 259 | ENABLE_NS_ASSERTIONS = NO; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | GCC_C_LANGUAGE_STANDARD = gnu11; 262 | GCC_NO_COMMON_BLOCKS = YES; 263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 265 | GCC_WARN_UNDECLARED_SELECTOR = YES; 266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 267 | GCC_WARN_UNUSED_FUNCTION = YES; 268 | GCC_WARN_UNUSED_VARIABLE = YES; 269 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 270 | MTL_ENABLE_DEBUG_INFO = NO; 271 | SDKROOT = iphoneos; 272 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 273 | VALIDATE_PRODUCT = YES; 274 | }; 275 | name = Release; 276 | }; 277 | 835262A11F80FD5600039B50 /* Debug */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 281 | CODE_SIGN_STYLE = Automatic; 282 | DEVELOPMENT_TEAM = SL7736T2Z4; 283 | INFOPLIST_FILE = AnimationTesting/Info.plist; 284 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 285 | PRODUCT_BUNDLE_IDENTIFIER = io.objcio.AnimationTesting; 286 | PRODUCT_NAME = "$(TARGET_NAME)"; 287 | SWIFT_VERSION = 4.0; 288 | TARGETED_DEVICE_FAMILY = "1,2"; 289 | }; 290 | name = Debug; 291 | }; 292 | 835262A21F80FD5600039B50 /* Release */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 296 | CODE_SIGN_STYLE = Automatic; 297 | DEVELOPMENT_TEAM = SL7736T2Z4; 298 | INFOPLIST_FILE = AnimationTesting/Info.plist; 299 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 300 | PRODUCT_BUNDLE_IDENTIFIER = io.objcio.AnimationTesting; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | SWIFT_VERSION = 4.0; 303 | TARGETED_DEVICE_FAMILY = "1,2"; 304 | }; 305 | name = Release; 306 | }; 307 | /* End XCBuildConfiguration section */ 308 | 309 | /* Begin XCConfigurationList section */ 310 | 835262891F80FD5600039B50 /* Build configuration list for PBXProject "AnimationTesting" */ = { 311 | isa = XCConfigurationList; 312 | buildConfigurations = ( 313 | 8352629E1F80FD5600039B50 /* Debug */, 314 | 8352629F1F80FD5600039B50 /* Release */, 315 | ); 316 | defaultConfigurationIsVisible = 0; 317 | defaultConfigurationName = Release; 318 | }; 319 | 835262A01F80FD5600039B50 /* Build configuration list for PBXNativeTarget "AnimationTesting" */ = { 320 | isa = XCConfigurationList; 321 | buildConfigurations = ( 322 | 835262A11F80FD5600039B50 /* Debug */, 323 | 835262A21F80FD5600039B50 /* Release */, 324 | ); 325 | defaultConfigurationIsVisible = 0; 326 | defaultConfigurationName = Release; 327 | }; 328 | /* End XCConfigurationList section */ 329 | }; 330 | rootObject = 835262861F80FD5600039B50 /* Project object */; 331 | } 332 | -------------------------------------------------------------------------------- /AnimationResult/AnimationTesting/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AnimationTesting 4 | // 5 | // Created by Chris Eidhof on 01.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /AnimationResult/AnimationTesting/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /AnimationResult/AnimationTesting/Backup.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ViewController.swift 3 | //// AnimationTesting 4 | //// 5 | //// Created by Chris Eidhof on 01.10.17. 6 | //// Copyright © 2017 objc.io. All rights reserved. 7 | //// 8 | // 9 | //import UIKit 10 | // 11 | ////: Playground - noun: a place where people can play 12 | // 13 | //import UIKit 14 | // 15 | //struct Animation { 16 | // let value: (RelativeTime) -> A 17 | // 18 | // init(_ value: @escaping (RelativeTime) -> A) { 19 | // self.value = value 20 | // } 21 | // 22 | // init(constant: A) { 23 | // self.value = { _ in constant } 24 | // } 25 | // 26 | //} 27 | // 28 | //extension Animation where A == () { 29 | // static let one = Animation<()>() 30 | // 31 | // init() { 32 | // self.value = { _ in () } 33 | // } 34 | // 35 | //} 36 | // 37 | //typealias Progress = Double 38 | // 39 | //typealias AnimationCurve = (Progress) -> Progress 40 | // 41 | //extension Double { 42 | // func clamped(to: ClosedRange) -> Double { 43 | // if self < to.lowerBound { return to.lowerBound } 44 | // if self > to.upperBound { return to.upperBound } 45 | // return self 46 | // } 47 | //} 48 | //func linear(_ progress: Progress) -> Progress { 49 | // return progress 50 | //} 51 | // 52 | //func quadratic(_ progress: Progress) -> Progress { 53 | // return Progress(progress * progress) 54 | //} 55 | // 56 | //func cubic(_ progress: Progress) -> Progress { 57 | // return Progress(progress * progress * progress) 58 | //} 59 | // 60 | //func spring(damping b: Double = 10, mass m: Double = 1, stiffness k: Double = 100, velocity v0: Double = 0) -> AnimationCurve { 61 | // let beta: Double = b / (2*m) 62 | // let omega0: Double = sqrt(k/m) 63 | // let omega1: Double = sqrt((omega0 * omega0) - (beta * beta)) 64 | // let omega2: Double = sqrt((beta * beta) - (omega0 * omega0)) 65 | // 66 | // let x0: Double = -1 67 | // //if (!self.allowsOverdamping && beta > omega0) beta = omega0; 68 | // if beta < omega0 { 69 | // return { t in 70 | // let envelope: Double = exp(-beta * t) 71 | // return -x0 + envelope * (x0 * cos(omega1 * t) + ((beta * x0 + v0) / omega1) * sin(omega1 * t)) 72 | // } 73 | // } else if (beta == omega0) { 74 | // return { t in 75 | // let envelope = exp(-beta * t) 76 | // return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 77 | // } 78 | // } else { 79 | // // Overdamped 80 | // return { t in 81 | // let envelope = exp(-beta * t); 82 | // return -x0 + envelope * (x0 * cosh(omega2 * t) + ((beta * x0 + v0) / omega2) * sinh(omega2 * t)); 83 | // } 84 | // } 85 | //} 86 | // 87 | //extension Animation where A == CGFloat { 88 | // init(from start: CGFloat, speed: CGFloat) { 89 | // value = { time in 90 | // return start + CGFloat(time)*speed 91 | // } 92 | // } 93 | // 94 | //} 95 | // 96 | //extension Animation { 97 | // func parallel(_ other: Animation, _ transform: @escaping (A,B) -> C) -> Animation { 98 | // return Animation({ time in 99 | // let a = self.value(time) 100 | // let b = other.value(time) 101 | // return transform(a,b) 102 | // }) 103 | // } 104 | //} 105 | // 106 | //// RelativeTime is never less than zero 107 | //typealias RelativeTime = CFAbsoluteTime 108 | // 109 | //// Parallel Composition 110 | //func +(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 111 | // return lhs.parallel(rhs) { ($0, $1) } 112 | //} 113 | // 114 | //// Sequential Composition 115 | ////func *(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 116 | //// return Animation({ time in 117 | //// let a = lhs.value(time) 118 | //// let b = rhs.value(time-lhs.duration) 119 | //// return (a,b) 120 | //// }) 121 | ////} 122 | // 123 | ////extension Animation { 124 | //// func delay(by: RelativeTime) -> Animation { 125 | //// return (Animation<()>(duration: by) * self).map { $0.1 } 126 | //// } 127 | ////} 128 | // 129 | //extension Animation { 130 | // func map(_ f: @escaping (A) -> B) -> Animation { 131 | // return Animation({ f(self.value($0))}) 132 | // } 133 | // 134 | // func mapp(_ f: @escaping (RelativeTime, A) -> B) -> Animation { 135 | // return Animation { time in 136 | // f(time, self.value(time)) 137 | // } 138 | // } 139 | // 140 | //} 141 | // 142 | //extension Animation where A == CGFloat { 143 | // func addSpeed(_ speed: A) -> Animation { 144 | // return mapp { time, value in value + CGFloat(time)*speed } 145 | // } 146 | // 147 | // init(from: CGFloat, to: CGFloat, duration: TimeInterval, curve: @escaping AnimationCurve = linear) { 148 | // let speed = (to-from)/CGFloat(duration) 149 | // self = Animation(constant: from).addSpeed(speed).after(duration: duration, switchTo: { endValue in Animation(constant: endValue) }).map { $0.value }.augmentCurve(curve: { time in curve(time/duration)}) 150 | // } 151 | //} 152 | // 153 | //extension Animation { 154 | // // func changeSpeed(factor: RelativeTime) -> Animation { 155 | // // return Animation(duration: duration / factor, { time in 156 | // // self.value(time * factor) 157 | // // }) 158 | // // } 159 | // // 160 | // // var halfSpeed: Animation { 161 | // // return changeSpeed(factor: 0.5) 162 | // // } 163 | // 164 | // fileprivate func augmentCurve(curve: @escaping AnimationCurve) -> Animation { 165 | // return Animation { time in 166 | // return self.value(curve(time)) 167 | // } 168 | // } 169 | //} 170 | // 171 | //enum Either { 172 | // case left(A) 173 | // case right(B) 174 | //} 175 | // 176 | //extension Animation { 177 | // func after(duration: RelativeTime, switchTo other: Animation) -> Animation> { 178 | // return Animation> { time in 179 | // if time < duration { return .left(self.value(time)) } 180 | // else { return .right(other.value(time)) } 181 | // } 182 | // } 183 | // 184 | // func after(duration: RelativeTime, switchTo other: @escaping (A) -> Animation) -> Animation> { 185 | // return Animation> { time in 186 | // if time < duration { 187 | // return .left(self.value(time)) 188 | // } 189 | // else { 190 | // let otherValue = other(self.value(duration)) 191 | // return .right(otherValue.value(time-duration)) 192 | // } 193 | // } 194 | // } 195 | // 196 | //} 197 | // 198 | // 199 | // 200 | //final class Driver: NSObject { 201 | // var displayLink: CADisplayLink! 202 | // var animate: ((CFAbsoluteTime) -> ())! 203 | // 204 | // init(animation: Animation, _ interpret: @escaping (A) -> ()) { 205 | // super.init() 206 | // displayLink = CADisplayLink(target: self, selector: #selector(step(_:))) 207 | // displayLink.add(to: RunLoop.main, forMode: .commonModes) 208 | // var startTime: CFAbsoluteTime? = nil 209 | // self.animate = { [unowned self] nextTime in 210 | // if startTime == nil { startTime = nextTime } 211 | // let time = nextTime - startTime! 212 | // let value = animation.value(time) 213 | // interpret(value) 214 | // // if time >= animation.duration { 215 | // //// print((time, animation.duration)) 216 | // //// self.displayLink.isPaused = true 217 | // // } 218 | // } 219 | // } 220 | // 221 | // 222 | // 223 | // @objc func step(_ displayLink: CADisplayLink) { 224 | // animate(displayLink.targetTimestamp) 225 | // } 226 | //} 227 | // 228 | //extension Either where A == B { 229 | // var value: A { 230 | // switch self { 231 | // case .left(let x): return x 232 | // case .right(let y): return y 233 | // } 234 | // } 235 | //} 236 | // 237 | //class ViewController: UIViewController { 238 | // var driver: Driver? 239 | // 240 | // @IBOutlet weak var redBox: UIView! 241 | // override func viewDidLoad() { 242 | // super.viewDidLoad() 243 | // CATransaction.setDisableActions(true) 244 | // 245 | // redBox.frame.origin = .zero 246 | // let singlePoint = Animation(from: 0, to: 100, duration: 2).after(duration: 4, switchTo: { end in Animation(from: end, to: 0, duration: 2) }).map{ $0.value } 247 | // let other = singlePoint.augmentCurve(curve: { t in spring()(t / 4) } ) 248 | // let point = (singlePoint + other).map { CGPoint(x: $0.0, y: $0.1 )} 249 | // let origin = point 250 | // let animation = (origin).map { result in 251 | // CGAffineTransform(translationX: result.x, y: result.y) 252 | // } 253 | // 254 | // driver = Driver(animation: animation, { [unowned self] transform in 255 | // self.redBox.transform = transform 256 | // }) 257 | // } 258 | // 259 | // override func didReceiveMemoryWarning() { 260 | // super.didReceiveMemoryWarning() 261 | // // Dispose of any resources that can be recreated. 262 | // } 263 | // 264 | // 265 | //} 266 | // 267 | // 268 | -------------------------------------------------------------------------------- /AnimationResult/AnimationTesting/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /AnimationResult/AnimationTesting/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /AnimationResult/AnimationTesting/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /AnimationResult/AnimationTesting/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // AnimationTesting 4 | // 5 | // Created by Chris Eidhof on 01.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum Either { 12 | case left(A) 13 | case right(B) 14 | } 15 | 16 | extension Either where A == B { 17 | var value: A { 18 | switch self { 19 | case .left(let x): return x 20 | case .right(let y): return y 21 | } 22 | } 23 | } 24 | 25 | enum AnimationResult { 26 | case done(A, at: RelativeTime) 27 | case inProgress(A) 28 | 29 | var value: A { 30 | switch self { 31 | case let .done(x,_): return x 32 | case .inProgress(let x): return x 33 | } 34 | } 35 | 36 | func zip(_ other: AnimationResult, with: (A,B) -> C) -> AnimationResult { 37 | switch (self,other) { 38 | case let (.done(x, t1), .done(y, t2)): return .done(with(x,y), at: max(t1, t2)) 39 | default: return .inProgress(with(self.value, other.value)) 40 | } 41 | } 42 | 43 | func map(_ transform: (A) -> B) -> AnimationResult { 44 | switch self { 45 | case let .done(x, at: time): return .done(transform(x), at: time) 46 | case .inProgress(let x): return .inProgress(transform(x)) 47 | } 48 | } 49 | } 50 | 51 | struct Animation { 52 | let value: (RelativeTime) -> AnimationResult 53 | 54 | init(_ value: @escaping (RelativeTime) -> AnimationResult) { 55 | self.value = value 56 | } 57 | 58 | init(constant: A) { 59 | self.value = { _ in .done(constant, at: 0) } 60 | } 61 | 62 | init(inProgress constant: A) { 63 | self.value = { _ in .inProgress(constant) } 64 | } 65 | 66 | } 67 | 68 | extension Animation where A == () { 69 | // static let one = Animation<()>(.done(())) 70 | 71 | init() { 72 | self.init(constant: ()) 73 | } 74 | 75 | } 76 | 77 | typealias Progress = Double 78 | 79 | typealias AnimationCurve = (Progress) -> Progress 80 | 81 | extension Double { 82 | func clamped(to: ClosedRange) -> Double { 83 | if self < to.lowerBound { return to.lowerBound } 84 | if self > to.upperBound { return to.upperBound } 85 | return self 86 | } 87 | } 88 | 89 | struct TimingFunction { 90 | enum Builtin { 91 | case linear 92 | case easeIn 93 | case easeOut 94 | case easeInEaseOut 95 | case `default` 96 | 97 | var controlPoints: (Double,Double,Double,Double) { 98 | switch self { 99 | case .linear: 100 | return (0, 0, 1, 1) 101 | case .easeIn: 102 | return (0.42, 0, 1, 1) 103 | case .easeOut: 104 | return (0, 0, 0.58, 1) 105 | case .easeInEaseOut: 106 | return (0.42, 0, 0.58, 1) 107 | case .`default`: 108 | return (0.25, 0.1, 0.25, 1) 109 | } 110 | } 111 | } 112 | 113 | 114 | // Taken from https://gist.github.com/raphaelschaad/6739676 115 | 116 | private var ax,bx,cx,ay,by,cy: Double 117 | init(controlPoints p1x: Double, _ p1y: Double, _ p2x: Double, _ p2y: Double) { 118 | cx = 3.0 * p1x 119 | bx = 3.0 * (p2x - p1x) - cx 120 | ax = 1.0 - cx - bx 121 | 122 | cy = 3.0 * p1y 123 | by = 3.0 * (p2y - p1y) - cy 124 | ay = 1.0 - cy - by 125 | } 126 | 127 | init(type: Builtin = .`default`) { 128 | let (p1, p2, p3, p4) = type.controlPoints 129 | self.init(controlPoints: p1, p2, p3, p4) 130 | } 131 | 132 | func value(x: Double) -> Double { 133 | let xSolved = solve(curveX: x, epsilon: epsilon) 134 | return sampleCurveY(t: xSolved) 135 | } 136 | 137 | let duration: Double = 1 138 | 139 | private var epsilon: Double { 140 | return 1 / (200*duration) 141 | } 142 | 143 | 144 | private func sampleCurveX(t: Double) -> Double { 145 | return ((ax * t + bx) * t + cx) * t 146 | } 147 | 148 | private func sampleCurveY(t: Double) -> Double { 149 | return ((ay * t + by) * t + cy) * t; 150 | } 151 | 152 | private func sampleCurveDerivativeX(t: Double) -> Double { 153 | return (3.0 * ax * t + 2.0 * bx) * t + cx 154 | } 155 | 156 | private func solve(curveX x: Double, epsilon: Double) -> Double { 157 | var t2, x2, d2: Double 158 | 159 | // First try a few iterations of Newton's method -- normally very fast. 160 | t2 = x 161 | for _ in 0..<8 { 162 | x2 = sampleCurveX(t: t2) - x 163 | if (fabs(x2) < epsilon) { 164 | return t2 165 | } 166 | d2 = sampleCurveDerivativeX(t: t2) 167 | if (fabs(d2) < 1e-6) { 168 | break 169 | } 170 | t2 = t2 - x2 / d2; 171 | } 172 | 173 | // Fall back to the bisection method for reliability. 174 | var t0: Double = 0.0 175 | var t1: Double = 1.0 176 | t2 = x 177 | 178 | if (t2 < t0) { 179 | return t0 180 | } 181 | if (t2 > t1) { 182 | return t1 183 | } 184 | 185 | while (t0 < t1) { 186 | x2 = sampleCurveX(t: t2) 187 | if (fabs(x2 - x) < epsilon) { 188 | return t2 189 | } 190 | if (x > x2) { 191 | t0 = t2 192 | } else { 193 | t1 = t2 194 | } 195 | t2 = (t1 - t0) * 0.5 + t0 196 | } 197 | 198 | // Failure. 199 | return t2 200 | } 201 | } 202 | 203 | func linear(_ progress: Progress) -> Progress { 204 | return progress 205 | } 206 | 207 | func quadratic(_ progress: Progress) -> Progress { 208 | return Progress(progress * progress) 209 | } 210 | 211 | func cubic(_ progress: Progress) -> Progress { 212 | return Progress(progress * progress * progress) 213 | } 214 | 215 | func builtin(_ b: TimingFunction.Builtin) -> AnimationCurve { 216 | let tf = TimingFunction(type: b) 217 | return tf.value 218 | } 219 | 220 | func spring(damping b: Double = 10, mass m: Double = 1, stiffness k: Double = 100, velocity v0: Double = 0) -> AnimationCurve { 221 | let beta: Double = b / (2*m) 222 | let omega0: Double = sqrt(k/m) 223 | let omega1: Double = sqrt((omega0 * omega0) - (beta * beta)) 224 | let omega2: Double = sqrt((beta * beta) - (omega0 * omega0)) 225 | 226 | let x0: Double = -1 227 | //if (!self.allowsOverdamping && beta > omega0) beta = omega0; 228 | if beta < omega0 { 229 | return { t in 230 | let envelope: Double = exp(-beta * t) 231 | return -x0 + envelope * (x0 * cos(omega1 * t) + ((beta * x0 + v0) / omega1) * sin(omega1 * t)) 232 | } 233 | } else if (beta == omega0) { 234 | return { t in 235 | let envelope = exp(-beta * t) 236 | return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 237 | } 238 | } else { 239 | // Overdamped 240 | return { t in 241 | let envelope = exp(-beta * t); 242 | return -x0 + envelope * (x0 * cosh(omega2 * t) + ((beta * x0 + v0) / omega2) * sinh(omega2 * t)); 243 | } 244 | } 245 | } 246 | 247 | extension Animation { 248 | func parallel(_ other: Animation, _ transform: @escaping (A,B) -> C) -> Animation { 249 | return Animation { time in 250 | let a = self.value(time) 251 | let b = other.value(time) 252 | return a.zip(b, with: transform) 253 | } 254 | } 255 | 256 | func sequential(_ other: @escaping (A) -> Animation) -> Animation> { 257 | return Animation> { time in 258 | let a = self.value(time) 259 | switch a { 260 | case let .done(value, finishTime): 261 | return other(value).value(time-finishTime).map(Either.right) 262 | case .inProgress: 263 | return a.map(Either.left) 264 | } 265 | 266 | } 267 | 268 | } 269 | } 270 | 271 | // RelativeTime is never less than zero 272 | typealias RelativeTime = CFAbsoluteTime 273 | 274 | // Parallel Composition 275 | func +(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 276 | return lhs.parallel(rhs) { ($0, $1) } 277 | } 278 | 279 | // Sequential Composition 280 | func *(lhs: Animation, rhs: Animation) -> Animation> { 281 | return lhs.sequential { _ in rhs } 282 | } 283 | 284 | func *(lhs: Animation, rhs: Animation) -> Animation { 285 | return (lhs * rhs).map { $0.value } 286 | } 287 | 288 | extension Animation { 289 | func map(_ f: @escaping (A) -> B) -> Animation { 290 | return Animation { self.value($0).map(f) } 291 | } 292 | 293 | func mapWithTime(_ f: @escaping (RelativeTime, A) -> B) -> Animation { 294 | return Animation { time in 295 | return self.value(time).map { f(time,$0) } 296 | } 297 | } 298 | 299 | func stop(after endTime: RelativeTime) -> Animation { 300 | return Animation { time in 301 | if time >= endTime { 302 | return .done(self.value(endTime).value, at: endTime) 303 | } else { 304 | return self.value(time) 305 | } 306 | } 307 | } 308 | 309 | } 310 | 311 | extension Animation where A == CGFloat { 312 | func addSpeed(_ speed: A) -> Animation { 313 | return mapWithTime { time, value in 314 | return value + CGFloat(time)*speed 315 | } 316 | } 317 | 318 | init(from: CGFloat, to: CGFloat, duration: TimeInterval, curve: @escaping AnimationCurve = linear) { 319 | // todo curve 320 | let speed = (to-from)/CGFloat(duration) 321 | self = Animation(inProgress: from).addSpeed(speed).stop(after: duration).augmentCurve(curve: curve, duration: duration).stop(after: duration) // the double stop isn't nice :( 322 | } 323 | } 324 | 325 | extension Animation { 326 | func changeSpeed(factor: RelativeTime) -> Animation { 327 | return Animation { time in 328 | self.value(time * factor) 329 | } 330 | } 331 | 332 | var halfSpeed: Animation { 333 | return changeSpeed(factor: 0.5) 334 | } 335 | 336 | var doubleSpeed: Animation { 337 | return changeSpeed(factor: 2) 338 | } 339 | 340 | func delay(by: RelativeTime, initialValue: A) -> Animation { 341 | return Animation(inProgress: initialValue).stop(after: by) * self 342 | } 343 | 344 | fileprivate func augmentCurve(curve: @escaping AnimationCurve, duration: RelativeTime = 1.0) -> Animation { 345 | return Animation { time in 346 | self.value(curve(time/duration)) 347 | } 348 | } 349 | } 350 | 351 | struct InterpretedAnimation { 352 | enum State { case running, done } 353 | let run: (RelativeTime) -> State 354 | 355 | init(animation: Animation, interpret: @escaping (A) -> ()) { 356 | run = { 357 | let result = animation.value($0) 358 | interpret(result.value) 359 | if case .done = result { return .done } 360 | return .running 361 | } 362 | } 363 | } 364 | 365 | final class Driver: NSObject { 366 | var displayLink: CADisplayLink! 367 | var animations: [(startTime: CFAbsoluteTime, InterpretedAnimation)] = [] 368 | var pendingAnimations: [InterpretedAnimation] = [] 369 | 370 | override init() { 371 | super.init() 372 | displayLink = CADisplayLink(target: self, selector: #selector(step(_:))) 373 | displayLink.add(to: RunLoop.main, forMode: .commonModes) 374 | } 375 | 376 | func add(_ animation: InterpretedAnimation) { 377 | pendingAnimations.append(animation) 378 | displayLink.isPaused = false 379 | } 380 | 381 | @objc func step(_ displayLink: CADisplayLink) { 382 | let time = displayLink.targetTimestamp 383 | for p in pendingAnimations { 384 | animations.append((startTime: time, p)) 385 | } 386 | pendingAnimations = [] 387 | var toBeRemoved: [Int] = [] 388 | for ((startTime: startTime, animation), index) in zip(animations, animations.indices) { 389 | let animationTime = time - startTime 390 | let result = animation.run(animationTime) 391 | if result == .done { 392 | toBeRemoved.append(index) 393 | } 394 | } 395 | for i in toBeRemoved.reversed() { 396 | animations.remove(at: i) 397 | } 398 | if animations.isEmpty { 399 | displayLink.isPaused = true 400 | } 401 | } 402 | 403 | deinit { 404 | displayLink.remove(from: .main, forMode: .commonModes) 405 | } 406 | } 407 | 408 | func dot(origin: CGPoint, size: CGSize = CGSize(width: 30, height: 30), backgroundColor: UIColor = .orange) -> UIView { 409 | let result = UIView(frame: CGRect(origin: origin, size: size)) 410 | result.backgroundColor = backgroundColor 411 | result.layer.cornerRadius = size.width/2 412 | result.layer.masksToBounds = true 413 | return result 414 | } 415 | 416 | class ViewController: UIViewController { 417 | var driver: Driver = Driver() 418 | 419 | @IBOutlet weak var redBox: UIView! 420 | override func viewDidLoad() { 421 | super.viewDidLoad() 422 | 423 | CATransaction.setDisableActions(true) 424 | } 425 | 426 | override func viewDidAppear(_ animated: Bool) { 427 | let dots = (0..<5).map { 428 | dot(origin: CGPoint(x: -15, y: 40 * (1 + $0))) 429 | } 430 | 431 | let rightDots = (0..<5).map { 432 | dot(origin: CGPoint(x: view.bounds.width-15, y: 40 * (1 + CGFloat($0)))) 433 | } 434 | 435 | func makeAnimation(from: CGFloat, to: CGFloat) -> Animation { 436 | return Animation(from: from, 437 | to: from + (to-from)*1.2, 438 | duration: 0.37, 439 | curve: builtin(.easeIn)).sequential { previous in 440 | Animation(from: previous, to: to, duration: 0.63, curve: builtin(.easeOut)) 441 | }.map { $0.value } 442 | } 443 | 444 | for (index, dot) in dots.enumerated() { 445 | view.addSubview(dot) 446 | let animation = makeAnimation(from: 0, to: 30).delay(by: Double(index) * 0.1, initialValue: 0) 447 | 448 | driver.pendingAnimations.append(InterpretedAnimation(animation: animation, interpret: { value in 449 | dot.transform = CGAffineTransform(translationX: value, y: 0) 450 | })) 451 | } 452 | 453 | 454 | for (index, dot) in rightDots.enumerated() { 455 | view.addSubview(dot) 456 | let animation = makeAnimation(from: 0, to: -30).changeSpeed(factor: 1-(0.1*Double(index))) 457 | driver.add(InterpretedAnimation(animation: animation, interpret: { x in 458 | dot.transform = CGAffineTransform(translationX: x, y: 0) 459 | })) 460 | } 461 | 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /Diff/Diff.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 833804F21F868AA500546301 /* TimingFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833804F11F868AA500546301 /* TimingFunctions.swift */; }; 11 | 835262921F80FD5600039B50 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835262911F80FD5600039B50 /* AppDelegate.swift */; }; 12 | 835262941F80FD5600039B50 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835262931F80FD5600039B50 /* ViewController.swift */; }; 13 | 835262971F80FD5600039B50 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 835262951F80FD5600039B50 /* Main.storyboard */; }; 14 | 835262991F80FD5600039B50 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 835262981F80FD5600039B50 /* Assets.xcassets */; }; 15 | 8352629C1F80FD5600039B50 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 833804F11F868AA500546301 /* TimingFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimingFunctions.swift; sourceTree = ""; }; 20 | 8352628E1F80FD5600039B50 /* Diff.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Diff.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 835262911F80FD5600039B50 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 835262931F80FD5600039B50 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | 835262961F80FD5600039B50 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 835262981F80FD5600039B50 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 8352629B1F80FD5600039B50 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | 8352629D1F80FD5600039B50 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 8352628B1F80FD5600039B50 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 835262851F80FD5600039B50 = { 41 | isa = PBXGroup; 42 | children = ( 43 | 835262901F80FD5600039B50 /* Diff */, 44 | 8352628F1F80FD5600039B50 /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 8352628F1F80FD5600039B50 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 8352628E1F80FD5600039B50 /* Diff.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 835262901F80FD5600039B50 /* Diff */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 835262911F80FD5600039B50 /* AppDelegate.swift */, 60 | 833804F11F868AA500546301 /* TimingFunctions.swift */, 61 | 835262931F80FD5600039B50 /* ViewController.swift */, 62 | 835262951F80FD5600039B50 /* Main.storyboard */, 63 | 835262981F80FD5600039B50 /* Assets.xcassets */, 64 | 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */, 65 | 8352629D1F80FD5600039B50 /* Info.plist */, 66 | ); 67 | path = Diff; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | 8352628D1F80FD5600039B50 /* Diff */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = 835262A01F80FD5600039B50 /* Build configuration list for PBXNativeTarget "Diff" */; 76 | buildPhases = ( 77 | 8352628A1F80FD5600039B50 /* Sources */, 78 | 8352628B1F80FD5600039B50 /* Frameworks */, 79 | 8352628C1F80FD5600039B50 /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = Diff; 86 | productName = AnimationTesting; 87 | productReference = 8352628E1F80FD5600039B50 /* Diff.app */; 88 | productType = "com.apple.product-type.application"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 835262861F80FD5600039B50 /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastSwiftUpdateCheck = 0900; 97 | LastUpgradeCheck = 0900; 98 | ORGANIZATIONNAME = objc.io; 99 | TargetAttributes = { 100 | 8352628D1F80FD5600039B50 = { 101 | CreatedOnToolsVersion = 9.0; 102 | ProvisioningStyle = Automatic; 103 | }; 104 | }; 105 | }; 106 | buildConfigurationList = 835262891F80FD5600039B50 /* Build configuration list for PBXProject "Diff" */; 107 | compatibilityVersion = "Xcode 8.0"; 108 | developmentRegion = en; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | Base, 113 | ); 114 | mainGroup = 835262851F80FD5600039B50; 115 | productRefGroup = 8352628F1F80FD5600039B50 /* Products */; 116 | projectDirPath = ""; 117 | projectRoot = ""; 118 | targets = ( 119 | 8352628D1F80FD5600039B50 /* Diff */, 120 | ); 121 | }; 122 | /* End PBXProject section */ 123 | 124 | /* Begin PBXResourcesBuildPhase section */ 125 | 8352628C1F80FD5600039B50 /* Resources */ = { 126 | isa = PBXResourcesBuildPhase; 127 | buildActionMask = 2147483647; 128 | files = ( 129 | 8352629C1F80FD5600039B50 /* LaunchScreen.storyboard in Resources */, 130 | 835262991F80FD5600039B50 /* Assets.xcassets in Resources */, 131 | 835262971F80FD5600039B50 /* Main.storyboard in Resources */, 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXResourcesBuildPhase section */ 136 | 137 | /* Begin PBXSourcesBuildPhase section */ 138 | 8352628A1F80FD5600039B50 /* Sources */ = { 139 | isa = PBXSourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 833804F21F868AA500546301 /* TimingFunctions.swift in Sources */, 143 | 835262941F80FD5600039B50 /* ViewController.swift in Sources */, 144 | 835262921F80FD5600039B50 /* AppDelegate.swift in Sources */, 145 | ); 146 | runOnlyForDeploymentPostprocessing = 0; 147 | }; 148 | /* End PBXSourcesBuildPhase section */ 149 | 150 | /* Begin PBXVariantGroup section */ 151 | 835262951F80FD5600039B50 /* Main.storyboard */ = { 152 | isa = PBXVariantGroup; 153 | children = ( 154 | 835262961F80FD5600039B50 /* Base */, 155 | ); 156 | name = Main.storyboard; 157 | sourceTree = ""; 158 | }; 159 | 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */ = { 160 | isa = PBXVariantGroup; 161 | children = ( 162 | 8352629B1F80FD5600039B50 /* Base */, 163 | ); 164 | name = LaunchScreen.storyboard; 165 | sourceTree = ""; 166 | }; 167 | /* End PBXVariantGroup section */ 168 | 169 | /* Begin XCBuildConfiguration section */ 170 | 8352629E1F80FD5600039B50 /* Debug */ = { 171 | isa = XCBuildConfiguration; 172 | buildSettings = { 173 | ALWAYS_SEARCH_USER_PATHS = NO; 174 | CLANG_ANALYZER_NONNULL = YES; 175 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 176 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 177 | CLANG_CXX_LIBRARY = "libc++"; 178 | CLANG_ENABLE_MODULES = YES; 179 | CLANG_ENABLE_OBJC_ARC = YES; 180 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_COMMA = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 185 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 186 | CLANG_WARN_EMPTY_BODY = YES; 187 | CLANG_WARN_ENUM_CONVERSION = YES; 188 | CLANG_WARN_INFINITE_RECURSION = YES; 189 | CLANG_WARN_INT_CONVERSION = YES; 190 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 191 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 193 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 194 | CLANG_WARN_STRICT_PROTOTYPES = YES; 195 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 196 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 197 | CLANG_WARN_UNREACHABLE_CODE = YES; 198 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 199 | CODE_SIGN_IDENTITY = "iPhone Developer"; 200 | COPY_PHASE_STRIP = NO; 201 | DEBUG_INFORMATION_FORMAT = dwarf; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | ENABLE_TESTABILITY = YES; 204 | GCC_C_LANGUAGE_STANDARD = gnu11; 205 | GCC_DYNAMIC_NO_PIC = NO; 206 | GCC_NO_COMMON_BLOCKS = YES; 207 | GCC_OPTIMIZATION_LEVEL = 0; 208 | GCC_PREPROCESSOR_DEFINITIONS = ( 209 | "DEBUG=1", 210 | "$(inherited)", 211 | ); 212 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 213 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 214 | GCC_WARN_UNDECLARED_SELECTOR = YES; 215 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 216 | GCC_WARN_UNUSED_FUNCTION = YES; 217 | GCC_WARN_UNUSED_VARIABLE = YES; 218 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 219 | MTL_ENABLE_DEBUG_INFO = YES; 220 | ONLY_ACTIVE_ARCH = YES; 221 | SDKROOT = iphoneos; 222 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 223 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 224 | }; 225 | name = Debug; 226 | }; 227 | 8352629F1F80FD5600039B50 /* Release */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | ALWAYS_SEARCH_USER_PATHS = NO; 231 | CLANG_ANALYZER_NONNULL = YES; 232 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 233 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 234 | CLANG_CXX_LIBRARY = "libc++"; 235 | CLANG_ENABLE_MODULES = YES; 236 | CLANG_ENABLE_OBJC_ARC = YES; 237 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 238 | CLANG_WARN_BOOL_CONVERSION = YES; 239 | CLANG_WARN_COMMA = YES; 240 | CLANG_WARN_CONSTANT_CONVERSION = YES; 241 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 242 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 243 | CLANG_WARN_EMPTY_BODY = YES; 244 | CLANG_WARN_ENUM_CONVERSION = YES; 245 | CLANG_WARN_INFINITE_RECURSION = YES; 246 | CLANG_WARN_INT_CONVERSION = YES; 247 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 251 | CLANG_WARN_STRICT_PROTOTYPES = YES; 252 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 254 | CLANG_WARN_UNREACHABLE_CODE = YES; 255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 256 | CODE_SIGN_IDENTITY = "iPhone Developer"; 257 | COPY_PHASE_STRIP = NO; 258 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 259 | ENABLE_NS_ASSERTIONS = NO; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | GCC_C_LANGUAGE_STANDARD = gnu11; 262 | GCC_NO_COMMON_BLOCKS = YES; 263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 265 | GCC_WARN_UNDECLARED_SELECTOR = YES; 266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 267 | GCC_WARN_UNUSED_FUNCTION = YES; 268 | GCC_WARN_UNUSED_VARIABLE = YES; 269 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 270 | MTL_ENABLE_DEBUG_INFO = NO; 271 | SDKROOT = iphoneos; 272 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 273 | VALIDATE_PRODUCT = YES; 274 | }; 275 | name = Release; 276 | }; 277 | 835262A11F80FD5600039B50 /* Debug */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 281 | CODE_SIGN_STYLE = Automatic; 282 | DEVELOPMENT_TEAM = SL7736T2Z4; 283 | INFOPLIST_FILE = Diff/Info.plist; 284 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 285 | PRODUCT_BUNDLE_IDENTIFIER = io.objcio.AnimationTesting; 286 | PRODUCT_NAME = "$(TARGET_NAME)"; 287 | SWIFT_VERSION = 4.0; 288 | TARGETED_DEVICE_FAMILY = "1,2"; 289 | }; 290 | name = Debug; 291 | }; 292 | 835262A21F80FD5600039B50 /* Release */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 296 | CODE_SIGN_STYLE = Automatic; 297 | DEVELOPMENT_TEAM = SL7736T2Z4; 298 | INFOPLIST_FILE = Diff/Info.plist; 299 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 300 | PRODUCT_BUNDLE_IDENTIFIER = io.objcio.AnimationTesting; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | SWIFT_VERSION = 4.0; 303 | TARGETED_DEVICE_FAMILY = "1,2"; 304 | }; 305 | name = Release; 306 | }; 307 | /* End XCBuildConfiguration section */ 308 | 309 | /* Begin XCConfigurationList section */ 310 | 835262891F80FD5600039B50 /* Build configuration list for PBXProject "Diff" */ = { 311 | isa = XCConfigurationList; 312 | buildConfigurations = ( 313 | 8352629E1F80FD5600039B50 /* Debug */, 314 | 8352629F1F80FD5600039B50 /* Release */, 315 | ); 316 | defaultConfigurationIsVisible = 0; 317 | defaultConfigurationName = Release; 318 | }; 319 | 835262A01F80FD5600039B50 /* Build configuration list for PBXNativeTarget "Diff" */ = { 320 | isa = XCConfigurationList; 321 | buildConfigurations = ( 322 | 835262A11F80FD5600039B50 /* Debug */, 323 | 835262A21F80FD5600039B50 /* Release */, 324 | ); 325 | defaultConfigurationIsVisible = 0; 326 | defaultConfigurationName = Release; 327 | }; 328 | /* End XCConfigurationList section */ 329 | }; 330 | rootObject = 835262861F80FD5600039B50 /* Project object */; 331 | } 332 | -------------------------------------------------------------------------------- /Diff/Diff/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AnimationTesting 4 | // 5 | // Created by Chris Eidhof on 01.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Diff/Diff/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Diff/Diff/Backup.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ViewController.swift 3 | //// AnimationTesting 4 | //// 5 | //// Created by Chris Eidhof on 01.10.17. 6 | //// Copyright © 2017 objc.io. All rights reserved. 7 | //// 8 | // 9 | //import UIKit 10 | // 11 | ////: Playground - noun: a place where people can play 12 | // 13 | //import UIKit 14 | // 15 | //struct Animation { 16 | // let value: (RelativeTime) -> A 17 | // 18 | // init(_ value: @escaping (RelativeTime) -> A) { 19 | // self.value = value 20 | // } 21 | // 22 | // init(constant: A) { 23 | // self.value = { _ in constant } 24 | // } 25 | // 26 | //} 27 | // 28 | //extension Animation where A == () { 29 | // static let one = Animation<()>() 30 | // 31 | // init() { 32 | // self.value = { _ in () } 33 | // } 34 | // 35 | //} 36 | // 37 | //typealias Progress = Double 38 | // 39 | //typealias AnimationCurve = (Progress) -> Progress 40 | // 41 | //extension Double { 42 | // func clamped(to: ClosedRange) -> Double { 43 | // if self < to.lowerBound { return to.lowerBound } 44 | // if self > to.upperBound { return to.upperBound } 45 | // return self 46 | // } 47 | //} 48 | //func linear(_ progress: Progress) -> Progress { 49 | // return progress 50 | //} 51 | // 52 | //func quadratic(_ progress: Progress) -> Progress { 53 | // return Progress(progress * progress) 54 | //} 55 | // 56 | //func cubic(_ progress: Progress) -> Progress { 57 | // return Progress(progress * progress * progress) 58 | //} 59 | // 60 | //func spring(damping b: Double = 10, mass m: Double = 1, stiffness k: Double = 100, velocity v0: Double = 0) -> AnimationCurve { 61 | // let beta: Double = b / (2*m) 62 | // let omega0: Double = sqrt(k/m) 63 | // let omega1: Double = sqrt((omega0 * omega0) - (beta * beta)) 64 | // let omega2: Double = sqrt((beta * beta) - (omega0 * omega0)) 65 | // 66 | // let x0: Double = -1 67 | // //if (!self.allowsOverdamping && beta > omega0) beta = omega0; 68 | // if beta < omega0 { 69 | // return { t in 70 | // let envelope: Double = exp(-beta * t) 71 | // return -x0 + envelope * (x0 * cos(omega1 * t) + ((beta * x0 + v0) / omega1) * sin(omega1 * t)) 72 | // } 73 | // } else if (beta == omega0) { 74 | // return { t in 75 | // let envelope = exp(-beta * t) 76 | // return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 77 | // } 78 | // } else { 79 | // // Overdamped 80 | // return { t in 81 | // let envelope = exp(-beta * t); 82 | // return -x0 + envelope * (x0 * cosh(omega2 * t) + ((beta * x0 + v0) / omega2) * sinh(omega2 * t)); 83 | // } 84 | // } 85 | //} 86 | // 87 | //extension Animation where A == CGFloat { 88 | // init(from start: CGFloat, speed: CGFloat) { 89 | // value = { time in 90 | // return start + CGFloat(time)*speed 91 | // } 92 | // } 93 | // 94 | //} 95 | // 96 | //extension Animation { 97 | // func parallel(_ other: Animation, _ transform: @escaping (A,B) -> C) -> Animation { 98 | // return Animation({ time in 99 | // let a = self.value(time) 100 | // let b = other.value(time) 101 | // return transform(a,b) 102 | // }) 103 | // } 104 | //} 105 | // 106 | //// RelativeTime is never less than zero 107 | //typealias RelativeTime = CFAbsoluteTime 108 | // 109 | //// Parallel Composition 110 | //func +(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 111 | // return lhs.parallel(rhs) { ($0, $1) } 112 | //} 113 | // 114 | //// Sequential Composition 115 | ////func *(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 116 | //// return Animation({ time in 117 | //// let a = lhs.value(time) 118 | //// let b = rhs.value(time-lhs.duration) 119 | //// return (a,b) 120 | //// }) 121 | ////} 122 | // 123 | ////extension Animation { 124 | //// func delay(by: RelativeTime) -> Animation { 125 | //// return (Animation<()>(duration: by) * self).map { $0.1 } 126 | //// } 127 | ////} 128 | // 129 | //extension Animation { 130 | // func map(_ f: @escaping (A) -> B) -> Animation { 131 | // return Animation({ f(self.value($0))}) 132 | // } 133 | // 134 | // func mapp(_ f: @escaping (RelativeTime, A) -> B) -> Animation { 135 | // return Animation { time in 136 | // f(time, self.value(time)) 137 | // } 138 | // } 139 | // 140 | //} 141 | // 142 | //extension Animation where A == CGFloat { 143 | // func addSpeed(_ speed: A) -> Animation { 144 | // return mapp { time, value in value + CGFloat(time)*speed } 145 | // } 146 | // 147 | // init(from: CGFloat, to: CGFloat, duration: TimeInterval, curve: @escaping AnimationCurve = linear) { 148 | // let speed = (to-from)/CGFloat(duration) 149 | // self = Animation(constant: from).addSpeed(speed).after(duration: duration, switchTo: { endValue in Animation(constant: endValue) }).map { $0.value }.augmentCurve(curve: { time in curve(time/duration)}) 150 | // } 151 | //} 152 | // 153 | //extension Animation { 154 | // // func changeSpeed(factor: RelativeTime) -> Animation { 155 | // // return Animation(duration: duration / factor, { time in 156 | // // self.value(time * factor) 157 | // // }) 158 | // // } 159 | // // 160 | // // var halfSpeed: Animation { 161 | // // return changeSpeed(factor: 0.5) 162 | // // } 163 | // 164 | // fileprivate func augmentCurve(curve: @escaping AnimationCurve) -> Animation { 165 | // return Animation { time in 166 | // return self.value(curve(time)) 167 | // } 168 | // } 169 | //} 170 | // 171 | //enum Either { 172 | // case left(A) 173 | // case right(B) 174 | //} 175 | // 176 | //extension Animation { 177 | // func after(duration: RelativeTime, switchTo other: Animation) -> Animation> { 178 | // return Animation> { time in 179 | // if time < duration { return .left(self.value(time)) } 180 | // else { return .right(other.value(time)) } 181 | // } 182 | // } 183 | // 184 | // func after(duration: RelativeTime, switchTo other: @escaping (A) -> Animation) -> Animation> { 185 | // return Animation> { time in 186 | // if time < duration { 187 | // return .left(self.value(time)) 188 | // } 189 | // else { 190 | // let otherValue = other(self.value(duration)) 191 | // return .right(otherValue.value(time-duration)) 192 | // } 193 | // } 194 | // } 195 | // 196 | //} 197 | // 198 | // 199 | // 200 | //final class Driver: NSObject { 201 | // var displayLink: CADisplayLink! 202 | // var animate: ((CFAbsoluteTime) -> ())! 203 | // 204 | // init(animation: Animation, _ interpret: @escaping (A) -> ()) { 205 | // super.init() 206 | // displayLink = CADisplayLink(target: self, selector: #selector(step(_:))) 207 | // displayLink.add(to: RunLoop.main, forMode: .commonModes) 208 | // var startTime: CFAbsoluteTime? = nil 209 | // self.animate = { [unowned self] nextTime in 210 | // if startTime == nil { startTime = nextTime } 211 | // let time = nextTime - startTime! 212 | // let value = animation.value(time) 213 | // interpret(value) 214 | // // if time >= animation.duration { 215 | // //// print((time, animation.duration)) 216 | // //// self.displayLink.isPaused = true 217 | // // } 218 | // } 219 | // } 220 | // 221 | // 222 | // 223 | // @objc func step(_ displayLink: CADisplayLink) { 224 | // animate(displayLink.targetTimestamp) 225 | // } 226 | //} 227 | // 228 | //extension Either where A == B { 229 | // var value: A { 230 | // switch self { 231 | // case .left(let x): return x 232 | // case .right(let y): return y 233 | // } 234 | // } 235 | //} 236 | // 237 | //class ViewController: UIViewController { 238 | // var driver: Driver? 239 | // 240 | // @IBOutlet weak var redBox: UIView! 241 | // override func viewDidLoad() { 242 | // super.viewDidLoad() 243 | // CATransaction.setDisableActions(true) 244 | // 245 | // redBox.frame.origin = .zero 246 | // let singlePoint = Animation(from: 0, to: 100, duration: 2).after(duration: 4, switchTo: { end in Animation(from: end, to: 0, duration: 2) }).map{ $0.value } 247 | // let other = singlePoint.augmentCurve(curve: { t in spring()(t / 4) } ) 248 | // let point = (singlePoint + other).map { CGPoint(x: $0.0, y: $0.1 )} 249 | // let origin = point 250 | // let animation = (origin).map { result in 251 | // CGAffineTransform(translationX: result.x, y: result.y) 252 | // } 253 | // 254 | // driver = Driver(animation: animation, { [unowned self] transform in 255 | // self.redBox.transform = transform 256 | // }) 257 | // } 258 | // 259 | // override func didReceiveMemoryWarning() { 260 | // super.didReceiveMemoryWarning() 261 | // // Dispose of any resources that can be recreated. 262 | // } 263 | // 264 | // 265 | //} 266 | // 267 | // 268 | -------------------------------------------------------------------------------- /Diff/Diff/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Diff/Diff/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Diff/Diff/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Diff/Diff/TimingFunctions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimingFunctions.swift 3 | // Duration 4 | // 5 | // Created by Chris Eidhof on 05.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias Progress = Double 12 | 13 | typealias AnimationCurve = (Progress) -> Progress 14 | 15 | extension Double { 16 | func clamped(to: ClosedRange) -> Double { 17 | if self < to.lowerBound { return to.lowerBound } 18 | if self > to.upperBound { return to.upperBound } 19 | return self 20 | } 21 | } 22 | 23 | struct TimingFunction { 24 | enum Builtin { 25 | case linear 26 | case easeIn 27 | case easeOut 28 | case easeInEaseOut 29 | case `default` 30 | 31 | var controlPoints: (Double,Double,Double,Double) { 32 | switch self { 33 | case .linear: 34 | return (0, 0, 1, 1) 35 | case .easeIn: 36 | return (0.42, 0, 1, 1) 37 | case .easeOut: 38 | return (0, 0, 0.58, 1) 39 | case .easeInEaseOut: 40 | return (0.42, 0, 0.58, 1) 41 | case .`default`: 42 | return (0.25, 0.1, 0.25, 1) 43 | } 44 | } 45 | } 46 | 47 | 48 | // Taken from https://gist.github.com/raphaelschaad/6739676 49 | 50 | private var ax,bx,cx,ay,by,cy: Double 51 | init(controlPoints p1x: Double, _ p1y: Double, _ p2x: Double, _ p2y: Double) { 52 | cx = 3.0 * p1x 53 | bx = 3.0 * (p2x - p1x) - cx 54 | ax = 1.0 - cx - bx 55 | 56 | cy = 3.0 * p1y 57 | by = 3.0 * (p2y - p1y) - cy 58 | ay = 1.0 - cy - by 59 | } 60 | 61 | init(type: Builtin = .`default`) { 62 | let (p1, p2, p3, p4) = type.controlPoints 63 | self.init(controlPoints: p1, p2, p3, p4) 64 | } 65 | 66 | func value(x: Double) -> Double { 67 | let xSolved = solve(curveX: x, epsilon: epsilon) 68 | return sampleCurveY(t: xSolved) 69 | } 70 | 71 | let duration: Double = 1 72 | 73 | private var epsilon: Double { 74 | return 1 / (200*duration) 75 | } 76 | 77 | 78 | private func sampleCurveX(t: Double) -> Double { 79 | return ((ax * t + bx) * t + cx) * t 80 | } 81 | 82 | private func sampleCurveY(t: Double) -> Double { 83 | return ((ay * t + by) * t + cy) * t; 84 | } 85 | 86 | private func sampleCurveDerivativeX(t: Double) -> Double { 87 | return (3.0 * ax * t + 2.0 * bx) * t + cx 88 | } 89 | 90 | private func solve(curveX x: Double, epsilon: Double) -> Double { 91 | var t2, x2, d2: Double 92 | 93 | // First try a few iterations of Newton's method -- normally very fast. 94 | t2 = x 95 | for _ in 0..<8 { 96 | x2 = sampleCurveX(t: t2) - x 97 | if (fabs(x2) < epsilon) { 98 | return t2 99 | } 100 | d2 = sampleCurveDerivativeX(t: t2) 101 | if (fabs(d2) < 1e-6) { 102 | break 103 | } 104 | t2 = t2 - x2 / d2; 105 | } 106 | 107 | // Fall back to the bisection method for reliability. 108 | var t0: Double = 0.0 109 | var t1: Double = 1.0 110 | t2 = x 111 | 112 | if (t2 < t0) { 113 | return t0 114 | } 115 | if (t2 > t1) { 116 | return t1 117 | } 118 | 119 | while (t0 < t1) { 120 | x2 = sampleCurveX(t: t2) 121 | if (fabs(x2 - x) < epsilon) { 122 | return t2 123 | } 124 | if (x > x2) { 125 | t0 = t2 126 | } else { 127 | t1 = t2 128 | } 129 | t2 = (t1 - t0) * 0.5 + t0 130 | } 131 | 132 | // Failure. 133 | return t2 134 | } 135 | } 136 | 137 | func linear(_ progress: Progress) -> Progress { 138 | return progress 139 | } 140 | 141 | func quadratic(_ progress: Progress) -> Progress { 142 | return Progress(progress * progress) 143 | } 144 | 145 | func cubic(_ progress: Progress) -> Progress { 146 | return Progress(progress * progress * progress) 147 | } 148 | 149 | func builtin(_ b: TimingFunction.Builtin) -> AnimationCurve { 150 | let tf = TimingFunction(type: b) 151 | return tf.value 152 | } 153 | 154 | func spring(damping b: Double = 10, mass m: Double = 1, stiffness k: Double = 100, velocity v0: Double = 0) -> AnimationCurve { 155 | let beta: Double = b / (2*m) 156 | let omega0: Double = sqrt(k/m) 157 | let omega1: Double = sqrt((omega0 * omega0) - (beta * beta)) 158 | let omega2: Double = sqrt((beta * beta) - (omega0 * omega0)) 159 | 160 | let x0: Double = -1 161 | //if (!self.allowsOverdamping && beta > omega0) beta = omega0; 162 | if beta < omega0 { 163 | return { t in 164 | let envelope: Double = exp(-beta * t) 165 | return -x0 + envelope * (x0 * cos(omega1 * t) + ((beta * x0 + v0) / omega1) * sin(omega1 * t)) 166 | } 167 | } else if (beta == omega0) { 168 | return { t in 169 | let envelope = exp(-beta * t) 170 | return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 171 | } 172 | } else { 173 | // Overdamped 174 | return { t in 175 | let envelope = exp(-beta * t); 176 | return -x0 + envelope * (x0 * cosh(omega2 * t) + ((beta * x0 + v0) / omega2) * sinh(omega2 * t)); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Diff/Diff/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // AnimationTesting 4 | // 5 | // Created by Chris Eidhof on 01.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum Either { 12 | case left(A) 13 | case right(B) 14 | } 15 | 16 | extension Either where A == B { 17 | var value: A { 18 | switch self { 19 | case .left(let x): return x 20 | case .right(let y): return y 21 | } 22 | } 23 | } 24 | 25 | // RelativeTime is never less than zero 26 | typealias RelativeTime = CFAbsoluteTime 27 | 28 | struct Animation { 29 | let next: (inout State, inout A, _ delta: RelativeTime) -> () 30 | } 31 | 32 | extension Animation { 33 | init(constant: A) { 34 | self.next = { _, result, _ in result = constant } 35 | } 36 | 37 | } 38 | 39 | extension Animation where A == CGFloat { 40 | init(velocity: CGFloat) { 41 | next = { _, result, dt in 42 | result += CGFloat(dt) * velocity 43 | } 44 | } 45 | } 46 | 47 | extension Animation where State == CGFloat, A == CGFloat { 48 | init(force: CGFloat) { 49 | next = { velocity, result, dt in 50 | velocity += CGFloat(dt) * force 51 | result += CGFloat(dt) * velocity 52 | } 53 | } 54 | 55 | } 56 | 57 | extension Animation { 58 | func parallel(_ other: Animation) -> Animation<(State, State2), (A,B)> { 59 | return Animation<(State, State2), (A,B)> { s, result, dt in 60 | self.next(&s.0, &result.0, dt) 61 | other.next(&s.1, &result.1, dt) 62 | } 63 | } 64 | 65 | func sequence(_ other: Animation, after: (RelativeTime)) -> Animation { 66 | var time: RelativeTime = 0 67 | return Animation { s, result, dt in 68 | time += dt 69 | if time < after { 70 | self.next(&s, &result, dt) 71 | } else { 72 | other.next(&s, &result, dt) 73 | } 74 | } 75 | } 76 | } 77 | 78 | class InterpretedAnimation { 79 | func run(_ timeDiff: RelativeTime) { 80 | fatalError() 81 | } 82 | } 83 | 84 | final class _InterpretedAnimation: InterpretedAnimation { 85 | enum State { case done, running } 86 | var current: A 87 | var state: S 88 | var animation: Animation 89 | var interpret: (A) -> () 90 | 91 | init(initial: A, state: S, animation: Animation, interpret: @escaping (A) -> ()) { 92 | self.state = state 93 | self.current = initial 94 | self.animation = animation 95 | self.interpret = interpret 96 | } 97 | 98 | override func run(_ timeDiff: RelativeTime) { 99 | animation.next(&state, ¤t, timeDiff) 100 | interpret(current) 101 | } 102 | } 103 | 104 | final class Driver: NSObject { 105 | var displayLink: CADisplayLink! 106 | var animations: [InterpretedAnimation] = [] 107 | var previousTime: CFAbsoluteTime! 108 | 109 | override init() { 110 | super.init() 111 | displayLink = CADisplayLink(target: self, selector: #selector(step(_:))) 112 | displayLink.add(to: RunLoop.main, forMode: .commonModes) 113 | } 114 | 115 | func add(_ animation: InterpretedAnimation) { 116 | animations.append(animation) 117 | displayLink.isPaused = false 118 | } 119 | 120 | @objc func step(_ displayLink: CADisplayLink) { 121 | let time = displayLink.targetTimestamp 122 | defer { previousTime = time } 123 | if previousTime == nil { return } 124 | let diff = time - previousTime 125 | 126 | for animation in animations { 127 | animation.run(diff) 128 | } 129 | if animations.isEmpty { 130 | displayLink.isPaused = true 131 | } 132 | } 133 | 134 | deinit { 135 | displayLink.remove(from: .main, forMode: .commonModes) 136 | } 137 | } 138 | 139 | func dot(origin: CGPoint, size: CGSize = CGSize(width: 30, height: 30), backgroundColor: UIColor = .orange) -> UIView { 140 | let result = UIView(frame: CGRect(origin: origin, size: size)) 141 | result.backgroundColor = backgroundColor 142 | result.layer.cornerRadius = size.width/2 143 | result.layer.masksToBounds = true 144 | return result 145 | } 146 | 147 | class ViewController: UIViewController { 148 | var driver: Driver = Driver() 149 | 150 | override func viewDidLoad() { 151 | super.viewDidLoad() 152 | CATransaction.setDisableActions(true) 153 | } 154 | 155 | override func viewDidAppear(_ animated: Bool) { 156 | let x = dot(origin: CGPoint(x: 10, y: 10)) 157 | view.addSubview(x) 158 | let anim = Animation(force: 75).sequence(Animation(force: -100), after: 2) 159 | let i: InterpretedAnimation = _InterpretedAnimation(initial: 0, state: 0, animation: anim, interpret: { 160 | x.transform = CGAffineTransform(translationX: $0, y: 0) 161 | }) 162 | driver.add(i) 163 | // let dots = (0..<5).map { 164 | // dot(origin: CGPoint(x: -15, y: 40 * (1 + $0))) 165 | // } 166 | // 167 | // Animation(constant: 0) 168 | // 169 | // for (index, dot) in dots.enumerated() { 170 | // view.addSubview(dot) 171 | // } 172 | } 173 | } 174 | 175 | 176 | -------------------------------------------------------------------------------- /Duration/Duration.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 835262921F80FD5600039B50 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835262911F80FD5600039B50 /* AppDelegate.swift */; }; 11 | 835262941F80FD5600039B50 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835262931F80FD5600039B50 /* ViewController.swift */; }; 12 | 835262971F80FD5600039B50 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 835262951F80FD5600039B50 /* Main.storyboard */; }; 13 | 835262991F80FD5600039B50 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 835262981F80FD5600039B50 /* Assets.xcassets */; }; 14 | 8352629C1F80FD5600039B50 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 8352628E1F80FD5600039B50 /* Duration.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Duration.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 835262911F80FD5600039B50 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 20 | 835262931F80FD5600039B50 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 21 | 835262961F80FD5600039B50 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 22 | 835262981F80FD5600039B50 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23 | 8352629B1F80FD5600039B50 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 24 | 8352629D1F80FD5600039B50 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | 8352628B1F80FD5600039B50 /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | ); 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXFrameworksBuildPhase section */ 36 | 37 | /* Begin PBXGroup section */ 38 | 835262851F80FD5600039B50 = { 39 | isa = PBXGroup; 40 | children = ( 41 | 835262901F80FD5600039B50 /* Duration */, 42 | 8352628F1F80FD5600039B50 /* Products */, 43 | ); 44 | sourceTree = ""; 45 | }; 46 | 8352628F1F80FD5600039B50 /* Products */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | 8352628E1F80FD5600039B50 /* Duration.app */, 50 | ); 51 | name = Products; 52 | sourceTree = ""; 53 | }; 54 | 835262901F80FD5600039B50 /* Duration */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 835262911F80FD5600039B50 /* AppDelegate.swift */, 58 | 835262931F80FD5600039B50 /* ViewController.swift */, 59 | 835262951F80FD5600039B50 /* Main.storyboard */, 60 | 835262981F80FD5600039B50 /* Assets.xcassets */, 61 | 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */, 62 | 8352629D1F80FD5600039B50 /* Info.plist */, 63 | ); 64 | path = Duration; 65 | sourceTree = ""; 66 | }; 67 | /* End PBXGroup section */ 68 | 69 | /* Begin PBXNativeTarget section */ 70 | 8352628D1F80FD5600039B50 /* Duration */ = { 71 | isa = PBXNativeTarget; 72 | buildConfigurationList = 835262A01F80FD5600039B50 /* Build configuration list for PBXNativeTarget "Duration" */; 73 | buildPhases = ( 74 | 8352628A1F80FD5600039B50 /* Sources */, 75 | 8352628B1F80FD5600039B50 /* Frameworks */, 76 | 8352628C1F80FD5600039B50 /* Resources */, 77 | ); 78 | buildRules = ( 79 | ); 80 | dependencies = ( 81 | ); 82 | name = Duration; 83 | productName = AnimationTesting; 84 | productReference = 8352628E1F80FD5600039B50 /* Duration.app */; 85 | productType = "com.apple.product-type.application"; 86 | }; 87 | /* End PBXNativeTarget section */ 88 | 89 | /* Begin PBXProject section */ 90 | 835262861F80FD5600039B50 /* Project object */ = { 91 | isa = PBXProject; 92 | attributes = { 93 | LastSwiftUpdateCheck = 0900; 94 | LastUpgradeCheck = 0900; 95 | ORGANIZATIONNAME = objc.io; 96 | TargetAttributes = { 97 | 8352628D1F80FD5600039B50 = { 98 | CreatedOnToolsVersion = 9.0; 99 | ProvisioningStyle = Automatic; 100 | }; 101 | }; 102 | }; 103 | buildConfigurationList = 835262891F80FD5600039B50 /* Build configuration list for PBXProject "Duration" */; 104 | compatibilityVersion = "Xcode 8.0"; 105 | developmentRegion = en; 106 | hasScannedForEncodings = 0; 107 | knownRegions = ( 108 | en, 109 | Base, 110 | ); 111 | mainGroup = 835262851F80FD5600039B50; 112 | productRefGroup = 8352628F1F80FD5600039B50 /* Products */; 113 | projectDirPath = ""; 114 | projectRoot = ""; 115 | targets = ( 116 | 8352628D1F80FD5600039B50 /* Duration */, 117 | ); 118 | }; 119 | /* End PBXProject section */ 120 | 121 | /* Begin PBXResourcesBuildPhase section */ 122 | 8352628C1F80FD5600039B50 /* Resources */ = { 123 | isa = PBXResourcesBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | 8352629C1F80FD5600039B50 /* LaunchScreen.storyboard in Resources */, 127 | 835262991F80FD5600039B50 /* Assets.xcassets in Resources */, 128 | 835262971F80FD5600039B50 /* Main.storyboard in Resources */, 129 | ); 130 | runOnlyForDeploymentPostprocessing = 0; 131 | }; 132 | /* End PBXResourcesBuildPhase section */ 133 | 134 | /* Begin PBXSourcesBuildPhase section */ 135 | 8352628A1F80FD5600039B50 /* Sources */ = { 136 | isa = PBXSourcesBuildPhase; 137 | buildActionMask = 2147483647; 138 | files = ( 139 | 835262941F80FD5600039B50 /* ViewController.swift in Sources */, 140 | 835262921F80FD5600039B50 /* AppDelegate.swift in Sources */, 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | /* End PBXSourcesBuildPhase section */ 145 | 146 | /* Begin PBXVariantGroup section */ 147 | 835262951F80FD5600039B50 /* Main.storyboard */ = { 148 | isa = PBXVariantGroup; 149 | children = ( 150 | 835262961F80FD5600039B50 /* Base */, 151 | ); 152 | name = Main.storyboard; 153 | sourceTree = ""; 154 | }; 155 | 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */ = { 156 | isa = PBXVariantGroup; 157 | children = ( 158 | 8352629B1F80FD5600039B50 /* Base */, 159 | ); 160 | name = LaunchScreen.storyboard; 161 | sourceTree = ""; 162 | }; 163 | /* End PBXVariantGroup section */ 164 | 165 | /* Begin XCBuildConfiguration section */ 166 | 8352629E1F80FD5600039B50 /* Debug */ = { 167 | isa = XCBuildConfiguration; 168 | buildSettings = { 169 | ALWAYS_SEARCH_USER_PATHS = NO; 170 | CLANG_ANALYZER_NONNULL = YES; 171 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 172 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 173 | CLANG_CXX_LIBRARY = "libc++"; 174 | CLANG_ENABLE_MODULES = YES; 175 | CLANG_ENABLE_OBJC_ARC = YES; 176 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 177 | CLANG_WARN_BOOL_CONVERSION = YES; 178 | CLANG_WARN_COMMA = YES; 179 | CLANG_WARN_CONSTANT_CONVERSION = YES; 180 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 181 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 182 | CLANG_WARN_EMPTY_BODY = YES; 183 | CLANG_WARN_ENUM_CONVERSION = YES; 184 | CLANG_WARN_INFINITE_RECURSION = YES; 185 | CLANG_WARN_INT_CONVERSION = YES; 186 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 187 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 188 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 189 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 190 | CLANG_WARN_STRICT_PROTOTYPES = YES; 191 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 192 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 193 | CLANG_WARN_UNREACHABLE_CODE = YES; 194 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 195 | CODE_SIGN_IDENTITY = "iPhone Developer"; 196 | COPY_PHASE_STRIP = NO; 197 | DEBUG_INFORMATION_FORMAT = dwarf; 198 | ENABLE_STRICT_OBJC_MSGSEND = YES; 199 | ENABLE_TESTABILITY = YES; 200 | GCC_C_LANGUAGE_STANDARD = gnu11; 201 | GCC_DYNAMIC_NO_PIC = NO; 202 | GCC_NO_COMMON_BLOCKS = YES; 203 | GCC_OPTIMIZATION_LEVEL = 0; 204 | GCC_PREPROCESSOR_DEFINITIONS = ( 205 | "DEBUG=1", 206 | "$(inherited)", 207 | ); 208 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 209 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 210 | GCC_WARN_UNDECLARED_SELECTOR = YES; 211 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 212 | GCC_WARN_UNUSED_FUNCTION = YES; 213 | GCC_WARN_UNUSED_VARIABLE = YES; 214 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 215 | MTL_ENABLE_DEBUG_INFO = YES; 216 | ONLY_ACTIVE_ARCH = YES; 217 | SDKROOT = iphoneos; 218 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 219 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 220 | }; 221 | name = Debug; 222 | }; 223 | 8352629F1F80FD5600039B50 /* Release */ = { 224 | isa = XCBuildConfiguration; 225 | buildSettings = { 226 | ALWAYS_SEARCH_USER_PATHS = NO; 227 | CLANG_ANALYZER_NONNULL = YES; 228 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 229 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 230 | CLANG_CXX_LIBRARY = "libc++"; 231 | CLANG_ENABLE_MODULES = YES; 232 | CLANG_ENABLE_OBJC_ARC = YES; 233 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 234 | CLANG_WARN_BOOL_CONVERSION = YES; 235 | CLANG_WARN_COMMA = YES; 236 | CLANG_WARN_CONSTANT_CONVERSION = YES; 237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 238 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 239 | CLANG_WARN_EMPTY_BODY = YES; 240 | CLANG_WARN_ENUM_CONVERSION = YES; 241 | CLANG_WARN_INFINITE_RECURSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 244 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 245 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 246 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 247 | CLANG_WARN_STRICT_PROTOTYPES = YES; 248 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 249 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 250 | CLANG_WARN_UNREACHABLE_CODE = YES; 251 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 252 | CODE_SIGN_IDENTITY = "iPhone Developer"; 253 | COPY_PHASE_STRIP = NO; 254 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 255 | ENABLE_NS_ASSERTIONS = NO; 256 | ENABLE_STRICT_OBJC_MSGSEND = YES; 257 | GCC_C_LANGUAGE_STANDARD = gnu11; 258 | GCC_NO_COMMON_BLOCKS = YES; 259 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 260 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 261 | GCC_WARN_UNDECLARED_SELECTOR = YES; 262 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 263 | GCC_WARN_UNUSED_FUNCTION = YES; 264 | GCC_WARN_UNUSED_VARIABLE = YES; 265 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 266 | MTL_ENABLE_DEBUG_INFO = NO; 267 | SDKROOT = iphoneos; 268 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 269 | VALIDATE_PRODUCT = YES; 270 | }; 271 | name = Release; 272 | }; 273 | 835262A11F80FD5600039B50 /* Debug */ = { 274 | isa = XCBuildConfiguration; 275 | buildSettings = { 276 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 277 | CODE_SIGN_STYLE = Automatic; 278 | DEVELOPMENT_TEAM = SL7736T2Z4; 279 | INFOPLIST_FILE = Duration/Info.plist; 280 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 281 | PRODUCT_BUNDLE_IDENTIFIER = io.objcio.AnimationTesting; 282 | PRODUCT_NAME = "$(TARGET_NAME)"; 283 | SWIFT_VERSION = 4.0; 284 | TARGETED_DEVICE_FAMILY = "1,2"; 285 | }; 286 | name = Debug; 287 | }; 288 | 835262A21F80FD5600039B50 /* Release */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 292 | CODE_SIGN_STYLE = Automatic; 293 | DEVELOPMENT_TEAM = SL7736T2Z4; 294 | INFOPLIST_FILE = Duration/Info.plist; 295 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 296 | PRODUCT_BUNDLE_IDENTIFIER = io.objcio.AnimationTesting; 297 | PRODUCT_NAME = "$(TARGET_NAME)"; 298 | SWIFT_VERSION = 4.0; 299 | TARGETED_DEVICE_FAMILY = "1,2"; 300 | }; 301 | name = Release; 302 | }; 303 | /* End XCBuildConfiguration section */ 304 | 305 | /* Begin XCConfigurationList section */ 306 | 835262891F80FD5600039B50 /* Build configuration list for PBXProject "Duration" */ = { 307 | isa = XCConfigurationList; 308 | buildConfigurations = ( 309 | 8352629E1F80FD5600039B50 /* Debug */, 310 | 8352629F1F80FD5600039B50 /* Release */, 311 | ); 312 | defaultConfigurationIsVisible = 0; 313 | defaultConfigurationName = Release; 314 | }; 315 | 835262A01F80FD5600039B50 /* Build configuration list for PBXNativeTarget "Duration" */ = { 316 | isa = XCConfigurationList; 317 | buildConfigurations = ( 318 | 835262A11F80FD5600039B50 /* Debug */, 319 | 835262A21F80FD5600039B50 /* Release */, 320 | ); 321 | defaultConfigurationIsVisible = 0; 322 | defaultConfigurationName = Release; 323 | }; 324 | /* End XCConfigurationList section */ 325 | }; 326 | rootObject = 835262861F80FD5600039B50 /* Project object */; 327 | } 328 | -------------------------------------------------------------------------------- /Duration/Duration/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AnimationTesting 4 | // 5 | // Created by Chris Eidhof on 01.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Duration/Duration/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Duration/Duration/Backup.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ViewController.swift 3 | //// AnimationTesting 4 | //// 5 | //// Created by Chris Eidhof on 01.10.17. 6 | //// Copyright © 2017 objc.io. All rights reserved. 7 | //// 8 | // 9 | //import UIKit 10 | // 11 | ////: Playground - noun: a place where people can play 12 | // 13 | //import UIKit 14 | // 15 | //struct Animation { 16 | // let value: (RelativeTime) -> A 17 | // 18 | // init(_ value: @escaping (RelativeTime) -> A) { 19 | // self.value = value 20 | // } 21 | // 22 | // init(constant: A) { 23 | // self.value = { _ in constant } 24 | // } 25 | // 26 | //} 27 | // 28 | //extension Animation where A == () { 29 | // static let one = Animation<()>() 30 | // 31 | // init() { 32 | // self.value = { _ in () } 33 | // } 34 | // 35 | //} 36 | // 37 | //typealias Progress = Double 38 | // 39 | //typealias AnimationCurve = (Progress) -> Progress 40 | // 41 | //extension Double { 42 | // func clamped(to: ClosedRange) -> Double { 43 | // if self < to.lowerBound { return to.lowerBound } 44 | // if self > to.upperBound { return to.upperBound } 45 | // return self 46 | // } 47 | //} 48 | //func linear(_ progress: Progress) -> Progress { 49 | // return progress 50 | //} 51 | // 52 | //func quadratic(_ progress: Progress) -> Progress { 53 | // return Progress(progress * progress) 54 | //} 55 | // 56 | //func cubic(_ progress: Progress) -> Progress { 57 | // return Progress(progress * progress * progress) 58 | //} 59 | // 60 | //func spring(damping b: Double = 10, mass m: Double = 1, stiffness k: Double = 100, velocity v0: Double = 0) -> AnimationCurve { 61 | // let beta: Double = b / (2*m) 62 | // let omega0: Double = sqrt(k/m) 63 | // let omega1: Double = sqrt((omega0 * omega0) - (beta * beta)) 64 | // let omega2: Double = sqrt((beta * beta) - (omega0 * omega0)) 65 | // 66 | // let x0: Double = -1 67 | // //if (!self.allowsOverdamping && beta > omega0) beta = omega0; 68 | // if beta < omega0 { 69 | // return { t in 70 | // let envelope: Double = exp(-beta * t) 71 | // return -x0 + envelope * (x0 * cos(omega1 * t) + ((beta * x0 + v0) / omega1) * sin(omega1 * t)) 72 | // } 73 | // } else if (beta == omega0) { 74 | // return { t in 75 | // let envelope = exp(-beta * t) 76 | // return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 77 | // } 78 | // } else { 79 | // // Overdamped 80 | // return { t in 81 | // let envelope = exp(-beta * t); 82 | // return -x0 + envelope * (x0 * cosh(omega2 * t) + ((beta * x0 + v0) / omega2) * sinh(omega2 * t)); 83 | // } 84 | // } 85 | //} 86 | // 87 | //extension Animation where A == CGFloat { 88 | // init(from start: CGFloat, speed: CGFloat) { 89 | // value = { time in 90 | // return start + CGFloat(time)*speed 91 | // } 92 | // } 93 | // 94 | //} 95 | // 96 | //extension Animation { 97 | // func parallel(_ other: Animation, _ transform: @escaping (A,B) -> C) -> Animation { 98 | // return Animation({ time in 99 | // let a = self.value(time) 100 | // let b = other.value(time) 101 | // return transform(a,b) 102 | // }) 103 | // } 104 | //} 105 | // 106 | //// RelativeTime is never less than zero 107 | //typealias RelativeTime = CFAbsoluteTime 108 | // 109 | //// Parallel Composition 110 | //func +(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 111 | // return lhs.parallel(rhs) { ($0, $1) } 112 | //} 113 | // 114 | //// Sequential Composition 115 | ////func *(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 116 | //// return Animation({ time in 117 | //// let a = lhs.value(time) 118 | //// let b = rhs.value(time-lhs.duration) 119 | //// return (a,b) 120 | //// }) 121 | ////} 122 | // 123 | ////extension Animation { 124 | //// func delay(by: RelativeTime) -> Animation { 125 | //// return (Animation<()>(duration: by) * self).map { $0.1 } 126 | //// } 127 | ////} 128 | // 129 | //extension Animation { 130 | // func map(_ f: @escaping (A) -> B) -> Animation { 131 | // return Animation({ f(self.value($0))}) 132 | // } 133 | // 134 | // func mapp(_ f: @escaping (RelativeTime, A) -> B) -> Animation { 135 | // return Animation { time in 136 | // f(time, self.value(time)) 137 | // } 138 | // } 139 | // 140 | //} 141 | // 142 | //extension Animation where A == CGFloat { 143 | // func addSpeed(_ speed: A) -> Animation { 144 | // return mapp { time, value in value + CGFloat(time)*speed } 145 | // } 146 | // 147 | // init(from: CGFloat, to: CGFloat, duration: TimeInterval, curve: @escaping AnimationCurve = linear) { 148 | // let speed = (to-from)/CGFloat(duration) 149 | // self = Animation(constant: from).addSpeed(speed).after(duration: duration, switchTo: { endValue in Animation(constant: endValue) }).map { $0.value }.augmentCurve(curve: { time in curve(time/duration)}) 150 | // } 151 | //} 152 | // 153 | //extension Animation { 154 | // // func changeSpeed(factor: RelativeTime) -> Animation { 155 | // // return Animation(duration: duration / factor, { time in 156 | // // self.value(time * factor) 157 | // // }) 158 | // // } 159 | // // 160 | // // var halfSpeed: Animation { 161 | // // return changeSpeed(factor: 0.5) 162 | // // } 163 | // 164 | // fileprivate func augmentCurve(curve: @escaping AnimationCurve) -> Animation { 165 | // return Animation { time in 166 | // return self.value(curve(time)) 167 | // } 168 | // } 169 | //} 170 | // 171 | //enum Either { 172 | // case left(A) 173 | // case right(B) 174 | //} 175 | // 176 | //extension Animation { 177 | // func after(duration: RelativeTime, switchTo other: Animation) -> Animation> { 178 | // return Animation> { time in 179 | // if time < duration { return .left(self.value(time)) } 180 | // else { return .right(other.value(time)) } 181 | // } 182 | // } 183 | // 184 | // func after(duration: RelativeTime, switchTo other: @escaping (A) -> Animation) -> Animation> { 185 | // return Animation> { time in 186 | // if time < duration { 187 | // return .left(self.value(time)) 188 | // } 189 | // else { 190 | // let otherValue = other(self.value(duration)) 191 | // return .right(otherValue.value(time-duration)) 192 | // } 193 | // } 194 | // } 195 | // 196 | //} 197 | // 198 | // 199 | // 200 | //final class Driver: NSObject { 201 | // var displayLink: CADisplayLink! 202 | // var animate: ((CFAbsoluteTime) -> ())! 203 | // 204 | // init(animation: Animation, _ interpret: @escaping (A) -> ()) { 205 | // super.init() 206 | // displayLink = CADisplayLink(target: self, selector: #selector(step(_:))) 207 | // displayLink.add(to: RunLoop.main, forMode: .commonModes) 208 | // var startTime: CFAbsoluteTime? = nil 209 | // self.animate = { [unowned self] nextTime in 210 | // if startTime == nil { startTime = nextTime } 211 | // let time = nextTime - startTime! 212 | // let value = animation.value(time) 213 | // interpret(value) 214 | // // if time >= animation.duration { 215 | // //// print((time, animation.duration)) 216 | // //// self.displayLink.isPaused = true 217 | // // } 218 | // } 219 | // } 220 | // 221 | // 222 | // 223 | // @objc func step(_ displayLink: CADisplayLink) { 224 | // animate(displayLink.targetTimestamp) 225 | // } 226 | //} 227 | // 228 | //extension Either where A == B { 229 | // var value: A { 230 | // switch self { 231 | // case .left(let x): return x 232 | // case .right(let y): return y 233 | // } 234 | // } 235 | //} 236 | // 237 | //class ViewController: UIViewController { 238 | // var driver: Driver? 239 | // 240 | // @IBOutlet weak var redBox: UIView! 241 | // override func viewDidLoad() { 242 | // super.viewDidLoad() 243 | // CATransaction.setDisableActions(true) 244 | // 245 | // redBox.frame.origin = .zero 246 | // let singlePoint = Animation(from: 0, to: 100, duration: 2).after(duration: 4, switchTo: { end in Animation(from: end, to: 0, duration: 2) }).map{ $0.value } 247 | // let other = singlePoint.augmentCurve(curve: { t in spring()(t / 4) } ) 248 | // let point = (singlePoint + other).map { CGPoint(x: $0.0, y: $0.1 )} 249 | // let origin = point 250 | // let animation = (origin).map { result in 251 | // CGAffineTransform(translationX: result.x, y: result.y) 252 | // } 253 | // 254 | // driver = Driver(animation: animation, { [unowned self] transform in 255 | // self.redBox.transform = transform 256 | // }) 257 | // } 258 | // 259 | // override func didReceiveMemoryWarning() { 260 | // super.didReceiveMemoryWarning() 261 | // // Dispose of any resources that can be recreated. 262 | // } 263 | // 264 | // 265 | //} 266 | // 267 | // 268 | -------------------------------------------------------------------------------- /Duration/Duration/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Duration/Duration/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Duration/Duration/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Duration/Duration/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // AnimationTesting 4 | // 5 | // Created by Chris Eidhof on 01.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum Either { 12 | case left(A) 13 | case right(B) 14 | } 15 | 16 | extension Either where A == B { 17 | var value: A { 18 | switch self { 19 | case .left(let x): return x 20 | case .right(let y): return y 21 | } 22 | } 23 | } 24 | 25 | //enum AnimationResult { 26 | // case done(A, at: RelativeTime) 27 | // case inProgress(A) 28 | // 29 | // var value: A { 30 | // switch self { 31 | // case let .done(x,_): return x 32 | // case .inProgress(let x): return x 33 | // } 34 | // } 35 | // 36 | // func zip(_ other: AnimationResult, with: (A,B) -> C) -> AnimationResult { 37 | // switch (self,other) { 38 | // case let (.done(x, t1), .done(y, t2)): return .done(with(x,y), at: max(t1, t2)) 39 | // default: return .inProgress(with(self.value, other.value)) 40 | // } 41 | // } 42 | // 43 | // func map(_ transform: (A) -> B) -> AnimationResult { 44 | // switch self { 45 | // case let .done(x, at: time): return .done(transform(x), at: time) 46 | // case .inProgress(let x): return .inProgress(transform(x)) 47 | // } 48 | // } 49 | //} 50 | 51 | struct Animation { 52 | let value: (RelativeTime) -> A 53 | let duration: RelativeTime 54 | 55 | init(duration: RelativeTime, _ value: @escaping (RelativeTime) -> A) { 56 | self.duration = duration 57 | self.value = value 58 | } 59 | 60 | init(duration: RelativeTime, constant: A) { 61 | self.duration = duration 62 | self.value = { _ in constant } 63 | } 64 | // 65 | // init(inProgress constant: A) { 66 | // self.value = { _ in .inProgress(constant) } 67 | // } 68 | 69 | } 70 | 71 | //extension Animation where A == () { 72 | //// static let one = Animation<()>(.done(())) 73 | // 74 | // init() { 75 | // self.init(constant: ()) 76 | // } 77 | // 78 | //} 79 | 80 | typealias Progress = Double 81 | 82 | typealias AnimationCurve = (Progress) -> Progress 83 | 84 | extension Double { 85 | func clamped(to: ClosedRange) -> Double { 86 | if self < to.lowerBound { return to.lowerBound } 87 | if self > to.upperBound { return to.upperBound } 88 | return self 89 | } 90 | } 91 | 92 | struct TimingFunction { 93 | enum Builtin { 94 | case linear 95 | case easeIn 96 | case easeOut 97 | case easeInEaseOut 98 | case `default` 99 | 100 | var controlPoints: (Double,Double,Double,Double) { 101 | switch self { 102 | case .linear: 103 | return (0, 0, 1, 1) 104 | case .easeIn: 105 | return (0.42, 0, 1, 1) 106 | case .easeOut: 107 | return (0, 0, 0.58, 1) 108 | case .easeInEaseOut: 109 | return (0.42, 0, 0.58, 1) 110 | case .`default`: 111 | return (0.25, 0.1, 0.25, 1) 112 | } 113 | } 114 | } 115 | 116 | 117 | // Taken from https://gist.github.com/raphaelschaad/6739676 118 | 119 | private var ax,bx,cx,ay,by,cy: Double 120 | init(controlPoints p1x: Double, _ p1y: Double, _ p2x: Double, _ p2y: Double) { 121 | cx = 3.0 * p1x 122 | bx = 3.0 * (p2x - p1x) - cx 123 | ax = 1.0 - cx - bx 124 | 125 | cy = 3.0 * p1y 126 | by = 3.0 * (p2y - p1y) - cy 127 | ay = 1.0 - cy - by 128 | } 129 | 130 | init(type: Builtin = .`default`) { 131 | let (p1, p2, p3, p4) = type.controlPoints 132 | self.init(controlPoints: p1, p2, p3, p4) 133 | } 134 | 135 | func value(x: Double) -> Double { 136 | let xSolved = solve(curveX: x, epsilon: epsilon) 137 | return sampleCurveY(t: xSolved) 138 | } 139 | 140 | let duration: Double = 1 141 | 142 | private var epsilon: Double { 143 | return 1 / (200*duration) 144 | } 145 | 146 | 147 | private func sampleCurveX(t: Double) -> Double { 148 | return ((ax * t + bx) * t + cx) * t 149 | } 150 | 151 | private func sampleCurveY(t: Double) -> Double { 152 | return ((ay * t + by) * t + cy) * t; 153 | } 154 | 155 | private func sampleCurveDerivativeX(t: Double) -> Double { 156 | return (3.0 * ax * t + 2.0 * bx) * t + cx 157 | } 158 | 159 | private func solve(curveX x: Double, epsilon: Double) -> Double { 160 | var t2, x2, d2: Double 161 | 162 | // First try a few iterations of Newton's method -- normally very fast. 163 | t2 = x 164 | for _ in 0..<8 { 165 | x2 = sampleCurveX(t: t2) - x 166 | if (fabs(x2) < epsilon) { 167 | return t2 168 | } 169 | d2 = sampleCurveDerivativeX(t: t2) 170 | if (fabs(d2) < 1e-6) { 171 | break 172 | } 173 | t2 = t2 - x2 / d2; 174 | } 175 | 176 | // Fall back to the bisection method for reliability. 177 | var t0: Double = 0.0 178 | var t1: Double = 1.0 179 | t2 = x 180 | 181 | if (t2 < t0) { 182 | return t0 183 | } 184 | if (t2 > t1) { 185 | return t1 186 | } 187 | 188 | while (t0 < t1) { 189 | x2 = sampleCurveX(t: t2) 190 | if (fabs(x2 - x) < epsilon) { 191 | return t2 192 | } 193 | if (x > x2) { 194 | t0 = t2 195 | } else { 196 | t1 = t2 197 | } 198 | t2 = (t1 - t0) * 0.5 + t0 199 | } 200 | 201 | // Failure. 202 | return t2 203 | } 204 | } 205 | 206 | func linear(_ progress: Progress) -> Progress { 207 | return progress 208 | } 209 | 210 | func quadratic(_ progress: Progress) -> Progress { 211 | return Progress(progress * progress) 212 | } 213 | 214 | func cubic(_ progress: Progress) -> Progress { 215 | return Progress(progress * progress * progress) 216 | } 217 | 218 | func builtin(_ b: TimingFunction.Builtin) -> AnimationCurve { 219 | let tf = TimingFunction(type: b) 220 | return tf.value 221 | } 222 | 223 | func spring(damping b: Double = 10, mass m: Double = 1, stiffness k: Double = 100, velocity v0: Double = 0) -> AnimationCurve { 224 | let beta: Double = b / (2*m) 225 | let omega0: Double = sqrt(k/m) 226 | let omega1: Double = sqrt((omega0 * omega0) - (beta * beta)) 227 | let omega2: Double = sqrt((beta * beta) - (omega0 * omega0)) 228 | 229 | let x0: Double = -1 230 | //if (!self.allowsOverdamping && beta > omega0) beta = omega0; 231 | if beta < omega0 { 232 | return { t in 233 | let envelope: Double = exp(-beta * t) 234 | return -x0 + envelope * (x0 * cos(omega1 * t) + ((beta * x0 + v0) / omega1) * sin(omega1 * t)) 235 | } 236 | } else if (beta == omega0) { 237 | return { t in 238 | let envelope = exp(-beta * t) 239 | return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 240 | } 241 | } else { 242 | // Overdamped 243 | return { t in 244 | let envelope = exp(-beta * t); 245 | return -x0 + envelope * (x0 * cosh(omega2 * t) + ((beta * x0 + v0) / omega2) * sinh(omega2 * t)); 246 | } 247 | } 248 | } 249 | 250 | extension Animation { 251 | func parallel(_ other: Animation, _ transform: @escaping (A,B) -> C) -> Animation { 252 | return Animation(duration: max(self.duration, other.duration)) { time in 253 | let a = self.value(time) 254 | let b = other.value(time) 255 | return transform(a,b) 256 | } 257 | } 258 | 259 | func sequential(_ other: Animation) -> Animation> { 260 | return Animation>(duration: self.duration + other.duration) { time in 261 | return time <= self.duration ? .left(self.value(time)) : .right(other.value(time-self.duration)) 262 | } 263 | 264 | } 265 | } 266 | 267 | // RelativeTime is never less than zero 268 | typealias RelativeTime = CFAbsoluteTime 269 | 270 | // Parallel Composition 271 | func +(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 272 | return lhs.parallel(rhs) { ($0, $1) } 273 | } 274 | 275 | // Sequential Composition 276 | func *(lhs: Animation, rhs: Animation) -> Animation> { 277 | return lhs.sequential(rhs) 278 | } 279 | 280 | func *(lhs: Animation, rhs: Animation) -> Animation { 281 | return (lhs * rhs).map { $0.value } 282 | } 283 | 284 | extension Animation { 285 | func map(_ f: @escaping (A) -> B) -> Animation { 286 | return Animation(duration: duration) { f(self.value($0)) } 287 | } 288 | 289 | func mapWithTime(_ f: @escaping (RelativeTime, A) -> B) -> Animation { 290 | return Animation(duration: duration) { time in 291 | return f(time, self.value(time)) 292 | } 293 | } 294 | } 295 | 296 | extension Animation where A == CGFloat { 297 | func addSpeed(_ speed: A) -> Animation { 298 | return mapWithTime { time, value in 299 | return value + CGFloat(time)*speed 300 | } 301 | } 302 | 303 | init(from: CGFloat, to: CGFloat, duration: TimeInterval, curve: @escaping AnimationCurve = linear) { 304 | // todo curve 305 | let diff = to-from 306 | self = Animation(duration: duration) { 307 | let unitTime = $0 / duration 308 | return unitTime >= 1 ? to : from + diff*CGFloat(unitTime) 309 | }.augmentCurve(curve: curve) 310 | } 311 | } 312 | 313 | extension Animation { 314 | func changeSpeed(factor: RelativeTime) -> Animation { 315 | return Animation(duration: duration / factor) { time in 316 | self.value(time * factor) 317 | } 318 | } 319 | 320 | var halfSpeed: Animation { 321 | return changeSpeed(factor: 0.5) 322 | } 323 | 324 | var doubleSpeed: Animation { 325 | return changeSpeed(factor: 2) 326 | } 327 | 328 | func delay(by: RelativeTime, initialValue: A) -> Animation { 329 | return Animation(duration: by, constant: initialValue) * self 330 | } 331 | 332 | fileprivate func augmentCurve(curve: @escaping AnimationCurve) -> Animation { 333 | return Animation(duration: self.duration) { time in 334 | self.value(curve(time/self.duration)) 335 | } 336 | } 337 | } 338 | 339 | struct InterpretedAnimation { 340 | enum State { case running, done } 341 | let run: (RelativeTime) -> State 342 | 343 | init(animation: Animation, interpret: @escaping (A) -> ()) { 344 | run = { 345 | let result = animation.value($0) 346 | interpret(result) 347 | return $0 >= animation.duration ? .done : .running 348 | } 349 | } 350 | } 351 | 352 | final class Driver: NSObject { 353 | var displayLink: CADisplayLink! 354 | var animations: [(startTime: CFAbsoluteTime, InterpretedAnimation)] = [] 355 | var pendingAnimations: [InterpretedAnimation] = [] 356 | 357 | override init() { 358 | super.init() 359 | displayLink = CADisplayLink(target: self, selector: #selector(step(_:))) 360 | displayLink.add(to: RunLoop.main, forMode: .commonModes) 361 | } 362 | 363 | func add(_ animation: InterpretedAnimation) { 364 | pendingAnimations.append(animation) 365 | displayLink.isPaused = false 366 | } 367 | 368 | @objc func step(_ displayLink: CADisplayLink) { 369 | let time = displayLink.targetTimestamp 370 | for p in pendingAnimations { 371 | animations.append((startTime: time, p)) 372 | } 373 | pendingAnimations = [] 374 | var toBeRemoved: [Int] = [] 375 | for ((startTime: startTime, animation), index) in zip(animations, animations.indices) { 376 | let animationTime = time - startTime 377 | let result = animation.run(animationTime) 378 | if result == .done { 379 | toBeRemoved.append(index) 380 | } 381 | } 382 | for i in toBeRemoved.reversed() { 383 | animations.remove(at: i) 384 | } 385 | if animations.isEmpty { 386 | displayLink.isPaused = true 387 | } 388 | } 389 | 390 | deinit { 391 | displayLink.remove(from: .main, forMode: .commonModes) 392 | } 393 | } 394 | 395 | func dot(origin: CGPoint, size: CGSize = CGSize(width: 30, height: 30), backgroundColor: UIColor = .orange) -> UIView { 396 | let result = UIView(frame: CGRect(origin: origin, size: size)) 397 | result.backgroundColor = backgroundColor 398 | result.layer.cornerRadius = size.width/2 399 | result.layer.masksToBounds = true 400 | return result 401 | } 402 | 403 | class ViewController: UIViewController { 404 | var driver: Driver = Driver() 405 | 406 | @IBOutlet weak var redBox: UIView! 407 | override func viewDidLoad() { 408 | super.viewDidLoad() 409 | 410 | CATransaction.setDisableActions(true) 411 | } 412 | 413 | override func viewDidAppear(_ animated: Bool) { 414 | let dots = (0..<5).map { 415 | dot(origin: CGPoint(x: -15, y: 40 * (1 + $0))) 416 | } 417 | 418 | let rightDots = (0..<5).map { 419 | dot(origin: CGPoint(x: view.bounds.width-15, y: 40 * (1 + CGFloat($0)))) 420 | } 421 | 422 | func makeAnimation(from: CGFloat, to: CGFloat) -> Animation { 423 | let target = from + (to-from)*1.2 424 | return Animation(from: from, 425 | to: target, 426 | duration: 0.37, 427 | curve: linear) * 428 | Animation(from: target, to: to, duration: 0.63, curve: builtin(.easeOut)) 429 | } 430 | 431 | for (index, dot) in dots.enumerated() { 432 | view.addSubview(dot) 433 | let animation = makeAnimation(from: 0, to: 30).delay(by: Double(index) * 0.1, initialValue: 0) 434 | 435 | driver.pendingAnimations.append(InterpretedAnimation(animation: animation, interpret: { value in 436 | dot.transform = CGAffineTransform(translationX: value, y: 0) 437 | })) 438 | } 439 | 440 | 441 | for (index, dot) in rightDots.enumerated() { 442 | view.addSubview(dot) 443 | let animation = makeAnimation(from: 0, to: -30).changeSpeed(factor: 1-(0.1*Double(index))) 444 | driver.add(InterpretedAnimation(animation: animation, interpret: { x in 445 | dot.transform = CGAffineTransform(translationX: x, y: 0) 446 | })) 447 | } 448 | 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /UnitDuration/Duration.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 833804F21F868AA500546301 /* TimingFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833804F11F868AA500546301 /* TimingFunctions.swift */; }; 11 | 835262921F80FD5600039B50 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835262911F80FD5600039B50 /* AppDelegate.swift */; }; 12 | 835262941F80FD5600039B50 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835262931F80FD5600039B50 /* ViewController.swift */; }; 13 | 835262971F80FD5600039B50 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 835262951F80FD5600039B50 /* Main.storyboard */; }; 14 | 835262991F80FD5600039B50 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 835262981F80FD5600039B50 /* Assets.xcassets */; }; 15 | 8352629C1F80FD5600039B50 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 833804F11F868AA500546301 /* TimingFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimingFunctions.swift; sourceTree = ""; }; 20 | 8352628E1F80FD5600039B50 /* Duration.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Duration.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 835262911F80FD5600039B50 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 835262931F80FD5600039B50 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | 835262961F80FD5600039B50 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 835262981F80FD5600039B50 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 8352629B1F80FD5600039B50 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | 8352629D1F80FD5600039B50 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 8352628B1F80FD5600039B50 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 835262851F80FD5600039B50 = { 41 | isa = PBXGroup; 42 | children = ( 43 | 835262901F80FD5600039B50 /* Duration */, 44 | 8352628F1F80FD5600039B50 /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 8352628F1F80FD5600039B50 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 8352628E1F80FD5600039B50 /* Duration.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 835262901F80FD5600039B50 /* Duration */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 835262911F80FD5600039B50 /* AppDelegate.swift */, 60 | 833804F11F868AA500546301 /* TimingFunctions.swift */, 61 | 835262931F80FD5600039B50 /* ViewController.swift */, 62 | 835262951F80FD5600039B50 /* Main.storyboard */, 63 | 835262981F80FD5600039B50 /* Assets.xcassets */, 64 | 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */, 65 | 8352629D1F80FD5600039B50 /* Info.plist */, 66 | ); 67 | path = Duration; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | 8352628D1F80FD5600039B50 /* Duration */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = 835262A01F80FD5600039B50 /* Build configuration list for PBXNativeTarget "Duration" */; 76 | buildPhases = ( 77 | 8352628A1F80FD5600039B50 /* Sources */, 78 | 8352628B1F80FD5600039B50 /* Frameworks */, 79 | 8352628C1F80FD5600039B50 /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = Duration; 86 | productName = AnimationTesting; 87 | productReference = 8352628E1F80FD5600039B50 /* Duration.app */; 88 | productType = "com.apple.product-type.application"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 835262861F80FD5600039B50 /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastSwiftUpdateCheck = 0900; 97 | LastUpgradeCheck = 0900; 98 | ORGANIZATIONNAME = objc.io; 99 | TargetAttributes = { 100 | 8352628D1F80FD5600039B50 = { 101 | CreatedOnToolsVersion = 9.0; 102 | ProvisioningStyle = Automatic; 103 | }; 104 | }; 105 | }; 106 | buildConfigurationList = 835262891F80FD5600039B50 /* Build configuration list for PBXProject "Duration" */; 107 | compatibilityVersion = "Xcode 8.0"; 108 | developmentRegion = en; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | Base, 113 | ); 114 | mainGroup = 835262851F80FD5600039B50; 115 | productRefGroup = 8352628F1F80FD5600039B50 /* Products */; 116 | projectDirPath = ""; 117 | projectRoot = ""; 118 | targets = ( 119 | 8352628D1F80FD5600039B50 /* Duration */, 120 | ); 121 | }; 122 | /* End PBXProject section */ 123 | 124 | /* Begin PBXResourcesBuildPhase section */ 125 | 8352628C1F80FD5600039B50 /* Resources */ = { 126 | isa = PBXResourcesBuildPhase; 127 | buildActionMask = 2147483647; 128 | files = ( 129 | 8352629C1F80FD5600039B50 /* LaunchScreen.storyboard in Resources */, 130 | 835262991F80FD5600039B50 /* Assets.xcassets in Resources */, 131 | 835262971F80FD5600039B50 /* Main.storyboard in Resources */, 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXResourcesBuildPhase section */ 136 | 137 | /* Begin PBXSourcesBuildPhase section */ 138 | 8352628A1F80FD5600039B50 /* Sources */ = { 139 | isa = PBXSourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 833804F21F868AA500546301 /* TimingFunctions.swift in Sources */, 143 | 835262941F80FD5600039B50 /* ViewController.swift in Sources */, 144 | 835262921F80FD5600039B50 /* AppDelegate.swift in Sources */, 145 | ); 146 | runOnlyForDeploymentPostprocessing = 0; 147 | }; 148 | /* End PBXSourcesBuildPhase section */ 149 | 150 | /* Begin PBXVariantGroup section */ 151 | 835262951F80FD5600039B50 /* Main.storyboard */ = { 152 | isa = PBXVariantGroup; 153 | children = ( 154 | 835262961F80FD5600039B50 /* Base */, 155 | ); 156 | name = Main.storyboard; 157 | sourceTree = ""; 158 | }; 159 | 8352629A1F80FD5600039B50 /* LaunchScreen.storyboard */ = { 160 | isa = PBXVariantGroup; 161 | children = ( 162 | 8352629B1F80FD5600039B50 /* Base */, 163 | ); 164 | name = LaunchScreen.storyboard; 165 | sourceTree = ""; 166 | }; 167 | /* End PBXVariantGroup section */ 168 | 169 | /* Begin XCBuildConfiguration section */ 170 | 8352629E1F80FD5600039B50 /* Debug */ = { 171 | isa = XCBuildConfiguration; 172 | buildSettings = { 173 | ALWAYS_SEARCH_USER_PATHS = NO; 174 | CLANG_ANALYZER_NONNULL = YES; 175 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 176 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 177 | CLANG_CXX_LIBRARY = "libc++"; 178 | CLANG_ENABLE_MODULES = YES; 179 | CLANG_ENABLE_OBJC_ARC = YES; 180 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_COMMA = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 185 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 186 | CLANG_WARN_EMPTY_BODY = YES; 187 | CLANG_WARN_ENUM_CONVERSION = YES; 188 | CLANG_WARN_INFINITE_RECURSION = YES; 189 | CLANG_WARN_INT_CONVERSION = YES; 190 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 191 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 193 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 194 | CLANG_WARN_STRICT_PROTOTYPES = YES; 195 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 196 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 197 | CLANG_WARN_UNREACHABLE_CODE = YES; 198 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 199 | CODE_SIGN_IDENTITY = "iPhone Developer"; 200 | COPY_PHASE_STRIP = NO; 201 | DEBUG_INFORMATION_FORMAT = dwarf; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | ENABLE_TESTABILITY = YES; 204 | GCC_C_LANGUAGE_STANDARD = gnu11; 205 | GCC_DYNAMIC_NO_PIC = NO; 206 | GCC_NO_COMMON_BLOCKS = YES; 207 | GCC_OPTIMIZATION_LEVEL = 0; 208 | GCC_PREPROCESSOR_DEFINITIONS = ( 209 | "DEBUG=1", 210 | "$(inherited)", 211 | ); 212 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 213 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 214 | GCC_WARN_UNDECLARED_SELECTOR = YES; 215 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 216 | GCC_WARN_UNUSED_FUNCTION = YES; 217 | GCC_WARN_UNUSED_VARIABLE = YES; 218 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 219 | MTL_ENABLE_DEBUG_INFO = YES; 220 | ONLY_ACTIVE_ARCH = YES; 221 | SDKROOT = iphoneos; 222 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 223 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 224 | }; 225 | name = Debug; 226 | }; 227 | 8352629F1F80FD5600039B50 /* Release */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | ALWAYS_SEARCH_USER_PATHS = NO; 231 | CLANG_ANALYZER_NONNULL = YES; 232 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 233 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 234 | CLANG_CXX_LIBRARY = "libc++"; 235 | CLANG_ENABLE_MODULES = YES; 236 | CLANG_ENABLE_OBJC_ARC = YES; 237 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 238 | CLANG_WARN_BOOL_CONVERSION = YES; 239 | CLANG_WARN_COMMA = YES; 240 | CLANG_WARN_CONSTANT_CONVERSION = YES; 241 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 242 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 243 | CLANG_WARN_EMPTY_BODY = YES; 244 | CLANG_WARN_ENUM_CONVERSION = YES; 245 | CLANG_WARN_INFINITE_RECURSION = YES; 246 | CLANG_WARN_INT_CONVERSION = YES; 247 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 251 | CLANG_WARN_STRICT_PROTOTYPES = YES; 252 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 254 | CLANG_WARN_UNREACHABLE_CODE = YES; 255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 256 | CODE_SIGN_IDENTITY = "iPhone Developer"; 257 | COPY_PHASE_STRIP = NO; 258 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 259 | ENABLE_NS_ASSERTIONS = NO; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | GCC_C_LANGUAGE_STANDARD = gnu11; 262 | GCC_NO_COMMON_BLOCKS = YES; 263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 265 | GCC_WARN_UNDECLARED_SELECTOR = YES; 266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 267 | GCC_WARN_UNUSED_FUNCTION = YES; 268 | GCC_WARN_UNUSED_VARIABLE = YES; 269 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 270 | MTL_ENABLE_DEBUG_INFO = NO; 271 | SDKROOT = iphoneos; 272 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 273 | VALIDATE_PRODUCT = YES; 274 | }; 275 | name = Release; 276 | }; 277 | 835262A11F80FD5600039B50 /* Debug */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 281 | CODE_SIGN_STYLE = Automatic; 282 | DEVELOPMENT_TEAM = SL7736T2Z4; 283 | INFOPLIST_FILE = Duration/Info.plist; 284 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 285 | PRODUCT_BUNDLE_IDENTIFIER = io.objcio.AnimationTesting; 286 | PRODUCT_NAME = "$(TARGET_NAME)"; 287 | SWIFT_VERSION = 4.0; 288 | TARGETED_DEVICE_FAMILY = "1,2"; 289 | }; 290 | name = Debug; 291 | }; 292 | 835262A21F80FD5600039B50 /* Release */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 296 | CODE_SIGN_STYLE = Automatic; 297 | DEVELOPMENT_TEAM = SL7736T2Z4; 298 | INFOPLIST_FILE = Duration/Info.plist; 299 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 300 | PRODUCT_BUNDLE_IDENTIFIER = io.objcio.AnimationTesting; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | SWIFT_VERSION = 4.0; 303 | TARGETED_DEVICE_FAMILY = "1,2"; 304 | }; 305 | name = Release; 306 | }; 307 | /* End XCBuildConfiguration section */ 308 | 309 | /* Begin XCConfigurationList section */ 310 | 835262891F80FD5600039B50 /* Build configuration list for PBXProject "Duration" */ = { 311 | isa = XCConfigurationList; 312 | buildConfigurations = ( 313 | 8352629E1F80FD5600039B50 /* Debug */, 314 | 8352629F1F80FD5600039B50 /* Release */, 315 | ); 316 | defaultConfigurationIsVisible = 0; 317 | defaultConfigurationName = Release; 318 | }; 319 | 835262A01F80FD5600039B50 /* Build configuration list for PBXNativeTarget "Duration" */ = { 320 | isa = XCConfigurationList; 321 | buildConfigurations = ( 322 | 835262A11F80FD5600039B50 /* Debug */, 323 | 835262A21F80FD5600039B50 /* Release */, 324 | ); 325 | defaultConfigurationIsVisible = 0; 326 | defaultConfigurationName = Release; 327 | }; 328 | /* End XCConfigurationList section */ 329 | }; 330 | rootObject = 835262861F80FD5600039B50 /* Project object */; 331 | } 332 | -------------------------------------------------------------------------------- /UnitDuration/Duration/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AnimationTesting 4 | // 5 | // Created by Chris Eidhof on 01.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /UnitDuration/Duration/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /UnitDuration/Duration/Backup.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// ViewController.swift 3 | //// AnimationTesting 4 | //// 5 | //// Created by Chris Eidhof on 01.10.17. 6 | //// Copyright © 2017 objc.io. All rights reserved. 7 | //// 8 | // 9 | //import UIKit 10 | // 11 | ////: Playground - noun: a place where people can play 12 | // 13 | //import UIKit 14 | // 15 | //struct Animation { 16 | // let value: (RelativeTime) -> A 17 | // 18 | // init(_ value: @escaping (RelativeTime) -> A) { 19 | // self.value = value 20 | // } 21 | // 22 | // init(constant: A) { 23 | // self.value = { _ in constant } 24 | // } 25 | // 26 | //} 27 | // 28 | //extension Animation where A == () { 29 | // static let one = Animation<()>() 30 | // 31 | // init() { 32 | // self.value = { _ in () } 33 | // } 34 | // 35 | //} 36 | // 37 | //typealias Progress = Double 38 | // 39 | //typealias AnimationCurve = (Progress) -> Progress 40 | // 41 | //extension Double { 42 | // func clamped(to: ClosedRange) -> Double { 43 | // if self < to.lowerBound { return to.lowerBound } 44 | // if self > to.upperBound { return to.upperBound } 45 | // return self 46 | // } 47 | //} 48 | //func linear(_ progress: Progress) -> Progress { 49 | // return progress 50 | //} 51 | // 52 | //func quadratic(_ progress: Progress) -> Progress { 53 | // return Progress(progress * progress) 54 | //} 55 | // 56 | //func cubic(_ progress: Progress) -> Progress { 57 | // return Progress(progress * progress * progress) 58 | //} 59 | // 60 | //func spring(damping b: Double = 10, mass m: Double = 1, stiffness k: Double = 100, velocity v0: Double = 0) -> AnimationCurve { 61 | // let beta: Double = b / (2*m) 62 | // let omega0: Double = sqrt(k/m) 63 | // let omega1: Double = sqrt((omega0 * omega0) - (beta * beta)) 64 | // let omega2: Double = sqrt((beta * beta) - (omega0 * omega0)) 65 | // 66 | // let x0: Double = -1 67 | // //if (!self.allowsOverdamping && beta > omega0) beta = omega0; 68 | // if beta < omega0 { 69 | // return { t in 70 | // let envelope: Double = exp(-beta * t) 71 | // return -x0 + envelope * (x0 * cos(omega1 * t) + ((beta * x0 + v0) / omega1) * sin(omega1 * t)) 72 | // } 73 | // } else if (beta == omega0) { 74 | // return { t in 75 | // let envelope = exp(-beta * t) 76 | // return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 77 | // } 78 | // } else { 79 | // // Overdamped 80 | // return { t in 81 | // let envelope = exp(-beta * t); 82 | // return -x0 + envelope * (x0 * cosh(omega2 * t) + ((beta * x0 + v0) / omega2) * sinh(omega2 * t)); 83 | // } 84 | // } 85 | //} 86 | // 87 | //extension Animation where A == CGFloat { 88 | // init(from start: CGFloat, speed: CGFloat) { 89 | // value = { time in 90 | // return start + CGFloat(time)*speed 91 | // } 92 | // } 93 | // 94 | //} 95 | // 96 | //extension Animation { 97 | // func parallel(_ other: Animation, _ transform: @escaping (A,B) -> C) -> Animation { 98 | // return Animation({ time in 99 | // let a = self.value(time) 100 | // let b = other.value(time) 101 | // return transform(a,b) 102 | // }) 103 | // } 104 | //} 105 | // 106 | //// RelativeTime is never less than zero 107 | //typealias RelativeTime = CFAbsoluteTime 108 | // 109 | //// Parallel Composition 110 | //func +(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 111 | // return lhs.parallel(rhs) { ($0, $1) } 112 | //} 113 | // 114 | //// Sequential Composition 115 | ////func *(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 116 | //// return Animation({ time in 117 | //// let a = lhs.value(time) 118 | //// let b = rhs.value(time-lhs.duration) 119 | //// return (a,b) 120 | //// }) 121 | ////} 122 | // 123 | ////extension Animation { 124 | //// func delay(by: RelativeTime) -> Animation { 125 | //// return (Animation<()>(duration: by) * self).map { $0.1 } 126 | //// } 127 | ////} 128 | // 129 | //extension Animation { 130 | // func map(_ f: @escaping (A) -> B) -> Animation { 131 | // return Animation({ f(self.value($0))}) 132 | // } 133 | // 134 | // func mapp(_ f: @escaping (RelativeTime, A) -> B) -> Animation { 135 | // return Animation { time in 136 | // f(time, self.value(time)) 137 | // } 138 | // } 139 | // 140 | //} 141 | // 142 | //extension Animation where A == CGFloat { 143 | // func addSpeed(_ speed: A) -> Animation { 144 | // return mapp { time, value in value + CGFloat(time)*speed } 145 | // } 146 | // 147 | // init(from: CGFloat, to: CGFloat, duration: TimeInterval, curve: @escaping AnimationCurve = linear) { 148 | // let speed = (to-from)/CGFloat(duration) 149 | // self = Animation(constant: from).addSpeed(speed).after(duration: duration, switchTo: { endValue in Animation(constant: endValue) }).map { $0.value }.augmentCurve(curve: { time in curve(time/duration)}) 150 | // } 151 | //} 152 | // 153 | //extension Animation { 154 | // // func changeSpeed(factor: RelativeTime) -> Animation { 155 | // // return Animation(duration: duration / factor, { time in 156 | // // self.value(time * factor) 157 | // // }) 158 | // // } 159 | // // 160 | // // var halfSpeed: Animation { 161 | // // return changeSpeed(factor: 0.5) 162 | // // } 163 | // 164 | // fileprivate func augmentCurve(curve: @escaping AnimationCurve) -> Animation { 165 | // return Animation { time in 166 | // return self.value(curve(time)) 167 | // } 168 | // } 169 | //} 170 | // 171 | //enum Either { 172 | // case left(A) 173 | // case right(B) 174 | //} 175 | // 176 | //extension Animation { 177 | // func after(duration: RelativeTime, switchTo other: Animation) -> Animation> { 178 | // return Animation> { time in 179 | // if time < duration { return .left(self.value(time)) } 180 | // else { return .right(other.value(time)) } 181 | // } 182 | // } 183 | // 184 | // func after(duration: RelativeTime, switchTo other: @escaping (A) -> Animation) -> Animation> { 185 | // return Animation> { time in 186 | // if time < duration { 187 | // return .left(self.value(time)) 188 | // } 189 | // else { 190 | // let otherValue = other(self.value(duration)) 191 | // return .right(otherValue.value(time-duration)) 192 | // } 193 | // } 194 | // } 195 | // 196 | //} 197 | // 198 | // 199 | // 200 | //final class Driver: NSObject { 201 | // var displayLink: CADisplayLink! 202 | // var animate: ((CFAbsoluteTime) -> ())! 203 | // 204 | // init(animation: Animation, _ interpret: @escaping (A) -> ()) { 205 | // super.init() 206 | // displayLink = CADisplayLink(target: self, selector: #selector(step(_:))) 207 | // displayLink.add(to: RunLoop.main, forMode: .commonModes) 208 | // var startTime: CFAbsoluteTime? = nil 209 | // self.animate = { [unowned self] nextTime in 210 | // if startTime == nil { startTime = nextTime } 211 | // let time = nextTime - startTime! 212 | // let value = animation.value(time) 213 | // interpret(value) 214 | // // if time >= animation.duration { 215 | // //// print((time, animation.duration)) 216 | // //// self.displayLink.isPaused = true 217 | // // } 218 | // } 219 | // } 220 | // 221 | // 222 | // 223 | // @objc func step(_ displayLink: CADisplayLink) { 224 | // animate(displayLink.targetTimestamp) 225 | // } 226 | //} 227 | // 228 | //extension Either where A == B { 229 | // var value: A { 230 | // switch self { 231 | // case .left(let x): return x 232 | // case .right(let y): return y 233 | // } 234 | // } 235 | //} 236 | // 237 | //class ViewController: UIViewController { 238 | // var driver: Driver? 239 | // 240 | // @IBOutlet weak var redBox: UIView! 241 | // override func viewDidLoad() { 242 | // super.viewDidLoad() 243 | // CATransaction.setDisableActions(true) 244 | // 245 | // redBox.frame.origin = .zero 246 | // let singlePoint = Animation(from: 0, to: 100, duration: 2).after(duration: 4, switchTo: { end in Animation(from: end, to: 0, duration: 2) }).map{ $0.value } 247 | // let other = singlePoint.augmentCurve(curve: { t in spring()(t / 4) } ) 248 | // let point = (singlePoint + other).map { CGPoint(x: $0.0, y: $0.1 )} 249 | // let origin = point 250 | // let animation = (origin).map { result in 251 | // CGAffineTransform(translationX: result.x, y: result.y) 252 | // } 253 | // 254 | // driver = Driver(animation: animation, { [unowned self] transform in 255 | // self.redBox.transform = transform 256 | // }) 257 | // } 258 | // 259 | // override func didReceiveMemoryWarning() { 260 | // super.didReceiveMemoryWarning() 261 | // // Dispose of any resources that can be recreated. 262 | // } 263 | // 264 | // 265 | //} 266 | // 267 | // 268 | -------------------------------------------------------------------------------- /UnitDuration/Duration/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /UnitDuration/Duration/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /UnitDuration/Duration/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /UnitDuration/Duration/TimingFunctions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimingFunctions.swift 3 | // Duration 4 | // 5 | // Created by Chris Eidhof on 05.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias Progress = Double 12 | 13 | typealias AnimationCurve = (Progress) -> Progress 14 | 15 | extension Double { 16 | func clamped(to: ClosedRange) -> Double { 17 | if self < to.lowerBound { return to.lowerBound } 18 | if self > to.upperBound { return to.upperBound } 19 | return self 20 | } 21 | } 22 | 23 | struct TimingFunction { 24 | enum Builtin { 25 | case linear 26 | case easeIn 27 | case easeOut 28 | case easeInEaseOut 29 | case `default` 30 | 31 | var controlPoints: (Double,Double,Double,Double) { 32 | switch self { 33 | case .linear: 34 | return (0, 0, 1, 1) 35 | case .easeIn: 36 | return (0.42, 0, 1, 1) 37 | case .easeOut: 38 | return (0, 0, 0.58, 1) 39 | case .easeInEaseOut: 40 | return (0.42, 0, 0.58, 1) 41 | case .`default`: 42 | return (0.25, 0.1, 0.25, 1) 43 | } 44 | } 45 | } 46 | 47 | 48 | // Taken from https://gist.github.com/raphaelschaad/6739676 49 | 50 | private var ax,bx,cx,ay,by,cy: Double 51 | init(controlPoints p1x: Double, _ p1y: Double, _ p2x: Double, _ p2y: Double) { 52 | cx = 3.0 * p1x 53 | bx = 3.0 * (p2x - p1x) - cx 54 | ax = 1.0 - cx - bx 55 | 56 | cy = 3.0 * p1y 57 | by = 3.0 * (p2y - p1y) - cy 58 | ay = 1.0 - cy - by 59 | } 60 | 61 | init(type: Builtin = .`default`) { 62 | let (p1, p2, p3, p4) = type.controlPoints 63 | self.init(controlPoints: p1, p2, p3, p4) 64 | } 65 | 66 | func value(x: Double) -> Double { 67 | let xSolved = solve(curveX: x, epsilon: epsilon) 68 | return sampleCurveY(t: xSolved) 69 | } 70 | 71 | let duration: Double = 1 72 | 73 | private var epsilon: Double { 74 | return 1 / (200*duration) 75 | } 76 | 77 | 78 | private func sampleCurveX(t: Double) -> Double { 79 | return ((ax * t + bx) * t + cx) * t 80 | } 81 | 82 | private func sampleCurveY(t: Double) -> Double { 83 | return ((ay * t + by) * t + cy) * t; 84 | } 85 | 86 | private func sampleCurveDerivativeX(t: Double) -> Double { 87 | return (3.0 * ax * t + 2.0 * bx) * t + cx 88 | } 89 | 90 | private func solve(curveX x: Double, epsilon: Double) -> Double { 91 | var t2, x2, d2: Double 92 | 93 | // First try a few iterations of Newton's method -- normally very fast. 94 | t2 = x 95 | for _ in 0..<8 { 96 | x2 = sampleCurveX(t: t2) - x 97 | if (fabs(x2) < epsilon) { 98 | return t2 99 | } 100 | d2 = sampleCurveDerivativeX(t: t2) 101 | if (fabs(d2) < 1e-6) { 102 | break 103 | } 104 | t2 = t2 - x2 / d2; 105 | } 106 | 107 | // Fall back to the bisection method for reliability. 108 | var t0: Double = 0.0 109 | var t1: Double = 1.0 110 | t2 = x 111 | 112 | if (t2 < t0) { 113 | return t0 114 | } 115 | if (t2 > t1) { 116 | return t1 117 | } 118 | 119 | while (t0 < t1) { 120 | x2 = sampleCurveX(t: t2) 121 | if (fabs(x2 - x) < epsilon) { 122 | return t2 123 | } 124 | if (x > x2) { 125 | t0 = t2 126 | } else { 127 | t1 = t2 128 | } 129 | t2 = (t1 - t0) * 0.5 + t0 130 | } 131 | 132 | // Failure. 133 | return t2 134 | } 135 | } 136 | 137 | func linear(_ progress: Progress) -> Progress { 138 | return progress 139 | } 140 | 141 | func quadratic(_ progress: Progress) -> Progress { 142 | return Progress(progress * progress) 143 | } 144 | 145 | func cubic(_ progress: Progress) -> Progress { 146 | return Progress(progress * progress * progress) 147 | } 148 | 149 | func builtin(_ b: TimingFunction.Builtin) -> AnimationCurve { 150 | let tf = TimingFunction(type: b) 151 | return tf.value 152 | } 153 | 154 | func spring(damping b: Double = 10, mass m: Double = 1, stiffness k: Double = 100, velocity v0: Double = 0) -> AnimationCurve { 155 | let beta: Double = b / (2*m) 156 | let omega0: Double = sqrt(k/m) 157 | let omega1: Double = sqrt((omega0 * omega0) - (beta * beta)) 158 | let omega2: Double = sqrt((beta * beta) - (omega0 * omega0)) 159 | 160 | let x0: Double = -1 161 | //if (!self.allowsOverdamping && beta > omega0) beta = omega0; 162 | if beta < omega0 { 163 | return { t in 164 | let envelope: Double = exp(-beta * t) 165 | return -x0 + envelope * (x0 * cos(omega1 * t) + ((beta * x0 + v0) / omega1) * sin(omega1 * t)) 166 | } 167 | } else if (beta == omega0) { 168 | return { t in 169 | let envelope = exp(-beta * t) 170 | return -x0 + envelope * (x0 + (beta * x0 + v0) * t) 171 | } 172 | } else { 173 | // Overdamped 174 | return { t in 175 | let envelope = exp(-beta * t); 176 | return -x0 + envelope * (x0 * cosh(omega2 * t) + ((beta * x0 + v0) / omega2) * sinh(omega2 * t)); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /UnitDuration/Duration/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // AnimationTesting 4 | // 5 | // Created by Chris Eidhof on 01.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum Either { 12 | case left(A) 13 | case right(B) 14 | } 15 | 16 | extension Either where A == B { 17 | var value: A { 18 | switch self { 19 | case .left(let x): return x 20 | case .right(let y): return y 21 | } 22 | } 23 | } 24 | 25 | struct Animation { 26 | let value: (RelativeTime) -> A 27 | 28 | init(_ value: @escaping (RelativeTime) -> A) { 29 | self.value = value 30 | } 31 | 32 | init(constant: A) { 33 | self.value = { _ in constant } 34 | } 35 | } 36 | 37 | extension Animation { 38 | func parallel(_ other: Animation, _ transform: @escaping (A,B) -> C) -> Animation { 39 | return Animation { time in 40 | let a = self.value(time) 41 | let b = other.value(time) 42 | return transform(a,b) 43 | } 44 | } 45 | 46 | // ratio is the duration of self, other.duration is 1-ratio 47 | func sequential(ratio: RelativeTime = 0.5, _ other: Animation) -> Animation> { 48 | return Animation> { time in 49 | return time <= ratio ? .left(self.value(time/ratio)) : .right(other.value((time-ratio)/(1-ratio))) 50 | } 51 | } 52 | 53 | func sequential(ratio: RelativeTime = 0.5, _ other: @escaping (A) -> Animation) -> Animation> { 54 | let endValue = self.value(1) 55 | return Animation> { time in 56 | return time <= ratio ? .left(self.value(time/ratio)) : .right(other(endValue).value((time-ratio)/(1-ratio))) 57 | } 58 | } 59 | 60 | } 61 | 62 | // RelativeTime is never less than zero 63 | typealias RelativeTime = CFAbsoluteTime 64 | 65 | // Parallel Composition 66 | func +(lhs: Animation, rhs: Animation) -> Animation<(A,B)> { 67 | return lhs.parallel(rhs) { ($0, $1) } 68 | } 69 | 70 | // Sequential Composition 71 | func *(lhs: Animation, rhs: Animation) -> Animation> { 72 | return lhs.sequential(rhs) 73 | } 74 | 75 | func *(lhs: Animation, rhs: Animation) -> Animation { 76 | return (lhs * rhs).map { $0.value } 77 | } 78 | 79 | extension Animation { 80 | func map(_ f: @escaping (A) -> B) -> Animation { 81 | return Animation { f(self.value($0)) } 82 | } 83 | 84 | func mapWithTime(_ f: @escaping (RelativeTime, A) -> B) -> Animation { 85 | return Animation { time in 86 | return f(time, self.value(time)) 87 | } 88 | } 89 | } 90 | 91 | extension Animation where A == CGFloat { 92 | func addSpeed(_ speed: A) -> Animation { 93 | return mapWithTime { time, value in 94 | return value + CGFloat(time)*speed 95 | } 96 | } 97 | 98 | init(from: CGFloat, to: CGFloat, curve: @escaping AnimationCurve = linear) { 99 | // todo curve 100 | let diff = to-from 101 | self = Animation { 102 | return $0 >= 1 ? to : from + diff*CGFloat($0) 103 | }.augmentCurve(curve: curve) 104 | } 105 | } 106 | 107 | extension Animation { 108 | func delay(by: RelativeTime, initialValue: A) -> Animation { 109 | precondition((0.0...1.0).contains(by)) 110 | return Animation(constant: initialValue).sequential(ratio: by, self).map { $0.value } 111 | } 112 | 113 | fileprivate func augmentCurve(curve: @escaping AnimationCurve) -> Animation { 114 | return Animation { time in 115 | self.value(curve(time)) 116 | } 117 | } 118 | } 119 | 120 | struct InterpretedAnimation { 121 | enum State { case running, done } 122 | let run: (RelativeTime) -> State 123 | 124 | init(animation: Animation, duration: RelativeTime, interpret: @escaping (A) -> ()) { 125 | run = { 126 | let result = animation.value($0 / duration) 127 | interpret(result) 128 | return $0 >= duration ? .done : .running 129 | } 130 | } 131 | } 132 | 133 | final class Driver: NSObject { 134 | var displayLink: CADisplayLink! 135 | var animations: [(startTime: CFAbsoluteTime, InterpretedAnimation)] = [] 136 | var pendingAnimations: [InterpretedAnimation] = [] 137 | 138 | override init() { 139 | super.init() 140 | displayLink = CADisplayLink(target: self, selector: #selector(step(_:))) 141 | displayLink.add(to: RunLoop.main, forMode: .commonModes) 142 | } 143 | 144 | func add(_ animation: InterpretedAnimation) { 145 | pendingAnimations.append(animation) 146 | displayLink.isPaused = false 147 | } 148 | 149 | @objc func step(_ displayLink: CADisplayLink) { 150 | let time = displayLink.targetTimestamp 151 | for p in pendingAnimations { 152 | animations.append((startTime: time, p)) 153 | } 154 | pendingAnimations = [] 155 | var toBeRemoved: [Int] = [] 156 | for ((startTime: startTime, animation), index) in zip(animations, animations.indices) { 157 | let animationTime = time - startTime 158 | let result = animation.run(animationTime) 159 | if result == .done { 160 | toBeRemoved.append(index) 161 | } 162 | } 163 | for i in toBeRemoved.reversed() { 164 | animations.remove(at: i) 165 | } 166 | if animations.isEmpty { 167 | displayLink.isPaused = true 168 | } 169 | } 170 | 171 | deinit { 172 | displayLink.remove(from: .main, forMode: .commonModes) 173 | } 174 | } 175 | 176 | func dot(origin: CGPoint, size: CGSize = CGSize(width: 30, height: 30), backgroundColor: UIColor = .orange) -> UIView { 177 | let result = UIView(frame: CGRect(origin: origin, size: size)) 178 | result.backgroundColor = backgroundColor 179 | result.layer.cornerRadius = size.width/2 180 | result.layer.masksToBounds = true 181 | return result 182 | } 183 | 184 | class ViewController: UIViewController { 185 | var driver: Driver = Driver() 186 | 187 | override func viewDidLoad() { 188 | super.viewDidLoad() 189 | CATransaction.setDisableActions(true) 190 | } 191 | 192 | override func viewDidAppear(_ animated: Bool) { 193 | let dots = (0..<5).map { 194 | dot(origin: CGPoint(x: -15, y: 40 * (1 + $0))) 195 | } 196 | 197 | let rightDots = (0..<5).map { 198 | dot(origin: CGPoint(x: view.bounds.width-15, y: 40 * (1 + CGFloat($0)))) 199 | } 200 | 201 | func makeAnimation(from: CGFloat, to: CGFloat) -> Animation { 202 | return Animation(from: from, 203 | to: from + (to-from)*1.2, 204 | curve: builtin(.easeIn)).sequential(ratio: 0.37, { t in 205 | Animation(from: t, to: to, curve: builtin(.easeOut)) 206 | }).map { $0.value } 207 | } 208 | 209 | for (index, dot) in dots.enumerated() { 210 | view.addSubview(dot) 211 | let animation = makeAnimation(from: 0, to: 30).delay(by: Double(index) * 0.1, initialValue: 0) 212 | 213 | driver.pendingAnimations.append(InterpretedAnimation(animation: animation, duration: 3, interpret: { value in 214 | dot.transform = CGAffineTransform(translationX: value, y: 0) 215 | })) 216 | } 217 | 218 | 219 | for (index, dot) in rightDots.enumerated() { 220 | view.addSubview(dot) 221 | let animation = makeAnimation(from: 0, to: -30) 222 | driver.add(InterpretedAnimation(animation: animation, duration: 1 + 0.1*Double(index), interpret: { x in 223 | dot.transform = CGAffineTransform(translationX: x, y: 0) 224 | })) 225 | } 226 | 227 | } 228 | } 229 | --------------------------------------------------------------------------------