├── LICENSE ├── README.md ├── Stickers.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── Stickers ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json ├── Contents.json └── Stickers │ ├── Contents.json │ ├── sticker1.imageset │ ├── Contents.json │ └── penguin.png │ ├── sticker2.imageset │ ├── Contents.json │ └── rainbowdog.png │ ├── sticker3.imageset │ ├── Contents.json │ └── nocat.png │ ├── sticker4.imageset │ ├── Contents.json │ └── happybirthday.png │ ├── sticker5.imageset │ ├── Contents.json │ └── eggnog.png │ └── sticker6.imageset │ ├── Contents.json │ └── wreckingcat.png ├── Base.lproj └── LaunchScreen.storyboard ├── Info.plist ├── PeelOffInteraction.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── SceneDelegate.swift ├── StickerCollectionView.swift └── StickerView.swift /LICENSE: -------------------------------------------------------------------------------- 1 | The following does not apply to any image files bundled with this software: 2 | 3 | --- 4 | 5 | Copyright 2019 Robert Böhnke 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Peel-Off Animation Example Code 2 | 3 | This is the example code for [my article] on how to reimplement Message.app's 4 | sticker peel-off animation. 5 | 6 | [my article]: https://robb.is/working-on/a-peel-off-animation 7 | -------------------------------------------------------------------------------- /Stickers.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 542302672381FC120096C548 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542302662381FC120096C548 /* AppDelegate.swift */; }; 11 | 542302692381FC120096C548 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542302682381FC120096C548 /* SceneDelegate.swift */; }; 12 | 5423026B2381FC120096C548 /* StickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5423026A2381FC120096C548 /* StickerView.swift */; }; 13 | 5423026D2381FC140096C548 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5423026C2381FC140096C548 /* Assets.xcassets */; }; 14 | 542302702381FC140096C548 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5423026F2381FC140096C548 /* Preview Assets.xcassets */; }; 15 | 542302732381FC140096C548 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 542302712381FC140096C548 /* LaunchScreen.storyboard */; }; 16 | 542839042385E37800B26FAB /* StickerCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542839032385E37800B26FAB /* StickerCollectionView.swift */; }; 17 | 546EB60C23888A55004022B9 /* PeelOffInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546EB60B23888A55004022B9 /* PeelOffInteraction.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 542302632381FC120096C548 /* Stickers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stickers.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 542302662381FC120096C548 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | 542302682381FC120096C548 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 24 | 5423026A2381FC120096C548 /* StickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerView.swift; sourceTree = ""; }; 25 | 5423026C2381FC140096C548 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | 5423026F2381FC140096C548 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 27 | 542302722381FC140096C548 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | 542302742381FC140096C548 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 542839032385E37800B26FAB /* StickerCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerCollectionView.swift; sourceTree = ""; }; 30 | 546EB60B23888A55004022B9 /* PeelOffInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeelOffInteraction.swift; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 542302602381FC120096C548 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 5423025A2381FC120096C548 = { 45 | isa = PBXGroup; 46 | children = ( 47 | 542302652381FC120096C548 /* Stickers */, 48 | 542302642381FC120096C548 /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 542302642381FC120096C548 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 542302632381FC120096C548 /* Stickers.app */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 542302652381FC120096C548 /* Stickers */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 542302662381FC120096C548 /* AppDelegate.swift */, 64 | 5423026C2381FC140096C548 /* Assets.xcassets */, 65 | 542302742381FC140096C548 /* Info.plist */, 66 | 542302712381FC140096C548 /* LaunchScreen.storyboard */, 67 | 5423026E2381FC140096C548 /* Preview Content */, 68 | 542302682381FC120096C548 /* SceneDelegate.swift */, 69 | 542839032385E37800B26FAB /* StickerCollectionView.swift */, 70 | 546EB60B23888A55004022B9 /* PeelOffInteraction.swift */, 71 | 5423026A2381FC120096C548 /* StickerView.swift */, 72 | ); 73 | path = Stickers; 74 | sourceTree = ""; 75 | }; 76 | 5423026E2381FC140096C548 /* Preview Content */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 5423026F2381FC140096C548 /* Preview Assets.xcassets */, 80 | ); 81 | path = "Preview Content"; 82 | sourceTree = ""; 83 | }; 84 | /* End PBXGroup section */ 85 | 86 | /* Begin PBXNativeTarget section */ 87 | 542302622381FC120096C548 /* Stickers */ = { 88 | isa = PBXNativeTarget; 89 | buildConfigurationList = 542302772381FC140096C548 /* Build configuration list for PBXNativeTarget "Stickers" */; 90 | buildPhases = ( 91 | 5423025F2381FC120096C548 /* Sources */, 92 | 542302602381FC120096C548 /* Frameworks */, 93 | 542302612381FC120096C548 /* Resources */, 94 | ); 95 | buildRules = ( 96 | ); 97 | dependencies = ( 98 | ); 99 | name = Stickers; 100 | productName = Stickers; 101 | productReference = 542302632381FC120096C548 /* Stickers.app */; 102 | productType = "com.apple.product-type.application"; 103 | }; 104 | /* End PBXNativeTarget section */ 105 | 106 | /* Begin PBXProject section */ 107 | 5423025B2381FC120096C548 /* Project object */ = { 108 | isa = PBXProject; 109 | attributes = { 110 | LastSwiftUpdateCheck = 1110; 111 | LastUpgradeCheck = 1110; 112 | ORGANIZATIONNAME = "Robert Böhnke"; 113 | TargetAttributes = { 114 | 542302622381FC120096C548 = { 115 | CreatedOnToolsVersion = 11.1; 116 | }; 117 | }; 118 | }; 119 | buildConfigurationList = 5423025E2381FC120096C548 /* Build configuration list for PBXProject "Stickers" */; 120 | compatibilityVersion = "Xcode 9.3"; 121 | developmentRegion = en; 122 | hasScannedForEncodings = 0; 123 | knownRegions = ( 124 | en, 125 | Base, 126 | ); 127 | mainGroup = 5423025A2381FC120096C548; 128 | productRefGroup = 542302642381FC120096C548 /* Products */; 129 | projectDirPath = ""; 130 | projectRoot = ""; 131 | targets = ( 132 | 542302622381FC120096C548 /* Stickers */, 133 | ); 134 | }; 135 | /* End PBXProject section */ 136 | 137 | /* Begin PBXResourcesBuildPhase section */ 138 | 542302612381FC120096C548 /* Resources */ = { 139 | isa = PBXResourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 542302732381FC140096C548 /* LaunchScreen.storyboard in Resources */, 143 | 542302702381FC140096C548 /* Preview Assets.xcassets in Resources */, 144 | 5423026D2381FC140096C548 /* Assets.xcassets in Resources */, 145 | ); 146 | runOnlyForDeploymentPostprocessing = 0; 147 | }; 148 | /* End PBXResourcesBuildPhase section */ 149 | 150 | /* Begin PBXSourcesBuildPhase section */ 151 | 5423025F2381FC120096C548 /* Sources */ = { 152 | isa = PBXSourcesBuildPhase; 153 | buildActionMask = 2147483647; 154 | files = ( 155 | 542302672381FC120096C548 /* AppDelegate.swift in Sources */, 156 | 542302692381FC120096C548 /* SceneDelegate.swift in Sources */, 157 | 546EB60C23888A55004022B9 /* PeelOffInteraction.swift in Sources */, 158 | 542839042385E37800B26FAB /* StickerCollectionView.swift in Sources */, 159 | 5423026B2381FC120096C548 /* StickerView.swift in Sources */, 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | /* End PBXSourcesBuildPhase section */ 164 | 165 | /* Begin PBXVariantGroup section */ 166 | 542302712381FC140096C548 /* LaunchScreen.storyboard */ = { 167 | isa = PBXVariantGroup; 168 | children = ( 169 | 542302722381FC140096C548 /* Base */, 170 | ); 171 | name = LaunchScreen.storyboard; 172 | sourceTree = ""; 173 | }; 174 | /* End PBXVariantGroup section */ 175 | 176 | /* Begin XCBuildConfiguration section */ 177 | 542302752381FC140096C548 /* Debug */ = { 178 | isa = XCBuildConfiguration; 179 | buildSettings = { 180 | ALWAYS_SEARCH_USER_PATHS = NO; 181 | CLANG_ANALYZER_NONNULL = YES; 182 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 183 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 184 | CLANG_CXX_LIBRARY = "libc++"; 185 | CLANG_ENABLE_MODULES = YES; 186 | CLANG_ENABLE_OBJC_ARC = YES; 187 | CLANG_ENABLE_OBJC_WEAK = YES; 188 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 189 | CLANG_WARN_BOOL_CONVERSION = YES; 190 | CLANG_WARN_COMMA = YES; 191 | CLANG_WARN_CONSTANT_CONVERSION = YES; 192 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 193 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 194 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 195 | CLANG_WARN_EMPTY_BODY = YES; 196 | CLANG_WARN_ENUM_CONVERSION = YES; 197 | CLANG_WARN_INFINITE_RECURSION = YES; 198 | CLANG_WARN_INT_CONVERSION = YES; 199 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 200 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 201 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 202 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 203 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 204 | CLANG_WARN_STRICT_PROTOTYPES = YES; 205 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 206 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 207 | CLANG_WARN_UNREACHABLE_CODE = YES; 208 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 209 | COPY_PHASE_STRIP = NO; 210 | DEBUG_INFORMATION_FORMAT = dwarf; 211 | ENABLE_STRICT_OBJC_MSGSEND = YES; 212 | ENABLE_TESTABILITY = YES; 213 | GCC_C_LANGUAGE_STANDARD = gnu11; 214 | GCC_DYNAMIC_NO_PIC = NO; 215 | GCC_NO_COMMON_BLOCKS = YES; 216 | GCC_OPTIMIZATION_LEVEL = 0; 217 | GCC_PREPROCESSOR_DEFINITIONS = ( 218 | "DEBUG=1", 219 | "$(inherited)", 220 | ); 221 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 222 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 223 | GCC_WARN_UNDECLARED_SELECTOR = YES; 224 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 225 | GCC_WARN_UNUSED_FUNCTION = YES; 226 | GCC_WARN_UNUSED_VARIABLE = YES; 227 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 228 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 229 | MTL_FAST_MATH = YES; 230 | ONLY_ACTIVE_ARCH = YES; 231 | SDKROOT = iphoneos; 232 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 233 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 234 | }; 235 | name = Debug; 236 | }; 237 | 542302762381FC140096C548 /* Release */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_ENABLE_OBJC_WEAK = YES; 248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 249 | CLANG_WARN_BOOL_CONVERSION = YES; 250 | CLANG_WARN_COMMA = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 271 | ENABLE_NS_ASSERTIONS = NO; 272 | ENABLE_STRICT_OBJC_MSGSEND = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_NO_COMMON_BLOCKS = YES; 275 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 276 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 277 | GCC_WARN_UNDECLARED_SELECTOR = YES; 278 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 279 | GCC_WARN_UNUSED_FUNCTION = YES; 280 | GCC_WARN_UNUSED_VARIABLE = YES; 281 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 282 | MTL_ENABLE_DEBUG_INFO = NO; 283 | MTL_FAST_MATH = YES; 284 | SDKROOT = iphoneos; 285 | SWIFT_COMPILATION_MODE = wholemodule; 286 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 287 | VALIDATE_PRODUCT = YES; 288 | }; 289 | name = Release; 290 | }; 291 | 542302782381FC140096C548 /* Debug */ = { 292 | isa = XCBuildConfiguration; 293 | buildSettings = { 294 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 295 | CODE_SIGN_STYLE = Automatic; 296 | DEVELOPMENT_ASSET_PATHS = "\"Stickers/Preview Content\""; 297 | DEVELOPMENT_TEAM = QGW24SFH76; 298 | ENABLE_PREVIEWS = YES; 299 | INFOPLIST_FILE = Stickers/Info.plist; 300 | LD_RUNPATH_SEARCH_PATHS = ( 301 | "$(inherited)", 302 | "@executable_path/Frameworks", 303 | ); 304 | PRODUCT_BUNDLE_IDENTIFIER = com.robertboehnke.Stickers; 305 | PRODUCT_NAME = "$(TARGET_NAME)"; 306 | SWIFT_VERSION = 5.0; 307 | TARGETED_DEVICE_FAMILY = "1,2"; 308 | }; 309 | name = Debug; 310 | }; 311 | 542302792381FC140096C548 /* Release */ = { 312 | isa = XCBuildConfiguration; 313 | buildSettings = { 314 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 315 | CODE_SIGN_STYLE = Automatic; 316 | DEVELOPMENT_ASSET_PATHS = "\"Stickers/Preview Content\""; 317 | DEVELOPMENT_TEAM = QGW24SFH76; 318 | ENABLE_PREVIEWS = YES; 319 | INFOPLIST_FILE = Stickers/Info.plist; 320 | LD_RUNPATH_SEARCH_PATHS = ( 321 | "$(inherited)", 322 | "@executable_path/Frameworks", 323 | ); 324 | PRODUCT_BUNDLE_IDENTIFIER = com.robertboehnke.Stickers; 325 | PRODUCT_NAME = "$(TARGET_NAME)"; 326 | SWIFT_VERSION = 5.0; 327 | TARGETED_DEVICE_FAMILY = "1,2"; 328 | }; 329 | name = Release; 330 | }; 331 | /* End XCBuildConfiguration section */ 332 | 333 | /* Begin XCConfigurationList section */ 334 | 5423025E2381FC120096C548 /* Build configuration list for PBXProject "Stickers" */ = { 335 | isa = XCConfigurationList; 336 | buildConfigurations = ( 337 | 542302752381FC140096C548 /* Debug */, 338 | 542302762381FC140096C548 /* Release */, 339 | ); 340 | defaultConfigurationIsVisible = 0; 341 | defaultConfigurationName = Release; 342 | }; 343 | 542302772381FC140096C548 /* Build configuration list for PBXNativeTarget "Stickers" */ = { 344 | isa = XCConfigurationList; 345 | buildConfigurations = ( 346 | 542302782381FC140096C548 /* Debug */, 347 | 542302792381FC140096C548 /* Release */, 348 | ); 349 | defaultConfigurationIsVisible = 0; 350 | defaultConfigurationName = Release; 351 | }; 352 | /* End XCConfigurationList section */ 353 | }; 354 | rootObject = 5423025B2381FC120096C548 /* Project object */; 355 | } 356 | -------------------------------------------------------------------------------- /Stickers.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Stickers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Stickers/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 6 | true 7 | } 8 | 9 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 10 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Stickers/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 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "penguin.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker1.imageset/penguin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb/Peel-Off-Animation-Example-Code/4699a57cab9828d48473b59f691f5653d83d9b1e/Stickers/Assets.xcassets/Stickers/sticker1.imageset/penguin.png -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "rainbowdog.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker2.imageset/rainbowdog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb/Peel-Off-Animation-Example-Code/4699a57cab9828d48473b59f691f5653d83d9b1e/Stickers/Assets.xcassets/Stickers/sticker2.imageset/rainbowdog.png -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "nocat.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker3.imageset/nocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb/Peel-Off-Animation-Example-Code/4699a57cab9828d48473b59f691f5653d83d9b1e/Stickers/Assets.xcassets/Stickers/sticker3.imageset/nocat.png -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "happybirthday.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker4.imageset/happybirthday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb/Peel-Off-Animation-Example-Code/4699a57cab9828d48473b59f691f5653d83d9b1e/Stickers/Assets.xcassets/Stickers/sticker4.imageset/happybirthday.png -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "eggnog.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker5.imageset/eggnog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb/Peel-Off-Animation-Example-Code/4699a57cab9828d48473b59f691f5653d83d9b1e/Stickers/Assets.xcassets/Stickers/sticker5.imageset/eggnog.png -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "wreckingcat.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Stickers/Assets.xcassets/Stickers/sticker6.imageset/wreckingcat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb/Peel-Off-Animation-Example-Code/4699a57cab9828d48473b59f691f5653d83d9b1e/Stickers/Assets.xcassets/Stickers/sticker6.imageset/wreckingcat.png -------------------------------------------------------------------------------- /Stickers/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 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Stickers/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | 50 | UISupportedInterfaceOrientations~ipad 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationPortraitUpsideDown 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Stickers/PeelOffInteraction.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public final class PeelOffInteraction: NSObject, UIInteraction { 4 | public let gestureRecognizer = UILongPressGestureRecognizer() 5 | 6 | private var offset: CGPoint = .zero 7 | 8 | private var stickerView: StickerView? 9 | 10 | public var view: UIView? 11 | 12 | override public init() { 13 | super.init() 14 | 15 | gestureRecognizer.addTarget(self, action: #selector(gestureRecognizerDidUpdate)) 16 | gestureRecognizer.delegate = self 17 | gestureRecognizer.minimumPressDuration = 0.2 18 | } 19 | 20 | public func willMove(to view: UIView?) { 21 | if view == nil { 22 | self.view?.removeGestureRecognizer(gestureRecognizer) 23 | } 24 | 25 | self.view = view 26 | } 27 | 28 | public func didMove(to view: UIView?) { 29 | self.view = view 30 | 31 | view?.addGestureRecognizer(gestureRecognizer) 32 | } 33 | 34 | @objc 35 | func gestureRecognizerDidUpdate(sender: UILongPressGestureRecognizer) { 36 | switch sender.state { 37 | case .began: 38 | guard let view = sender.view else { return } 39 | 40 | stickerView = StickerView(frame: view.bounds) 41 | 42 | let renderer = UIGraphicsImageRenderer(bounds: view.bounds) 43 | 44 | stickerView!.image = renderer.image { rendererContext in 45 | view.drawHierarchy(in: view.bounds, afterScreenUpdates: false) 46 | } 47 | 48 | stickerView!.frame = view.window!.convert(view.frame, from: view.superview!) 49 | view.window!.addSubview(stickerView!) 50 | 51 | stickerView!.setIsPeeledOff(isPeeledOff: true, animated: false, start: { 52 | view.isHidden = true 53 | }) 54 | 55 | offset = sender.location(in: view) 56 | case .changed: 57 | guard let view = sender.view else { return } 58 | 59 | stickerView?.frame.origin = sender.location(in: view.window!) 60 | stickerView?.frame.origin.x -= offset.x 61 | stickerView?.frame.origin.y -= offset.y 62 | case .cancelled, .ended, .failed: 63 | UIView.animate( 64 | withDuration: StickerView.animationDuration, 65 | delay: 0, 66 | usingSpringWithDamping: 1, 67 | initialSpringVelocity: 0, 68 | options: [], 69 | animations: { 70 | guard let view = sender.view else { return } 71 | 72 | self.stickerView?.frame = view.window!.convert(view.frame, from: view.superview!) 73 | }, 74 | completion: nil 75 | ) 76 | 77 | stickerView?.setIsPeeledOff(isPeeledOff: false, animated: true, completion: { [weak self] in 78 | sender.view?.isHidden = false 79 | self?.stickerView?.removeFromSuperview() 80 | self?.stickerView = nil 81 | }) 82 | default: 83 | break 84 | } 85 | } 86 | } 87 | 88 | extension PeelOffInteraction: UIGestureRecognizerDelegate { 89 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { 90 | otherGestureRecognizer is UIPanGestureRecognizer 91 | } 92 | } 93 | 94 | extension NSDirectionalEdgeInsets { 95 | init(uniform: CGFloat) { 96 | self.init(top: uniform, leading: uniform, bottom: uniform, trailing: uniform) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Stickers/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Stickers/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 5 | var window: UIWindow? 6 | 7 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 8 | if let windowScene = scene as? UIWindowScene { 9 | let window = UIWindow(windowScene: windowScene) 10 | window.rootViewController = UINavigationController(rootViewController: StickerCollectionController()) 11 | self.window = window 12 | window.makeKeyAndVisible() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Stickers/StickerCollectionView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public class StickerCollectionController: UICollectionViewController { 4 | let stickers = [ 5 | UIImage(named: "sticker1"), 6 | UIImage(named: "sticker2"), 7 | UIImage(named: "sticker3"), 8 | UIImage(named: "sticker4"), 9 | UIImage(named: "sticker5"), 10 | UIImage(named: "sticker6") 11 | ] 12 | 13 | init() { 14 | let layout = UICollectionViewCompositionalLayout { index, environment in 15 | let itemSize = NSCollectionLayoutSize( 16 | widthDimension: .fractionalWidth(1.0 / 2.0), 17 | heightDimension: .fractionalWidth(1.0 / 2.0) 18 | ) 19 | 20 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 21 | item.contentInsets = NSDirectionalEdgeInsets(uniform: 5.0) 22 | 23 | let groupSize = NSCollectionLayoutSize( 24 | widthDimension: .fractionalWidth(1.0), 25 | heightDimension: .estimated(100) 26 | ) 27 | 28 | let group = NSCollectionLayoutGroup.horizontal( 29 | layoutSize: groupSize, 30 | subitems: [item] 31 | ) 32 | 33 | let section = NSCollectionLayoutSection(group: group) 34 | 35 | return section 36 | } 37 | 38 | super.init(collectionViewLayout: layout) 39 | 40 | collectionView.alwaysBounceVertical = true 41 | collectionView.backgroundColor = .secondarySystemGroupedBackground 42 | collectionView.register(StickerCell.self, forCellWithReuseIdentifier: StickerCell.reuseIdentifier) 43 | 44 | title = "Stickers" 45 | } 46 | 47 | required init?(coder: NSCoder) { 48 | fatalError("init(coder:) has not been implemented") 49 | } 50 | 51 | public override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 52 | stickers.count 53 | } 54 | 55 | public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 56 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: StickerCell.reuseIdentifier, for: indexPath) as! StickerCell 57 | 58 | cell.image = stickers[indexPath.item] 59 | cell.addInteraction(PeelOffInteraction()) 60 | 61 | return cell 62 | } 63 | } 64 | 65 | public final class StickerCell: UICollectionViewCell { 66 | static let reuseIdentifier = "StickerCell" 67 | 68 | public var image: UIImage? { 69 | get { 70 | imageView.image 71 | } 72 | set { 73 | imageView.image = newValue 74 | } 75 | } 76 | 77 | let imageView: UIImageView 78 | 79 | override init(frame: CGRect) { 80 | imageView = UIImageView() 81 | imageView.contentMode = .scaleAspectFit 82 | 83 | super.init(frame: frame) 84 | 85 | contentView.addSubview(imageView) 86 | } 87 | 88 | required init?(coder: NSCoder) { 89 | fatalError("init(coder:) has not been implemented") 90 | } 91 | 92 | public override func prepareForReuse() { 93 | imageView.image = nil 94 | } 95 | 96 | public override func layoutSubviews() { 97 | super.layoutSubviews() 98 | 99 | imageView.frame = contentView.bounds 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Stickers/StickerView.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | import UIKit 3 | 4 | public class StickerView: UIView { 5 | /// The size of `sceneView` relative to `bounds`. 6 | /// 7 | /// By exceeding the receiver, `sceneView` can capture the translation of the `shadowNode`. 8 | static let scaleFactor: CGFloat = 1.7 9 | 10 | public static let animationDuration = 0.8 11 | 12 | let sceneView: SCNView 13 | 14 | let reflection: SCNNode 15 | 16 | let sticker: SCNNode 17 | 18 | var image: UIImage? { 19 | didSet { 20 | sticker.geometry?.firstMaterial?.diffuse.contents = image 21 | reflection.geometry?.firstMaterial?.diffuse.contents = image 22 | } 23 | } 24 | 25 | private(set) var isPeeledOff: Bool = false 26 | 27 | override init(frame: CGRect) { 28 | sceneView = SCNView(frame: frame) 29 | sceneView.frame.origin = .zero 30 | 31 | var scaled = frame.size 32 | scaled.width /= StickerView.scaleFactor 33 | scaled.height /= StickerView.scaleFactor 34 | 35 | sticker = SCNNode(geometry: SCNPlane(size: scaled)) 36 | reflection = SCNNode(geometry: SCNPlane(size: scaled)) 37 | 38 | super.init(frame: frame) 39 | 40 | clipsToBounds = false 41 | 42 | sceneView.isPlaying = true 43 | sceneView.autoenablesDefaultLighting = true 44 | sceneView.backgroundColor = .clear 45 | 46 | let scene = SCNScene() 47 | sceneView.scene = scene 48 | 49 | let cameraNode = SCNNode() 50 | cameraNode.camera = SCNCamera() 51 | 52 | let fov = cameraNode.camera!.fieldOfView 53 | 54 | let cameraDistance = max(frame.size.width, frame.size.height) / (2 * tan(fov / 2 * .pi / 180)) 55 | 56 | cameraNode.position = SCNVector3(x: 0, y: 0, z: Float(cameraDistance)) 57 | cameraNode.camera!.zFar = ceil(Double(cameraDistance)) 58 | 59 | scene.rootNode.addChildNode(cameraNode) 60 | 61 | let parent = SCNNode() 62 | scene.rootNode.addChildNode(parent) 63 | 64 | parent.addChildNode(sticker) 65 | 66 | let geometryModifier = """ 67 | #pragma arguments 68 | float peeled; 69 | float liftDistance; 70 | 71 | #pragma transparent 72 | #pragma body 73 | 74 | // How far are we in the animation. 75 | float t = 2 * clamp(peeled - _geometry.texcoords[0].y / 2, 0.0, 0.5); 76 | 77 | // Quadratic ease in out 78 | if (t < 0.5) { 79 | t = (4 * t * t * t); 80 | } else { 81 | t = ((t - 1) * (2 * t - 2) * (2 * t - 2) + 1); 82 | } 83 | 84 | // Quantize the displacement. I got rendering artifacts without 85 | // this step. Is this actually necessary? 86 | t = round(t * 1000.0) / 1000.0; 87 | 88 | _geometry.position.xyz += _geometry.normal * liftDistance * t; 89 | """ 90 | 91 | let surfaceModifier = """ 92 | #pragma arguments 93 | float peeled; 94 | 95 | #pragma transparent 96 | #pragma body 97 | 98 | float t = 2 * clamp(peeled - _surface.diffuseTexcoord.y / 2, 0.0, 0.5); 99 | 100 | _surface.diffuse.rgb += float3(pow(sin(3.14159 * t), 12) / 8.0); 101 | """ 102 | 103 | sticker.geometry?.firstMaterial?.setValue(0.0, forKey: "peeled") 104 | sticker.geometry?.firstMaterial?.setValue(cameraDistance * 0.25, forKey: "liftDistance") 105 | sticker.geometry?.firstMaterial?.shaderModifiers = [ 106 | .geometry: geometryModifier, 107 | .surface: surfaceModifier 108 | ] 109 | 110 | let tesselator = SCNGeometryTessellator() 111 | tesselator.edgeTessellationFactor = 50 112 | tesselator.insideTessellationFactor = 50 113 | 114 | sticker.geometry?.tessellator = tesselator 115 | sticker.renderingOrder = 1 116 | sticker.geometry?.firstMaterial?.readsFromDepthBuffer = false 117 | 118 | parent.addChildNode(reflection) 119 | 120 | let gaussianBlurFilter = CIFilter(name: "CIGaussianBlur")! 121 | gaussianBlurFilter.name = "blur" 122 | gaussianBlurFilter.setValue(0.0, forKey: "inputRadius") 123 | 124 | reflection.filters = [ gaussianBlurFilter ] 125 | 126 | addSubview(sceneView) 127 | } 128 | 129 | required init?(coder: NSCoder) { 130 | fatalError("init(coder:) has not been implemented") 131 | } 132 | 133 | override public func layoutSubviews() { 134 | let transform = CGAffineTransform( 135 | scaleX: StickerView.scaleFactor, 136 | y: StickerView.scaleFactor 137 | ) 138 | 139 | sceneView.frame = bounds.applying(transform) 140 | sceneView.center.x = bounds.midX 141 | sceneView.center.y = bounds.midY 142 | } 143 | 144 | public func setIsPeeledOff(isPeeledOff: Bool, animated: Bool = false, start: @escaping () -> Void = {}, completion: @escaping () -> Void = {}) { 145 | self.isPeeledOff = isPeeledOff 146 | 147 | let peel: CAAnimation = { 148 | let (fromValue, toValue) = isPeeledOff ? (0.0, 1.0) : (1.0, 0.0) 149 | 150 | sticker.geometry?.firstMaterial?.setValue(toValue, forKey: "peeled") 151 | 152 | let animation = CABasicAnimation(keyPath: "geometry.firstMaterial.peeled") 153 | animation.fromValue = fromValue 154 | animation.toValue = toValue 155 | animation.duration = StickerView.animationDuration 156 | 157 | return animation 158 | }() 159 | 160 | sticker.addAnimation(peel, forKey: nil) 161 | 162 | let shadowTimingFunction = CAMediaTimingFunction(name: .easeInEaseOut) 163 | 164 | let blur: CAAnimation = { 165 | let (fromValue, toValue) = isPeeledOff ? (0.0, 11.0) : (11.0, 0.0) 166 | 167 | reflection.filters?.first?.setValue(toValue, forKey: "inputRadius") 168 | 169 | let animation = CABasicAnimation(keyPath: "filters.blur.inputRadius") 170 | animation.duration = StickerView.animationDuration 171 | animation.fillMode = .backwards 172 | animation.fromValue = fromValue 173 | animation.timingFunction = CAMediaTimingFunction(name: .linear) 174 | animation.toValue = toValue 175 | 176 | return animation 177 | }() 178 | 179 | let delay = isPeeledOff ? StickerView.animationDuration / 2.5 : 0 180 | 181 | let transparency: CAAnimation = { 182 | let (fromValue, toValue) = isPeeledOff ? (0.6, 0.4) : (0.4, 0.6) as (CGFloat, CGFloat) 183 | 184 | reflection.geometry?.firstMaterial?.transparency = toValue 185 | 186 | let animation = CABasicAnimation(keyPath: "geometry.firstMaterial.transparency") 187 | animation.beginTime = delay 188 | animation.duration = StickerView.animationDuration - delay 189 | animation.fillMode = .backwards 190 | animation.fromValue = fromValue 191 | animation.timingFunction = shadowTimingFunction 192 | animation.toValue = toValue 193 | 194 | return animation 195 | }() 196 | 197 | let transform: CAAnimation = { 198 | let identity = SCNMatrix4Identity 199 | let translated = SCNMatrix4MakeTranslation(0, -30, 0) 200 | 201 | let (fromValue, toValue) = isPeeledOff ? (identity, translated) : (translated, identity) 202 | 203 | reflection.transform = toValue 204 | 205 | let animation = CABasicAnimation(keyPath: "transform") 206 | animation.beginTime = delay 207 | animation.timingFunction = shadowTimingFunction 208 | animation.fromValue = fromValue 209 | animation.toValue = toValue 210 | animation.duration = StickerView.animationDuration - delay 211 | animation.fillMode = .backwards 212 | 213 | return animation 214 | }() 215 | 216 | let group = CAAnimationGroup() 217 | group.animations = [ blur, transparency, transform ] 218 | group.duration = StickerView.animationDuration 219 | 220 | reflection.addAnimation(group, forKey: nil) 221 | 222 | // For some reason, `animationDidStart` and `animationDidStop` are not 223 | // being called if we wrap `group` in an `SCNAction`. 224 | // 225 | // Instead, let's manually time actions alongside them. This seems to 226 | // work reasonably well. 227 | sceneView.scene?.rootNode.runAction(SCNAction()) { 228 | DispatchQueue.main.async { 229 | start() 230 | } 231 | } 232 | 233 | sceneView.scene?.rootNode.runAction(SCNAction.wait(duration: StickerView.animationDuration)) { 234 | DispatchQueue.main.async { 235 | completion() 236 | } 237 | } 238 | } 239 | } 240 | 241 | private extension SCNPlane { 242 | convenience init(size: CGSize) { 243 | self.init(width: size.width, height: size.height) 244 | } 245 | } 246 | --------------------------------------------------------------------------------