├── .gitignore ├── Control-Panel-Interactive-Animation ├── ControlPanel.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ │ └── seedante.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── seedante.xcuserdatad │ │ └── xcschemes │ │ ├── InteractiveAnimation.xcscheme │ │ └── xcschememanagement.plist └── InteractiveAnimation │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift ├── ControlPanelInteractiveAnimation.gif ├── LICENSE ├── README.md └── iOS10-Control-Panel-Interactive-Animation ├── iOS10ControlPanel.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── seedante.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── seedante.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── iOS10ControlPane.xcscheme │ └── xcschememanagement.plist └── iOS10InteractiveAnimation ├── AppDelegate.swift ├── Assets.xcassets └── AppIcon.appiconset │ └── Contents.json ├── Base.lproj ├── LaunchScreen.storyboard └── Main.storyboard ├── Info.plist ├── ViewControllerOne.swift ├── ViewControllerTwo.swift └── ViewControllerZero.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ##### OSX 2 | .DS_Store 3 | 4 | ##### Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | *.moved-aside 17 | DerivedData 18 | *.hmap 19 | *.ipa 20 | *.xcuserstate 21 | project.xcworkspace 22 | 23 | ##### node.js 24 | node_modules 25 | npm-debug.log 26 | 27 | ##### Code Editor Settings 28 | .idea/ 29 | .vscode/ 30 | 31 | ##### Android 32 | *.iml 33 | .gradle 34 | /local.properties 35 | /.idea/workspace.xml 36 | /.idea/libraries 37 | /build 38 | /captures 39 | .externalNativeBuild 40 | 41 | #### Selenium 42 | selenium-debug.log 43 | test/reports/* 44 | test/screenshots/* -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/ControlPanel.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FDFF07E11D1036F500F526A4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF07E01D1036F500F526A4 /* AppDelegate.swift */; }; 11 | FDFF07E31D1036F500F526A4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF07E21D1036F500F526A4 /* ViewController.swift */; }; 12 | FDFF07E61D1036F500F526A4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FDFF07E41D1036F500F526A4 /* Main.storyboard */; }; 13 | FDFF07E81D1036F500F526A4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FDFF07E71D1036F500F526A4 /* Assets.xcassets */; }; 14 | FDFF07EB1D1036F500F526A4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FDFF07E91D1036F500F526A4 /* LaunchScreen.storyboard */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | FDFF07DD1D1036F500F526A4 /* ControlPanel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ControlPanel.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | FDFF07E01D1036F500F526A4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 20 | FDFF07E21D1036F500F526A4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 21 | FDFF07E51D1036F500F526A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 22 | FDFF07E71D1036F500F526A4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23 | FDFF07EA1D1036F500F526A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 24 | FDFF07EC1D1036F500F526A4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | FDFF07DA1D1036F500F526A4 /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | ); 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXFrameworksBuildPhase section */ 36 | 37 | /* Begin PBXGroup section */ 38 | FDFF07D41D1036F500F526A4 = { 39 | isa = PBXGroup; 40 | children = ( 41 | FDFF07DF1D1036F500F526A4 /* InteractiveAnimation */, 42 | FDFF07DE1D1036F500F526A4 /* Products */, 43 | ); 44 | sourceTree = ""; 45 | }; 46 | FDFF07DE1D1036F500F526A4 /* Products */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | FDFF07DD1D1036F500F526A4 /* ControlPanel.app */, 50 | ); 51 | name = Products; 52 | sourceTree = ""; 53 | }; 54 | FDFF07DF1D1036F500F526A4 /* InteractiveAnimation */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | FDFF07E01D1036F500F526A4 /* AppDelegate.swift */, 58 | FDFF07E21D1036F500F526A4 /* ViewController.swift */, 59 | FDFF07E41D1036F500F526A4 /* Main.storyboard */, 60 | FDFF07E71D1036F500F526A4 /* Assets.xcassets */, 61 | FDFF07E91D1036F500F526A4 /* LaunchScreen.storyboard */, 62 | FDFF07EC1D1036F500F526A4 /* Info.plist */, 63 | ); 64 | path = InteractiveAnimation; 65 | sourceTree = ""; 66 | }; 67 | /* End PBXGroup section */ 68 | 69 | /* Begin PBXNativeTarget section */ 70 | FDFF07DC1D1036F500F526A4 /* ControlPanel */ = { 71 | isa = PBXNativeTarget; 72 | buildConfigurationList = FDFF07EF1D1036F500F526A4 /* Build configuration list for PBXNativeTarget "ControlPanel" */; 73 | buildPhases = ( 74 | FDFF07D91D1036F500F526A4 /* Sources */, 75 | FDFF07DA1D1036F500F526A4 /* Frameworks */, 76 | FDFF07DB1D1036F500F526A4 /* Resources */, 77 | ); 78 | buildRules = ( 79 | ); 80 | dependencies = ( 81 | ); 82 | name = ControlPanel; 83 | productName = InteractiveAnimation; 84 | productReference = FDFF07DD1D1036F500F526A4 /* ControlPanel.app */; 85 | productType = "com.apple.product-type.application"; 86 | }; 87 | /* End PBXNativeTarget section */ 88 | 89 | /* Begin PBXProject section */ 90 | FDFF07D51D1036F500F526A4 /* Project object */ = { 91 | isa = PBXProject; 92 | attributes = { 93 | LastSwiftUpdateCheck = 0730; 94 | LastUpgradeCheck = 0730; 95 | ORGANIZATIONNAME = seedante; 96 | TargetAttributes = { 97 | FDFF07DC1D1036F500F526A4 = { 98 | CreatedOnToolsVersion = 7.3.1; 99 | DevelopmentTeam = 6Z8XUV9H6J; 100 | DevelopmentTeamName = "Ye Zongyu (Personal Team)"; 101 | LastSwiftMigration = 0830; 102 | }; 103 | }; 104 | }; 105 | buildConfigurationList = FDFF07D81D1036F500F526A4 /* Build configuration list for PBXProject "ControlPanel" */; 106 | compatibilityVersion = "Xcode 3.2"; 107 | developmentRegion = English; 108 | hasScannedForEncodings = 0; 109 | knownRegions = ( 110 | en, 111 | Base, 112 | ); 113 | mainGroup = FDFF07D41D1036F500F526A4; 114 | productRefGroup = FDFF07DE1D1036F500F526A4 /* Products */; 115 | projectDirPath = ""; 116 | projectRoot = ""; 117 | targets = ( 118 | FDFF07DC1D1036F500F526A4 /* ControlPanel */, 119 | ); 120 | }; 121 | /* End PBXProject section */ 122 | 123 | /* Begin PBXResourcesBuildPhase section */ 124 | FDFF07DB1D1036F500F526A4 /* Resources */ = { 125 | isa = PBXResourcesBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | FDFF07EB1D1036F500F526A4 /* LaunchScreen.storyboard in Resources */, 129 | FDFF07E81D1036F500F526A4 /* Assets.xcassets in Resources */, 130 | FDFF07E61D1036F500F526A4 /* Main.storyboard in Resources */, 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXResourcesBuildPhase section */ 135 | 136 | /* Begin PBXSourcesBuildPhase section */ 137 | FDFF07D91D1036F500F526A4 /* Sources */ = { 138 | isa = PBXSourcesBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | FDFF07E31D1036F500F526A4 /* ViewController.swift in Sources */, 142 | FDFF07E11D1036F500F526A4 /* AppDelegate.swift in Sources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXSourcesBuildPhase section */ 147 | 148 | /* Begin PBXVariantGroup section */ 149 | FDFF07E41D1036F500F526A4 /* Main.storyboard */ = { 150 | isa = PBXVariantGroup; 151 | children = ( 152 | FDFF07E51D1036F500F526A4 /* Base */, 153 | ); 154 | name = Main.storyboard; 155 | sourceTree = ""; 156 | }; 157 | FDFF07E91D1036F500F526A4 /* LaunchScreen.storyboard */ = { 158 | isa = PBXVariantGroup; 159 | children = ( 160 | FDFF07EA1D1036F500F526A4 /* Base */, 161 | ); 162 | name = LaunchScreen.storyboard; 163 | sourceTree = ""; 164 | }; 165 | /* End PBXVariantGroup section */ 166 | 167 | /* Begin XCBuildConfiguration section */ 168 | FDFF07ED1D1036F500F526A4 /* Debug */ = { 169 | isa = XCBuildConfiguration; 170 | buildSettings = { 171 | ALWAYS_SEARCH_USER_PATHS = NO; 172 | CLANG_ANALYZER_NONNULL = YES; 173 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 174 | CLANG_CXX_LIBRARY = "libc++"; 175 | CLANG_ENABLE_MODULES = YES; 176 | CLANG_ENABLE_OBJC_ARC = YES; 177 | CLANG_WARN_BOOL_CONVERSION = YES; 178 | CLANG_WARN_CONSTANT_CONVERSION = YES; 179 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 180 | CLANG_WARN_EMPTY_BODY = YES; 181 | CLANG_WARN_ENUM_CONVERSION = YES; 182 | CLANG_WARN_INT_CONVERSION = YES; 183 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 184 | CLANG_WARN_UNREACHABLE_CODE = YES; 185 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 186 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 187 | COPY_PHASE_STRIP = NO; 188 | DEBUG_INFORMATION_FORMAT = dwarf; 189 | ENABLE_STRICT_OBJC_MSGSEND = YES; 190 | ENABLE_TESTABILITY = YES; 191 | GCC_C_LANGUAGE_STANDARD = gnu99; 192 | GCC_DYNAMIC_NO_PIC = NO; 193 | GCC_NO_COMMON_BLOCKS = YES; 194 | GCC_OPTIMIZATION_LEVEL = 0; 195 | GCC_PREPROCESSOR_DEFINITIONS = ( 196 | "DEBUG=1", 197 | "$(inherited)", 198 | ); 199 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 200 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 201 | GCC_WARN_UNDECLARED_SELECTOR = YES; 202 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 203 | GCC_WARN_UNUSED_FUNCTION = YES; 204 | GCC_WARN_UNUSED_VARIABLE = YES; 205 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 206 | MTL_ENABLE_DEBUG_INFO = YES; 207 | ONLY_ACTIVE_ARCH = YES; 208 | SDKROOT = iphoneos; 209 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 210 | TARGETED_DEVICE_FAMILY = "1,2"; 211 | }; 212 | name = Debug; 213 | }; 214 | FDFF07EE1D1036F500F526A4 /* Release */ = { 215 | isa = XCBuildConfiguration; 216 | buildSettings = { 217 | ALWAYS_SEARCH_USER_PATHS = NO; 218 | CLANG_ANALYZER_NONNULL = YES; 219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 220 | CLANG_CXX_LIBRARY = "libc++"; 221 | CLANG_ENABLE_MODULES = YES; 222 | CLANG_ENABLE_OBJC_ARC = YES; 223 | CLANG_WARN_BOOL_CONVERSION = YES; 224 | CLANG_WARN_CONSTANT_CONVERSION = YES; 225 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 226 | CLANG_WARN_EMPTY_BODY = YES; 227 | CLANG_WARN_ENUM_CONVERSION = YES; 228 | CLANG_WARN_INT_CONVERSION = YES; 229 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 230 | CLANG_WARN_UNREACHABLE_CODE = YES; 231 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 232 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 233 | COPY_PHASE_STRIP = NO; 234 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 235 | ENABLE_NS_ASSERTIONS = NO; 236 | ENABLE_STRICT_OBJC_MSGSEND = YES; 237 | GCC_C_LANGUAGE_STANDARD = gnu99; 238 | GCC_NO_COMMON_BLOCKS = YES; 239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 241 | GCC_WARN_UNDECLARED_SELECTOR = YES; 242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 243 | GCC_WARN_UNUSED_FUNCTION = YES; 244 | GCC_WARN_UNUSED_VARIABLE = YES; 245 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 246 | MTL_ENABLE_DEBUG_INFO = NO; 247 | SDKROOT = iphoneos; 248 | TARGETED_DEVICE_FAMILY = "1,2"; 249 | VALIDATE_PRODUCT = YES; 250 | }; 251 | name = Release; 252 | }; 253 | FDFF07F01D1036F500F526A4 /* Debug */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 257 | CODE_SIGN_IDENTITY = "iPhone Developer"; 258 | INFOPLIST_FILE = InteractiveAnimation/Info.plist; 259 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 260 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 261 | PRODUCT_BUNDLE_IDENTIFIER = seedante.InteractiveAnimation.UIViewVersion; 262 | PRODUCT_NAME = ControlPanel; 263 | SWIFT_VERSION = 3.0; 264 | }; 265 | name = Debug; 266 | }; 267 | FDFF07F11D1036F500F526A4 /* Release */ = { 268 | isa = XCBuildConfiguration; 269 | buildSettings = { 270 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 271 | CODE_SIGN_IDENTITY = "iPhone Developer"; 272 | INFOPLIST_FILE = InteractiveAnimation/Info.plist; 273 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 274 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 275 | PRODUCT_BUNDLE_IDENTIFIER = seedante.InteractiveAnimation.UIViewVersion; 276 | PRODUCT_NAME = ControlPanel; 277 | SWIFT_VERSION = 3.0; 278 | }; 279 | name = Release; 280 | }; 281 | /* End XCBuildConfiguration section */ 282 | 283 | /* Begin XCConfigurationList section */ 284 | FDFF07D81D1036F500F526A4 /* Build configuration list for PBXProject "ControlPanel" */ = { 285 | isa = XCConfigurationList; 286 | buildConfigurations = ( 287 | FDFF07ED1D1036F500F526A4 /* Debug */, 288 | FDFF07EE1D1036F500F526A4 /* Release */, 289 | ); 290 | defaultConfigurationIsVisible = 0; 291 | defaultConfigurationName = Release; 292 | }; 293 | FDFF07EF1D1036F500F526A4 /* Build configuration list for PBXNativeTarget "ControlPanel" */ = { 294 | isa = XCConfigurationList; 295 | buildConfigurations = ( 296 | FDFF07F01D1036F500F526A4 /* Debug */, 297 | FDFF07F11D1036F500F526A4 /* Release */, 298 | ); 299 | defaultConfigurationIsVisible = 0; 300 | defaultConfigurationName = Release; 301 | }; 302 | /* End XCConfigurationList section */ 303 | }; 304 | rootObject = FDFF07D51D1036F500F526A4 /* Project object */; 305 | } 306 | -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/ControlPanel.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/ControlPanel.xcodeproj/project.xcworkspace/xcuserdata/seedante.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedante/ControlPanelAnimation/d9b743c4f83fd60a0b02d565ba169a97837fe595/Control-Panel-Interactive-Animation/ControlPanel.xcodeproj/project.xcworkspace/xcuserdata/seedante.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/ControlPanel.xcodeproj/xcuserdata/seedante.xcuserdatad/xcschemes/InteractiveAnimation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/ControlPanel.xcodeproj/xcuserdata/seedante.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | InteractiveAnimation.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | FDFF07DC1D1036F500F526A4 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/InteractiveAnimation/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // InteractiveAnimation 4 | // 5 | // Created by seedante on 16/6/14. 6 | // Copyright © 2016年 seedante. 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 throttle down OpenGL ES frame rates. 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 inactive 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 | -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/InteractiveAnimation/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "83.5x83.5", 66 | "scale" : "2x" 67 | } 68 | ], 69 | "info" : { 70 | "version" : 1, 71 | "author" : "xcode" 72 | } 73 | } -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/InteractiveAnimation/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 | -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/InteractiveAnimation/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 | -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/InteractiveAnimation/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ControlPanel 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Control-Panel-Interactive-Animation/InteractiveAnimation/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // InteractiveAnimation 4 | // 5 | // Created by seedante on 16/6/14. 6 | // Copyright © 2016年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | /** 13 | Continue the topic "Interactive Animations" in https://www.objc.io/issues/12-animations/interactive-animations/ , 14 | use UIView Animation/Core Animation to implement control panel open/close animation, which is interactive, interruptible, smooth. 15 | */ 16 | 17 | class ViewController: UIViewController { 18 | var pan = UIPanGestureRecognizer() 19 | var tap = UITapGestureRecognizer() 20 | var panelView = UIView() 21 | var panelOpened = true 22 | 23 | ///0.5s is too short for interaction, you could set it longer for test. 24 | let duration: TimeInterval = 0.5 25 | let relayDuration: TimeInterval = 0.3 26 | let diff: CGFloat = 150 27 | var USE_COREANIMATION = false 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | // Do any additional setup after loading the view, typically from a nib. 32 | 33 | panelView.frame = view.bounds 34 | panelView.center.y = view.center.y * (3 - 0.5) 35 | panelView.backgroundColor = UIColor.gray 36 | panelView.layer.cornerRadius = 5 37 | view.addSubview(panelView) 38 | 39 | pan.addTarget(self, action: #selector(ViewController.handlePan(_:))) 40 | panelView.addGestureRecognizer(pan) 41 | tap.addTarget(self, action: #selector(ViewController.handleTap(_:))) 42 | panelView.addGestureRecognizer(tap) 43 | } 44 | 45 | override func viewDidAppear(_ animated: Bool) { 46 | super.viewDidAppear(animated) 47 | } 48 | 49 | override func didReceiveMemoryWarning() { 50 | super.didReceiveMemoryWarning() 51 | // Dispose of any resources that can be recreated. 52 | } 53 | 54 | func stopMoveAnimation(){ 55 | let currentPosition = (panelView.layer.presentation() as! CALayer).position 56 | panelView.layer.removeAllAnimations() 57 | panelView.layer.position = currentPosition 58 | } 59 | 60 | func handlePan(_ panGesture: UIPanGestureRecognizer){ 61 | switch panGesture.state { 62 | case .began: 63 | stopMoveAnimation() 64 | case .changed: 65 | let point = panGesture.translation(in: view) 66 | panelView.center = CGPoint(x: panelView.center.x, y: panelView.center.y + point.y) 67 | panGesture.setTranslation(CGPoint.zero, in: view) 68 | case .ended, .cancelled: 69 | let gestureVelocity = panGesture.velocity(in: view) 70 | let isUp = gestureVelocity.y < 0 ? true : false 71 | let targetY = isUp ? view.center.y + diff : view.center.y * 2.5 72 | let velocity = abs(gestureVelocity.y) / abs(panelView.center.y - targetY) 73 | 74 | // Relay leave speed. You should provide .AllowUserInteraction option otherwise your touch can't interact with moving view. 75 | UIView.animate(withDuration: relayDuration, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: velocity, options: .allowUserInteraction, animations: { 76 | self.panelView.center.y = targetY 77 | }, completion: nil) 78 | self.panelOpened = isUp ? false : true 79 | default:break 80 | } 81 | } 82 | 83 | func handleTap(_ geture: UITapGestureRecognizer){ 84 | switch geture.state { 85 | case .ended, .cancelled: 86 | let targetY = panelOpened ? view.center.y + diff : view.center.y * 2.5 87 | 88 | if USE_COREANIMATION{ 89 | let openOrcloseAni = CABasicAnimation(keyPath: "position.y") 90 | openOrcloseAni.isAdditive = true 91 | openOrcloseAni.duration = duration 92 | openOrcloseAni.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault) 93 | 94 | // When Core Animation is additive, scope of animation atteched to presentationLayer is: modelLayer + fromValue ~ modelLayer + toValue. 95 | openOrcloseAni.fromValue = panelView.center.y - targetY 96 | openOrcloseAni.toValue = 0 97 | if panelOpened{ 98 | panelView.layer.add(openOrcloseAni, forKey: "close") 99 | }else{ 100 | panelView.layer.add(openOrcloseAni, forKey: "open") 101 | } 102 | panelView.center.y = targetY 103 | }else{ 104 | // UIView Animation is additive since iOS 8. You should provide AllowUserInteraction option otherwise your touch can't interact with moving view. 105 | UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 2, options: .allowUserInteraction, animations: { 106 | self.panelView.center.y = targetY 107 | }, completion: nil) 108 | } 109 | panelOpened = !panelOpened 110 | default:break 111 | } 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /ControlPanelInteractiveAnimation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedante/ControlPanelAnimation/d9b743c4f83fd60a0b02d565ba169a97837fe595/ControlPanelInteractiveAnimation.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Control Panel Interactive Animation 2 | 3 | ![](https://github.com/seedante/ControlPanelAnimation/blob/master/ControlPanelInteractiveAnimation.gif?raw=true) 4 | 5 | Continue the topic ["Interactive Animations"](https://www.objc.io/issues/12-animations/interactive-animations/) which objc.io talked two years ago. When the article was released(just before WWDC2014 and iOS 8), a key problem can't be resolved by UIView Animation in iOS 7. Two authors of the article implement the above interacitive animation in two ways: UIKit Dynamics, and spring animation effect implemented by CADisplayLink, latter is admirable. 6 | 7 | [WWDC 2014 Session 236: Building Interruptible and Responsive Interactions](https://developer.apple.com/videos/play/wwdc2014/236/) explains how to bulid fluid interactive animations. There are three transition problems to resolve: 8 | 9 | 1. Animation to Gesture: pause or stop animation in current position. 10 | 2. Gesture to Animation: add a spring animation with special initial velocity to make smooth transition. 11 | 3. Animation to Animation: reverse animation in BeginFromCurrentState or Additive mode. 12 | 13 | Almost two weeks ago, [Session 216: Advances in UIKit Animations and Transitions](https://developer.apple.com/videos/play/wwdc2016/216/) introduces [UIViewPropertyAnimator](https://developer.apple.com/reference/uikit/uiviewpropertyanimator) in iOS 10 to make completely interactive, interruptible animations. 14 | 15 | I implement above animation in two ways: 16 | 17 | 1. UIView Animation/Core Animation: after iOS 8, it's very easy to achieve the goal; on iOS 7, there is a little limitation that UIView Animation is not additive until iOS 8 and Spring Core Animation is not public until iOS 9. 18 | 2. UIViewPropertyAnimator: it's awesome and very flexible, and relatively complex, not a little. You can use it like UIView Animation, or in its style, or combine two styles. I show you how to implement above interactive animation by `UIViewPropertyAnimator` in three ways in 'iOS10-Control-Panel-Interactive-Animation' project. 19 | 20 | 两篇详细的博客: 21 | 22 | 1. [交互式动画(上):iOS 10 以下的实现](http://www.jianshu.com/p/f62cc43a5d6c),使用 UIView Animation/Core Animation 来实现上述交互动画。 23 | 2. [交互式动画(下):UIViewPropertyAnimator in iOS 10](http://www.jianshu.com/p/bc5fce6a06db),使用 UIViewPropertyAnimator实现上述交互动画,这个类相当灵活,可以说是面向对象版本的 UIView Animation。但在实现交互控制时,相比 UIView Animation/Core Animation,使用上复杂了许多。 24 | 25 | ## Requirements 26 | 27 | 1. Control-Panel-Interactive-Animation: iOS 8+, Swift 2.2+ 28 | 2. iOS10-Control-Panel-Interactive-Animation: Xcode 8 beta, iOS 10+, Swift 3 29 | 30 | ## View Controller Transition 31 | 32 | Before iOS 10, view controller transition is not completely interactive, interruptible, for example, push and pop in UINavigationController: if start transition in non-interactive, you’ll have to wait until the transition animation is finished before you can do anything; if start transition in interactive, after interaction end, the transition animation is no more interactive. 33 | 34 | iOS 10 introduce the protocol `UIViewImplicitlyAnimating`, which `UIViewPropertyAnimator` conform to, into view controller transition protocol which is very complex now. Actually, with the help of `UIViewPropertyAnimator`, we can simplify view controller transition protocol. A demo for this: [iOS10PushPop](https://github.com/seedante/iOS-ViewController-Transition-Demo/tree/master/iOS10PushPop). 35 | 36 | 详细内容已在 [iOS 视图控制器转场详解](https://github.com/seedante/iOS-Note/wiki/View-Controller-Transition-PartII#Chapter3.7)中更新。 37 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10ControlPanel.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FD0568011D19628F00596748 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0568001D19628F00596748 /* AppDelegate.swift */; }; 11 | FD0568031D19628F00596748 /* ViewControllerZero.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0568021D19628F00596748 /* ViewControllerZero.swift */; }; 12 | FD0568061D19628F00596748 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FD0568041D19628F00596748 /* Main.storyboard */; }; 13 | FD0568081D19628F00596748 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FD0568071D19628F00596748 /* Assets.xcassets */; }; 14 | FD05680B1D19628F00596748 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FD0568091D19628F00596748 /* LaunchScreen.storyboard */; }; 15 | FD25436F1D1CAFFB00541C11 /* ViewControllerTwo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD25436E1D1CAFFB00541C11 /* ViewControllerTwo.swift */; }; 16 | FD385D171D1A7C8E004CA9A4 /* ViewControllerOne.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD385D161D1A7C8E004CA9A4 /* ViewControllerOne.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | FD0567FD1D19628F00596748 /* iOS10ControlPanel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS10ControlPanel.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | FD0568001D19628F00596748 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | FD0568021D19628F00596748 /* ViewControllerZero.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerZero.swift; sourceTree = ""; }; 23 | FD0568051D19628F00596748 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | FD0568071D19628F00596748 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | FD05680A1D19628F00596748 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | FD05680C1D19628F00596748 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | FD25436E1D1CAFFB00541C11 /* ViewControllerTwo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerTwo.swift; sourceTree = ""; }; 28 | FD385D161D1A7C8E004CA9A4 /* ViewControllerOne.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerOne.swift; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | FD0567FA1D19628E00596748 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | FD0567F41D19628E00596748 = { 43 | isa = PBXGroup; 44 | children = ( 45 | FD0567FF1D19628F00596748 /* InteractiveAnimation */, 46 | FD0567FE1D19628F00596748 /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | FD0567FE1D19628F00596748 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | FD0567FD1D19628F00596748 /* iOS10ControlPanel.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | FD0567FF1D19628F00596748 /* InteractiveAnimation */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | FD0568001D19628F00596748 /* AppDelegate.swift */, 62 | FD0568021D19628F00596748 /* ViewControllerZero.swift */, 63 | FD385D161D1A7C8E004CA9A4 /* ViewControllerOne.swift */, 64 | FD25436E1D1CAFFB00541C11 /* ViewControllerTwo.swift */, 65 | FD0568041D19628F00596748 /* Main.storyboard */, 66 | FD0568071D19628F00596748 /* Assets.xcassets */, 67 | FD0568091D19628F00596748 /* LaunchScreen.storyboard */, 68 | FD05680C1D19628F00596748 /* Info.plist */, 69 | ); 70 | name = InteractiveAnimation; 71 | path = iOS10InteractiveAnimation; 72 | sourceTree = ""; 73 | }; 74 | /* End PBXGroup section */ 75 | 76 | /* Begin PBXNativeTarget section */ 77 | FD0567FC1D19628E00596748 /* iOS10ControlPanel */ = { 78 | isa = PBXNativeTarget; 79 | buildConfigurationList = FD05680F1D19628F00596748 /* Build configuration list for PBXNativeTarget "iOS10ControlPanel" */; 80 | buildPhases = ( 81 | FD0567F91D19628E00596748 /* Sources */, 82 | FD0567FA1D19628E00596748 /* Frameworks */, 83 | FD0567FB1D19628E00596748 /* Resources */, 84 | ); 85 | buildRules = ( 86 | ); 87 | dependencies = ( 88 | ); 89 | name = iOS10ControlPanel; 90 | productName = iOS10InteractiveAnimation; 91 | productReference = FD0567FD1D19628F00596748 /* iOS10ControlPanel.app */; 92 | productType = "com.apple.product-type.application"; 93 | }; 94 | /* End PBXNativeTarget section */ 95 | 96 | /* Begin PBXProject section */ 97 | FD0567F51D19628E00596748 /* Project object */ = { 98 | isa = PBXProject; 99 | attributes = { 100 | LastSwiftUpdateCheck = 0800; 101 | LastUpgradeCheck = 0800; 102 | ORGANIZATIONNAME = seedante; 103 | TargetAttributes = { 104 | FD0567FC1D19628E00596748 = { 105 | CreatedOnToolsVersion = 8.0; 106 | DevelopmentTeam = 6Z8XUV9H6J; 107 | DevelopmentTeamName = "Ye Zongyu (Personal Team)"; 108 | ProvisioningStyle = Manual; 109 | }; 110 | }; 111 | }; 112 | buildConfigurationList = FD0567F81D19628E00596748 /* Build configuration list for PBXProject "iOS10ControlPanel" */; 113 | compatibilityVersion = "Xcode 3.2"; 114 | developmentRegion = English; 115 | hasScannedForEncodings = 0; 116 | knownRegions = ( 117 | en, 118 | Base, 119 | ); 120 | mainGroup = FD0567F41D19628E00596748; 121 | productRefGroup = FD0567FE1D19628F00596748 /* Products */; 122 | projectDirPath = ""; 123 | projectRoot = ""; 124 | targets = ( 125 | FD0567FC1D19628E00596748 /* iOS10ControlPanel */, 126 | ); 127 | }; 128 | /* End PBXProject section */ 129 | 130 | /* Begin PBXResourcesBuildPhase section */ 131 | FD0567FB1D19628E00596748 /* Resources */ = { 132 | isa = PBXResourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | FD05680B1D19628F00596748 /* LaunchScreen.storyboard in Resources */, 136 | FD0568081D19628F00596748 /* Assets.xcassets in Resources */, 137 | FD0568061D19628F00596748 /* Main.storyboard in Resources */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | /* End PBXResourcesBuildPhase section */ 142 | 143 | /* Begin PBXSourcesBuildPhase section */ 144 | FD0567F91D19628E00596748 /* Sources */ = { 145 | isa = PBXSourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | FD25436F1D1CAFFB00541C11 /* ViewControllerTwo.swift in Sources */, 149 | FD385D171D1A7C8E004CA9A4 /* ViewControllerOne.swift in Sources */, 150 | FD0568031D19628F00596748 /* ViewControllerZero.swift in Sources */, 151 | FD0568011D19628F00596748 /* AppDelegate.swift in Sources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXSourcesBuildPhase section */ 156 | 157 | /* Begin PBXVariantGroup section */ 158 | FD0568041D19628F00596748 /* Main.storyboard */ = { 159 | isa = PBXVariantGroup; 160 | children = ( 161 | FD0568051D19628F00596748 /* Base */, 162 | ); 163 | name = Main.storyboard; 164 | sourceTree = ""; 165 | }; 166 | FD0568091D19628F00596748 /* LaunchScreen.storyboard */ = { 167 | isa = PBXVariantGroup; 168 | children = ( 169 | FD05680A1D19628F00596748 /* Base */, 170 | ); 171 | name = LaunchScreen.storyboard; 172 | sourceTree = ""; 173 | }; 174 | /* End PBXVariantGroup section */ 175 | 176 | /* Begin XCBuildConfiguration section */ 177 | FD05680D1D19628F00596748 /* Debug */ = { 178 | isa = XCBuildConfiguration; 179 | buildSettings = { 180 | ALWAYS_SEARCH_USER_PATHS = NO; 181 | CLANG_ANALYZER_NONNULL = YES; 182 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 183 | CLANG_CXX_LIBRARY = "libc++"; 184 | CLANG_ENABLE_MODULES = YES; 185 | CLANG_ENABLE_OBJC_ARC = YES; 186 | CLANG_WARN_BOOL_CONVERSION = YES; 187 | CLANG_WARN_CONSTANT_CONVERSION = YES; 188 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 189 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 190 | CLANG_WARN_EMPTY_BODY = YES; 191 | CLANG_WARN_ENUM_CONVERSION = YES; 192 | CLANG_WARN_INT_CONVERSION = YES; 193 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 194 | CLANG_WARN_UNREACHABLE_CODE = YES; 195 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 196 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 197 | COPY_PHASE_STRIP = NO; 198 | DEBUG_INFORMATION_FORMAT = dwarf; 199 | ENABLE_STRICT_OBJC_MSGSEND = YES; 200 | ENABLE_TESTABILITY = YES; 201 | GCC_C_LANGUAGE_STANDARD = gnu99; 202 | GCC_DYNAMIC_NO_PIC = NO; 203 | GCC_NO_COMMON_BLOCKS = YES; 204 | GCC_OPTIMIZATION_LEVEL = 0; 205 | GCC_PREPROCESSOR_DEFINITIONS = ( 206 | "DEBUG=1", 207 | "$(inherited)", 208 | ); 209 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 210 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 211 | GCC_WARN_UNDECLARED_SELECTOR = YES; 212 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 213 | GCC_WARN_UNUSED_FUNCTION = YES; 214 | GCC_WARN_UNUSED_VARIABLE = YES; 215 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 216 | MTL_ENABLE_DEBUG_INFO = YES; 217 | ONLY_ACTIVE_ARCH = YES; 218 | SDKROOT = iphoneos; 219 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 220 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 221 | TARGETED_DEVICE_FAMILY = "1,2"; 222 | }; 223 | name = Debug; 224 | }; 225 | FD05680E1D19628F00596748 /* Release */ = { 226 | isa = XCBuildConfiguration; 227 | buildSettings = { 228 | ALWAYS_SEARCH_USER_PATHS = NO; 229 | CLANG_ANALYZER_NONNULL = YES; 230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 231 | CLANG_CXX_LIBRARY = "libc++"; 232 | CLANG_ENABLE_MODULES = YES; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_WARN_BOOL_CONVERSION = YES; 235 | CLANG_WARN_CONSTANT_CONVERSION = YES; 236 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 237 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 238 | CLANG_WARN_EMPTY_BODY = YES; 239 | CLANG_WARN_ENUM_CONVERSION = YES; 240 | CLANG_WARN_INT_CONVERSION = YES; 241 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 242 | CLANG_WARN_UNREACHABLE_CODE = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 245 | COPY_PHASE_STRIP = NO; 246 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 247 | ENABLE_NS_ASSERTIONS = NO; 248 | ENABLE_STRICT_OBJC_MSGSEND = YES; 249 | GCC_C_LANGUAGE_STANDARD = gnu99; 250 | GCC_NO_COMMON_BLOCKS = YES; 251 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 252 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 253 | GCC_WARN_UNDECLARED_SELECTOR = YES; 254 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 255 | GCC_WARN_UNUSED_FUNCTION = YES; 256 | GCC_WARN_UNUSED_VARIABLE = YES; 257 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 258 | MTL_ENABLE_DEBUG_INFO = NO; 259 | SDKROOT = iphoneos; 260 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 261 | TARGETED_DEVICE_FAMILY = "1,2"; 262 | VALIDATE_PRODUCT = YES; 263 | }; 264 | name = Release; 265 | }; 266 | FD0568101D19628F00596748 /* Debug */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 270 | INFOPLIST_FILE = "$(SRCROOT)/iOS10InteractiveAnimation/Info.plist"; 271 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 272 | PRODUCT_BUNDLE_IDENTIFIER = iOS10ControlPanelInteractiveAnimation; 273 | PRODUCT_NAME = "$(TARGET_NAME)"; 274 | SWIFT_VERSION = 3.0; 275 | }; 276 | name = Debug; 277 | }; 278 | FD0568111D19628F00596748 /* Release */ = { 279 | isa = XCBuildConfiguration; 280 | buildSettings = { 281 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 282 | INFOPLIST_FILE = "$(SRCROOT)/iOS10InteractiveAnimation/Info.plist"; 283 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 284 | PRODUCT_BUNDLE_IDENTIFIER = iOS10ControlPanelInteractiveAnimation; 285 | PRODUCT_NAME = "$(TARGET_NAME)"; 286 | SWIFT_VERSION = 3.0; 287 | }; 288 | name = Release; 289 | }; 290 | /* End XCBuildConfiguration section */ 291 | 292 | /* Begin XCConfigurationList section */ 293 | FD0567F81D19628E00596748 /* Build configuration list for PBXProject "iOS10ControlPanel" */ = { 294 | isa = XCConfigurationList; 295 | buildConfigurations = ( 296 | FD05680D1D19628F00596748 /* Debug */, 297 | FD05680E1D19628F00596748 /* Release */, 298 | ); 299 | defaultConfigurationIsVisible = 0; 300 | defaultConfigurationName = Release; 301 | }; 302 | FD05680F1D19628F00596748 /* Build configuration list for PBXNativeTarget "iOS10ControlPanel" */ = { 303 | isa = XCConfigurationList; 304 | buildConfigurations = ( 305 | FD0568101D19628F00596748 /* Debug */, 306 | FD0568111D19628F00596748 /* Release */, 307 | ); 308 | defaultConfigurationIsVisible = 0; 309 | defaultConfigurationName = Release; 310 | }; 311 | /* End XCConfigurationList section */ 312 | }; 313 | rootObject = FD0567F51D19628E00596748 /* Project object */; 314 | } 315 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10ControlPanel.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10ControlPanel.xcodeproj/project.xcworkspace/xcuserdata/seedante.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seedante/ControlPanelAnimation/d9b743c4f83fd60a0b02d565ba169a97837fe595/iOS10-Control-Panel-Interactive-Animation/iOS10ControlPanel.xcodeproj/project.xcworkspace/xcuserdata/seedante.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10ControlPanel.xcodeproj/xcuserdata/seedante.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10ControlPanel.xcodeproj/xcuserdata/seedante.xcuserdatad/xcschemes/iOS10ControlPane.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10ControlPanel.xcodeproj/xcuserdata/seedante.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | iOS10ControlPane.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | FD0567FC1D19628E00596748 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10InteractiveAnimation/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS10InteractiveAnimation 4 | // 5 | // Created by seedante on 16/6/21. 6 | // Copyright © 2016年 seedante. 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: [NSObject: AnyObject]?) -> 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 throttle down OpenGL ES frame rates. 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 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10InteractiveAnimation/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10InteractiveAnimation/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 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10InteractiveAnimation/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 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10InteractiveAnimation/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | iOS10ControlPanel 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10InteractiveAnimation/ViewControllerOne.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerOne.swift 3 | // iOS10InteractiveAnimation 4 | // 5 | // Created by seedante on 16/6/22. 6 | // Copyright © 2016年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | Continue the topic "Interactive Animations" in https://www.objc.io/issues/12-animations/interactive-animations/ , use UIViewPropertyAnimator 13 | introduced in iOS 10 to implement control pane open/close interactive animation with the style of UIView Animation. UIViewPropertyAnimator is 14 | additive except that inited with UISpringTimingParameters and initialVelocity isn't (0,0). 15 | */ 16 | 17 | class ViewControllerOne: UIViewController { 18 | 19 | var pan = UIPanGestureRecognizer() 20 | var tap = UITapGestureRecognizer() 21 | var paneView = UIView() 22 | var paneOpened = true 23 | 24 | var animator: UIViewPropertyAnimator = UIViewPropertyAnimator() 25 | let duration: TimeInterval = 0.5 26 | let relayDuration: TimeInterval = 0.3 27 | let dampingRatio: CGFloat = 0.7 28 | let diff: CGFloat = 150 29 | 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | // Do any additional setup after loading the view, typically from a nib. 34 | 35 | paneView.frame = view.bounds 36 | paneView.center.y = view.center.y * (3 - 0.5) 37 | paneView.backgroundColor = .gray 38 | paneView.layer.cornerRadius = 5 39 | view.addSubview(paneView) 40 | 41 | pan.addTarget(self, action: #selector(ViewControllerOne.handlePan(gesture:))) 42 | paneView.addGestureRecognizer(pan) 43 | tap.addTarget(self, action: #selector(ViewControllerOne.handleTap(gesture:))) 44 | paneView.addGestureRecognizer(tap) 45 | 46 | let timeing = UISpringTimingParameters(dampingRatio: dampingRatio, initialVelocity: CGVector(dx: 0, dy: 1)) 47 | animator = UIViewPropertyAnimator(duration: duration, timingParameters: timeing) 48 | // If isInterruptible == false, except startAnimation(), other methods and writeable property in Protocol UIViewAnimating can't use, otherwise it raise exception. 49 | // And isRunning will be false always, state won't be stoped, just inactive and active. In this situation, use UIViewPropertyAnimator likes UIView Animation. 50 | animator.isInterruptible = false 51 | } 52 | 53 | func handleTap(gesture: UITapGestureRecognizer){ 54 | switch gesture.state { 55 | case .ended, .cancelled: 56 | let targetY = paneOpened ? view.center.y + diff : view.center.y * 2.5 57 | paneOpened = !paneOpened 58 | // when isInterruptible == false, UIViewPropertyAnimator is like UIView Animation. 59 | animator.addAnimations({ [unowned self] in 60 | self.paneView.center.y = targetY 61 | }) 62 | if animator.state == .inactive{ 63 | animator.startAnimation() 64 | } 65 | default:break 66 | } 67 | } 68 | 69 | 70 | //MARK: Handle Gesture 71 | func handlePan(gesture: UIPanGestureRecognizer){ 72 | switch gesture.state { 73 | case .began: 74 | if animator.state == .active{ 75 | let currentFrame = (paneView.layer.presentation()! as CALayer).frame 76 | // Remove animation of view in an UIViewPropertyAnimator's animation block, even not all views in the block, will break UIViewPropertyAnimator's state to be inactive. 77 | paneView.layer.removeAllAnimations() 78 | paneView.layer.frame = currentFrame 79 | } 80 | case .changed: 81 | let point = gesture.translation(in: view) 82 | paneView.center.y += point.y 83 | gesture.setTranslation(CGPoint(x: 0, y: 0), in: view) 84 | case .ended, .cancelled: 85 | paneOpened = !paneOpened 86 | let gestureVelocity = gesture.velocity(in: view) 87 | let isUp = gestureVelocity.y < 0 ? true : false 88 | let targetY = isUp ? view.center.y + diff : view.center.y * 2.5 89 | let velocityY = abs(gestureVelocity.y) / abs(paneView.center.y - targetY) 90 | let timeing = UISpringTimingParameters(dampingRatio: dampingRatio, initialVelocity: CGVector(dx: 0, dy: velocityY)) 91 | 92 | //You could assign this new UIViewPropertyAnimator to 'animator', and in Tap gesture, you must reassign a new objct with original timing. 93 | let relayAnimator = UIViewPropertyAnimator(duration: relayDuration, timingParameters: timeing) 94 | relayAnimator.addAnimations({[unowned self] in 95 | self.paneView.center.y = targetY 96 | }) 97 | relayAnimator.startAnimation() 98 | default:break 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10InteractiveAnimation/ViewControllerTwo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerTwo.swift 3 | // iOS10InteractiveAnimation 4 | // 5 | // Created by seedante on 16/6/24. 6 | // Copyright © 2016年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | Continue the topic "Interactive Animations" in https://www.objc.io/issues/12-animations/interactive-animations/ , use UIViewPropertyAnimator 13 | introduced in iOS 10 to implement control pane open/close interactive animation with the style of UIView Animation. UIViewPropertyAnimator is 14 | additive except that inited with UISpringTimingParameters and initialVelocity isn't (0,0). 15 | */ 16 | 17 | class ViewControllerTwo: UIViewController { 18 | 19 | var pan = UIPanGestureRecognizer() 20 | var tap = UITapGestureRecognizer() 21 | var paneView = UIView() 22 | var paneOpened = true 23 | 24 | let duration: TimeInterval = 0.5 //0.5s is too short for test mix tap and pan 25 | let relayDuration: TimeInterval = 0.3 26 | let dampingRatio: CGFloat = 0.7 27 | let diff: CGFloat = 150 28 | 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | paneView.frame = view.bounds 34 | paneView.center.y = view.center.y * (3 - 0.5) 35 | paneView.backgroundColor = .gray 36 | paneView.layer.cornerRadius = 5 37 | view.addSubview(paneView) 38 | 39 | pan.addTarget(self, action: #selector(ViewControllerOne.handlePan(gesture:))) 40 | paneView.addGestureRecognizer(pan) 41 | tap.addTarget(self, action: #selector(ViewControllerOne.handleTap(gesture:))) 42 | paneView.addGestureRecognizer(tap) 43 | } 44 | 45 | override func didReceiveMemoryWarning() { 46 | super.didReceiveMemoryWarning() 47 | // Dispose of any resources that can be recreated. 48 | } 49 | 50 | 51 | //MARK: Handle Gesture 52 | func handlePan(gesture: UIPanGestureRecognizer){ 53 | switch gesture.state { 54 | case .began: 55 | stopMoveAnimation(of: [paneView]) 56 | case .changed: 57 | let point = gesture.translation(in: view) 58 | paneView.center.y += point.y 59 | gesture.setTranslation(CGPoint(x: 0, y: 0), in: view) 60 | case .ended, .cancelled: 61 | let gestureVelocity = gesture.velocity(in: view) 62 | let isUp = gestureVelocity.y < 0 ? true : false 63 | paneOpened = isUp ? false : true 64 | let targetY = isUp ? view.center.y + diff : view.center.y * 2.5 65 | let velocityY = abs(gestureVelocity.y) / abs(paneView.center.y - targetY) 66 | let timeing = UISpringTimingParameters(dampingRatio: dampingRatio, initialVelocity: CGVector(dx: 0, dy: velocityY)) 67 | 68 | // Thought UIViewPropertyAnimator is not additive when initialVelocity is not (0, 0), it can works with other UIViewPropertyAnimator is additive, the result is still additive. 69 | let relayAnimator = UIViewPropertyAnimator(duration: relayDuration, timingParameters: timeing) 70 | relayAnimator.addAnimations({[unowned self] in 71 | self.paneView.center.y = targetY 72 | }) 73 | relayAnimator.startAnimation() 74 | default:break 75 | } 76 | } 77 | 78 | func stopMoveAnimation(of views:[UIView]){ 79 | for anyView in views{ 80 | let currentFrame = (anyView.layer.presentation()! as CALayer).frame 81 | // Remove animation of view in an UIViewPropertyAnimator's animation block, even not all views in the block, will break UIViewPropertyAnimator's state to be inactive. 82 | anyView.layer.removeAllAnimations() 83 | anyView.layer.frame = currentFrame 84 | } 85 | } 86 | 87 | func handleTap(gesture: UITapGestureRecognizer){ 88 | switch gesture.state { 89 | case .ended, .cancelled: 90 | let targetY = paneOpened ? view.center.y + diff : view.center.y * 2.5 91 | paneOpened = !paneOpened 92 | 93 | // This method is also additive, use it like UIView Animation API, just it has no spring animation interface, So this method can't start animation with specifed speed. 94 | // And the effect is not completely same with the animation in gif. 95 | UIViewPropertyAnimator.runningPropertyAnimator(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: {[unowned self] in 96 | self.paneView.center.y = targetY 97 | }, completion: nil) 98 | default:break 99 | } 100 | } 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /iOS10-Control-Panel-Interactive-Animation/iOS10InteractiveAnimation/ViewControllerZero.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // iOS10InteractiveAnimation 4 | // 5 | // Created by seedante on 16/6/21. 6 | // Copyright © 2016年 seedante. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewAnimatingState{ 12 | var description: String{ 13 | switch self { 14 | case .inactive: 15 | return "inactive" 16 | case .active: 17 | return "active" 18 | case .stopped: 19 | return "stopped" 20 | } 21 | } 22 | } 23 | 24 | extension UIViewAnimatingPosition{ 25 | var description: String{ 26 | switch self { 27 | case .end: 28 | return "end" 29 | case .current: 30 | return "current" 31 | case .start: 32 | return "start" 33 | } 34 | } 35 | } 36 | 37 | /** 38 | Continue the topic "Interactive Animations" in https://www.objc.io/issues/12-animations/interactive-animations/ , use UIViewPropertyAnimator 39 | introduced in iOS 10 to implement control panel open/close interactive animation in its way. UIViewPropertyAnimator is additive except that 40 | inited with UISpringTimingParameters and initialVelocity isn't (0,0). 41 | */ 42 | 43 | class ViewControllerZero: UIViewController { 44 | 45 | var pan = UIPanGestureRecognizer() 46 | var tap = UITapGestureRecognizer() 47 | var panelView = UIView() 48 | var panelOpened = true 49 | 50 | var animator: UIViewPropertyAnimator = UIViewPropertyAnimator() 51 | let duration: TimeInterval = 0.5 52 | let relayDuration: TimeInterval = 0.3 53 | let dampingRatio: CGFloat = 0.7 54 | let diff: CGFloat = 150 55 | 56 | override func viewDidLoad() { 57 | super.viewDidLoad() 58 | 59 | panelView.frame = view.bounds 60 | panelView.center.y = view.center.y * (3 - 0.5) 61 | panelView.backgroundColor = .gray 62 | panelView.layer.cornerRadius = 5 63 | view.addSubview(panelView) 64 | 65 | pan.addTarget(self, action: #selector(ViewControllerOne.handlePan(gesture:))) 66 | panelView.addGestureRecognizer(pan) 67 | tap.addTarget(self, action: #selector(ViewControllerOne.handleTap(gesture:))) 68 | panelView.addGestureRecognizer(tap) 69 | } 70 | 71 | // MARK: Helper Method 72 | /// Return an UIViewPropertyAnimator configured with the animation of open or close control panel. 73 | func interactiveAnimator() -> UIViewPropertyAnimator{ 74 | // UIViewPropertyAnimator's animation is additive and can mix multiple animation blocks except UISpringTimingParameters with initialVelocity not equal (0, 0). 75 | let timing = UISpringTimingParameters(dampingRatio: dampingRatio, initialVelocity: CGVector(dx: 0, dy: 1)) 76 | let targetY = panelOpened ? view.center.y + diff : view.center.y * 2.5 77 | let animator = UIViewPropertyAnimator(duration: duration, timingParameters: timing) 78 | animator.addAnimations({ [unowned self] in 79 | self.panelView.center.y = targetY 80 | }) 81 | animator.addCompletion({ [unowned self] position in 82 | if position == .end{ 83 | self.panelOpened = !self.panelOpened 84 | } 85 | print("Original Animation Completed with panel opened: \(self.panelOpened), poistion: \(position.description)") 86 | }) 87 | return animator 88 | } 89 | 90 | /// Return a list (UISpringTimingParameters, Bool, CGFloat) to congigure new UIViewPropertyAnimator object and animation when pan gesture is ended. 91 | /// 92 | /// - Returns: 93 | /// - relayTiming: An UISpringTimingParameters use to configure new UIViewPropertyAnimator object to continue animation smoothly; 94 | /// - isUp: finger leave direction in Y axis: up or down; 95 | /// - targetY: panelView's destination in Y axis. 96 | func relayTiming_direction_targetY(withPangesture panGesture: UIPanGestureRecognizer) -> (relayTiming:UISpringTimingParameters, isUp:Bool, targetY:CGFloat) { 97 | let gestureVelocity = panGesture.velocity(in: view) 98 | let isUp = gestureVelocity.y < 0 ? true : false 99 | let targetY = isUp ? view.center.y + diff : view.center.y * 2.5 100 | let velocityY = abs(gestureVelocity.y) / abs(panelView.center.y - targetY) 101 | let velocity = CGVector(dx: 0, dy: velocityY) 102 | let timing = UISpringTimingParameters(dampingRatio: dampingRatio, initialVelocity: velocity) 103 | return (timing, isUp, targetY) 104 | } 105 | 106 | /// Move panel view by your finger on the screen, just Y axis. 107 | func movePanelWithPan(gesture: UIPanGestureRecognizer){ 108 | let point = gesture.translation(in: view) 109 | panelView.center.y += point.y 110 | gesture.setTranslation(CGPoint(x: 0, y: 0), in: view) 111 | } 112 | 113 | func checkState(of animator: UIViewPropertyAnimator){ 114 | print(animator) 115 | print("state: \(animator.state.description)") 116 | print("isRunning: \(animator.isRunning)") 117 | print("isReversed: \(animator.isReversed)") 118 | print("timing: \(animator.timingParameters!)") 119 | print("duration: \(animator.duration)") 120 | } 121 | 122 | // MARK: Handle Gesture 123 | /// After tap on panelView, it will go to opposite, if it's moving, go back. If you keep tap panelView before it reach opposite, it always go to opposite again. 124 | func handleTap(gesture: UITapGestureRecognizer){ 125 | switch gesture.state { 126 | case .ended, .cancelled: 127 | switch animator.state { 128 | // If animator is not active, it means animator has completed its animation block or has not any animatin block to run. 129 | // But its configuration, include timing and duration, maybe changed in pan gesture, so must create a new animator with original configuration. 130 | case .inactive, .stopped: 131 | animator = interactiveAnimator() 132 | animator.startAnimation() 133 | print("Tap: start animation") 134 | // If animator is active, it's sure that there is an animation block is running. Pause and reverse it. 135 | case .active: 136 | animator.pauseAnimation() 137 | print(String(format: "Tap: Reverse from fractionComplete: %.2f", (animator.fractionComplete * 100)) + "%") 138 | animator.isReversed = !(animator.isReversed) 139 | animator.startAnimation() 140 | } 141 | default:break 142 | } 143 | } 144 | 145 | func handlePan(gesture: UIPanGestureRecognizer){ 146 | //Run only one follow method. 147 | 148 | // Style I: Pause animation, then continue it after pan gesture is ended. 149 | // PauseAndContinueAnimation(withPanGesture: gesture) 150 | // Style II: Stop animation, then add a new animation complete the rest move after pan gesture is ended. 151 | // StopAndRenewAnimation(withPanGesture: gesture) 152 | /// Right way. 153 | fixDefect_PauseAndContinueAnimation(withPanGesture: gesture) 154 | } 155 | 156 | /// Pause the animator in pan gesture's 'began' stage if the animator is running, and then continue animator with new timing in 'ended' stage. 157 | /// If animator is not running in 'began' stage, when pan is ended, create a new aniamtor with new timing and add animation to run. 158 | /// There is a bug in this way like use stopAnimation(): if you drag panelView with pan gesture first, reverse animation before it finish, the panelView 159 | /// will go back to where your finger leave the screen, and it's not place we expect. 160 | /// How to fix it? look fixDefect_PauseAndContinueAnimation(withPanGesture panGesture: UIPanGestureRecognizer) 161 | func PauseAndContinueAnimation(withPanGesture panGesture: UIPanGestureRecognizer){ 162 | switch panGesture.state { 163 | case .began: 164 | if animator.isRunning{ 165 | print("Pan: pauseAnimation") 166 | animator.pauseAnimation() 167 | // Why cancel reverse here? When pan is ended, need combine velocity direction and panel opened/closed to judge the animation's derection, 168 | // if isReversed is true, count it in, it's more complex. Cancel it here don't affect later judgement. 169 | if animator.isReversed{ 170 | animator.isReversed = false 171 | } 172 | } 173 | case .changed: 174 | movePanelWithPan(gesture: panGesture) 175 | case .ended, .cancelled: 176 | // With pan gesture, we can get a new timing to continue to move smoothly, animation continue direction and panelView's destination position. 177 | let (timing, isUp, targetY) = relayTiming_direction_targetY(withPangesture: panGesture) 178 | 179 | switch animator.state { 180 | // If the animator is not active, it means it has no animation block to run. Because even if animator is paused, it's still active. 181 | // Here we need to continue with new timing, but there's no way to modify timing directly. Yeah, we have continueAnimation:, 182 | // but this method must called after startAnimation(), otherwise animation doesn't run. So I create a new animator with new timing. 183 | case .inactive, .stopped: 184 | print("Pan: No animation is running, renew Animation.") 185 | animator = UIViewPropertyAnimator(duration: duration, timingParameters: timing) 186 | animator.addAnimations({ 187 | self.panelView.center.y = targetY 188 | }) 189 | animator.addCompletion({[unowned self] position in 190 | if position == .end{ 191 | self.panelOpened = isUp ? false : true 192 | } 193 | print("Pan: completion at \(position.description), panelOpened: \(self.panelOpened)") 194 | }) 195 | animator.startAnimation() 196 | 197 | // There is an animation block waitting for continue if animator's state is active 198 | case .active: 199 | let isSameDirection: Bool = (panelOpened && isUp) || (!panelOpened && !isUp) 200 | animator.isReversed = isSameDirection ? false : true 201 | print("Pan: continue original animation.") 202 | 203 | // This method temporarily modify the timing and use new duration to run animation from current state to pre-set state. 204 | // This method won't change original timing to new timing, but it may changes original 'duration' by some rules, it's very weird design. 205 | // In this scene, we need the same configuration to run animation in tap gesture, luckily, it won't change original duration if 'durationFactor' is 0. 206 | // How much is animation's running time after call this method? runing time = durationFactor * original duration, if durationFactor is 0, 207 | // it will be original 'duration' time. After call this method, if you need original configure to run animation, reassign a new animator. 208 | animator.continueAnimation(withTimingParameters: timing, durationFactor: 0) 209 | } 210 | default:break 211 | } 212 | 213 | } 214 | 215 | /// Stop the animator in current position if animator is running, and then use a new animator with a new timing to continue to move after pan is ended. 216 | /// Before the new animator, which is created in pan gesture's ended stage, finish its animation, if reverse it with tap panelView, it will back to position 217 | /// where finger leave the screen, also is the place animation start, and it's must not be where we expect. Actually in real scene, the bug is hard to trigger, 218 | /// because a normal animation's duration is 0.3~0.5 second, it is too fast to user tap to reverse it. How to fix it, use pauseAnimation() correctly. 219 | /// Look fixDefect_PauseAndContinueAnimation(withPanGesture panGesture: UIPanGestureRecognizer). 220 | func StopAndRenewAnimation(withPanGesture panGesture: UIPanGestureRecognizer){ 221 | switch panGesture.state { 222 | case .began: 223 | // Stop animation and stay current position if animator is runing its animation. 224 | if animator.isRunning{ 225 | print("Pan: stop animtion") 226 | //This make animator to inactive, and panelView stop at its current position. 227 | animator.stopAnimation(true) 228 | 229 | /* Or, like this, this make animator's state to be stoped 230 | animator.stopAnimation(false) 231 | //Only call this method after stopAnimation(false). if animator have no animation to run, this method will raise exeception. 232 | animator.finishAnimation(at: .current) 233 | */ 234 | } 235 | case .changed: 236 | movePanelWithPan(gesture: panGesture) 237 | case .ended, .cancelled: 238 | // With pan gesture, we can get a new timing to continue to move smoothly, animation continue direction and panelView's destination position. 239 | let (timing, isUp, targetY) = relayTiming_direction_targetY(withPangesture: panGesture) 240 | animator = UIViewPropertyAnimator(duration: relayDuration, timingParameters: timing) 241 | animator.addAnimations({[unowned self] in 242 | self.panelView.center.y = targetY 243 | }) 244 | animator.addCompletion({[unowned self] position in 245 | if position == .end{ 246 | self.panelOpened = isUp ? false : true 247 | } 248 | }) 249 | animator.startAnimation() 250 | print("Pan: continue stoped animation with new timing") 251 | default:break 252 | } 253 | } 254 | 255 | 256 | /// Right way to interactive with animation: start animation from pan gesture's began stage, which make animation completed. 257 | func fixDefect_PauseAndContinueAnimation(withPanGesture panGesture: UIPanGestureRecognizer){ 258 | switch panGesture.state { 259 | // Establish a babis from here, in ended stage, just continue animation. 260 | case .began: 261 | switch animator.state { 262 | // If animator is inactive, it means no animation is running and panelView must be at bottom or top position. 263 | // Create a new animator, add animation, and pasue it, when pan gesture is ended, continue it. 264 | case .inactive: 265 | animator = interactiveAnimator() 266 | animator.startAnimation() //must start animation, otherwise continueAnimation: doesn't work. 267 | animator.pauseAnimation() 268 | print("Pan: renew and pause animation") 269 | // An animator is running, pause it. 270 | case .active: 271 | animator.pauseAnimation() //Note: after pause, animator's state is still active 272 | if animator.isReversed{ 273 | animator.isReversed = false 274 | } 275 | print("Pan: pause running animation") 276 | case .stopped:break 277 | } 278 | case .changed: 279 | movePanelWithPan(gesture: panGesture) 280 | case .ended, .cancelled: 281 | // With pan gesture, we can get a new timing to continue to move smoothly, animation continue direction and panelView's destination position. 282 | let (timing, isUp, _) = relayTiming_direction_targetY(withPangesture: panGesture) 283 | let isSameDirection: Bool = (panelOpened && isUp) || (!panelOpened && !isUp) 284 | animator.isReversed = isSameDirection ? false : true 285 | // If durationFactor is not 0, it will change animator's 'duration'. 286 | animator.continueAnimation(withTimingParameters: timing, durationFactor: 0) 287 | print("Pan: continue animation with new timing") 288 | default:break 289 | } 290 | } 291 | 292 | } 293 | --------------------------------------------------------------------------------