├── .gitignore ├── Example ├── AppStoreTransitionAnimation.xcodeproj │ └── project.pbxproj ├── AppStoreTransitionAnimation.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── AppStoreTransitionAnimation │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── chat-icon.imageset │ │ │ ├── Contents.json │ │ │ └── icons8-chat-bubble-filled-100.png │ │ ├── gray.colorset │ │ │ └── Contents.json │ │ ├── text-color.colorset │ │ │ └── Contents.json │ │ ├── type1-background.imageset │ │ │ ├── Contents.json │ │ │ └── beautiful-black-and-white-casual-2256268.jpg │ │ ├── type1-bg-bottom.imageset │ │ │ ├── Contents.json │ │ │ └── kelsey-curtis-1596289-unsplash.jpg │ │ ├── type1color.colorset │ │ │ └── Contents.json │ │ ├── type2-background.imageset │ │ │ ├── Contents.json │ │ │ └── landscape-mountain-mountain-peak-2262620.jpg │ │ ├── type2-bg-bottom.imageset │ │ │ ├── Contents.json │ │ │ └── analise-benevides-1596040-unsplash.jpg │ │ └── type2color.colorset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Cells │ │ ├── ItemTableViewCell.swift │ │ ├── ItemTableViewCell.xib │ │ ├── Type1CollectionViewCell.swift │ │ ├── Type1CollectionViewCell.xib │ │ ├── Type2CollectionViewCell.swift │ │ └── Type2CollectionViewCell.xib │ ├── Info.plist │ ├── Type1ViewController.swift │ ├── Type2ViewController.swift │ ├── ViewController.swift │ └── Views │ │ ├── Type1HeaderView.swift │ │ └── Type1HeaderView.xib ├── Podfile ├── Podfile.lock └── Pods │ ├── Local Podspecs │ └── appstore-card-transition.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ └── project.pbxproj │ └── Target Support Files │ ├── Pods-AppStoreTransitionAnimation │ ├── Pods-AppStoreTransitionAnimation-Info.plist │ ├── Pods-AppStoreTransitionAnimation-acknowledgements.markdown │ ├── Pods-AppStoreTransitionAnimation-acknowledgements.plist │ ├── Pods-AppStoreTransitionAnimation-dummy.m │ ├── Pods-AppStoreTransitionAnimation-frameworks-Debug-input-files.xcfilelist │ ├── Pods-AppStoreTransitionAnimation-frameworks-Debug-output-files.xcfilelist │ ├── Pods-AppStoreTransitionAnimation-frameworks-Release-input-files.xcfilelist │ ├── Pods-AppStoreTransitionAnimation-frameworks-Release-output-files.xcfilelist │ ├── Pods-AppStoreTransitionAnimation-frameworks.sh │ ├── Pods-AppStoreTransitionAnimation-umbrella.h │ ├── Pods-AppStoreTransitionAnimation.debug.xcconfig │ ├── Pods-AppStoreTransitionAnimation.modulemap │ └── Pods-AppStoreTransitionAnimation.release.xcconfig │ └── appstore-card-transition │ ├── appstore-card-transition-Info.plist │ ├── appstore-card-transition-dummy.m │ ├── appstore-card-transition-prefix.pch │ ├── appstore-card-transition-umbrella.h │ ├── appstore-card-transition.modulemap │ └── appstore-card-transition.xcconfig ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── AppstoreTransition.xcodeproj │ └── project.pbxproj └── AppstoreTransition │ ├── AppstoreTransition.h │ ├── CardCollectionViewCell.swift │ ├── CardDetailViewController.swift │ ├── CardPresentationController.swift │ ├── CardTransition.swift │ ├── DismissCardAnimator.swift │ ├── Info.plist │ ├── PresentCardAnimator.swift │ ├── UIView+Extension.swift │ └── UIViewController+Extension.swift ├── appstore-card-transition.podspec └── gif ├── example1.gif ├── example2.gif ├── example3.gif ├── example4.gif └── logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | .swiftpm/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots/**/*.png 69 | fastlane/test_output 70 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AppStoreTransitionAnimation 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. 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: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/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 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/chat-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "icons8-chat-bubble-filled-100.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/chat-icon.imageset/icons8-chat-bubble-filled-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/Example/AppStoreTransitionAnimation/Assets.xcassets/chat-icon.imageset/icons8-chat-bubble-filled-100.png -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "0xF2", 13 | "alpha" : "1.000", 14 | "blue" : "0xF2", 15 | "green" : "0xF2" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/text-color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "0x5B", 13 | "alpha" : "1.000", 14 | "blue" : "0x5B", 15 | "green" : "0x5B" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type1-background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "beautiful-black-and-white-casual-2256268.jpg", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type1-background.imageset/beautiful-black-and-white-casual-2256268.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/Example/AppStoreTransitionAnimation/Assets.xcassets/type1-background.imageset/beautiful-black-and-white-casual-2256268.jpg -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type1-bg-bottom.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "kelsey-curtis-1596289-unsplash.jpg", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type1-bg-bottom.imageset/kelsey-curtis-1596289-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/Example/AppStoreTransitionAnimation/Assets.xcassets/type1-bg-bottom.imageset/kelsey-curtis-1596289-unsplash.jpg -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type1color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "0x5E", 13 | "alpha" : "1.000", 14 | "blue" : "0xBE", 15 | "green" : "0x98" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type2-background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "landscape-mountain-mountain-peak-2262620.jpg", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type2-background.imageset/landscape-mountain-mountain-peak-2262620.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/Example/AppStoreTransitionAnimation/Assets.xcassets/type2-background.imageset/landscape-mountain-mountain-peak-2262620.jpg -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type2-bg-bottom.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "analise-benevides-1596040-unsplash.jpg", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type2-bg-bottom.imageset/analise-benevides-1596040-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/Example/AppStoreTransitionAnimation/Assets.xcassets/type2-bg-bottom.imageset/analise-benevides-1596040-unsplash.jpg -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Assets.xcassets/type2color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "0xFF", 13 | "alpha" : "1.000", 14 | "blue" : "0x03", 15 | "green" : "0xDE" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/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 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 130 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Cells/ItemTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemTableViewCell.swift 3 | // AppStoreTransitionAnimation 4 | // 5 | // Created by Razvan Chelemen on 16/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ItemTableViewCell: UITableViewCell { 12 | 13 | override func awakeFromNib() { 14 | super.awakeFromNib() 15 | // Initialization code 16 | } 17 | 18 | override func setSelected(_ selected: Bool, animated: Bool) { 19 | super.setSelected(selected, animated: animated) 20 | 21 | // Configure the view for the selected state 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Cells/ItemTableViewCell.xib: -------------------------------------------------------------------------------- 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 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Cells/Type1CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Type1CollectionViewCell.swift 3 | // AppStoreTransitionAnimation 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AppstoreTransition 11 | 12 | class Type1CollectionViewCell: UICollectionViewCell { 13 | @IBOutlet weak var containerView: UIView! 14 | @IBOutlet weak var bottomContainer: UIView! 15 | @IBOutlet weak var subtitleLabel: UILabel! 16 | @IBOutlet weak var backgroundImage: UIImageView! 17 | 18 | @IBOutlet weak var reviewsLabel: UILabel! 19 | @IBOutlet weak var commentImageView: UIButton! 20 | 21 | override func awakeFromNib() { 22 | super.awakeFromNib() 23 | 24 | containerView.layer.cornerRadius = 8 25 | containerView.clipsToBounds = true 26 | } 27 | 28 | // Make it appears very responsive to touch 29 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 30 | super.touchesBegan(touches, with: event) 31 | animate(isHighlighted: true) 32 | } 33 | 34 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 35 | super.touchesEnded(touches, with: event) 36 | animate(isHighlighted: false) 37 | } 38 | 39 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 40 | super.touchesCancelled(touches, with: event) 41 | animate(isHighlighted: false) 42 | } 43 | 44 | } 45 | 46 | extension Type1CollectionViewCell: CardCollectionViewCell { 47 | 48 | var cardContentView: UIView { 49 | get { 50 | return containerView 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Cells/Type1CollectionViewCell.xib: -------------------------------------------------------------------------------- 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 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 112 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Cells/Type2CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Type2CollectionViewCell.swift 3 | // AppStoreTransitionAnimation 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AppstoreTransition 11 | 12 | class Type2CollectionViewCell: UICollectionViewCell { 13 | @IBOutlet weak var containerView: UIView! 14 | @IBOutlet weak var subtitleLabel: UILabel! 15 | @IBOutlet weak var backgroundImage: UIImageView! 16 | 17 | override func awakeFromNib() { 18 | super.awakeFromNib() 19 | 20 | containerView.layer.cornerRadius = 8 21 | containerView.clipsToBounds = true 22 | 23 | layer.shadowColor = UIColor.black.cgColor 24 | layer.shadowOpacity = 0.1 25 | layer.shadowOffset = .init(width: 0, height: 0) 26 | layer.shadowRadius = 8 27 | } 28 | 29 | // Make it appears very responsive to touch 30 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 31 | super.touchesBegan(touches, with: event) 32 | animate(isHighlighted: true) 33 | } 34 | 35 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 36 | super.touchesEnded(touches, with: event) 37 | animate(isHighlighted: false) 38 | } 39 | 40 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 41 | super.touchesCancelled(touches, with: event) 42 | animate(isHighlighted: false) 43 | } 44 | } 45 | 46 | extension Type2CollectionViewCell: CardCollectionViewCell { 47 | 48 | var cardContentView: UIView { 49 | get { 50 | return containerView 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Cells/Type2CollectionViewCell.xib: -------------------------------------------------------------------------------- 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 | 49 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Type1ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Type1ViewController.swift 3 | // AppStoreTransitionAnimation 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AppstoreTransition 11 | 12 | class Type1ViewController: UIViewController { 13 | @IBOutlet weak var tableView: UITableView! 14 | 15 | @IBOutlet weak var coverView: UIView! 16 | @IBOutlet weak var commentCoverViewTop: NSLayoutConstraint! 17 | 18 | var dismissAnimationFinishedAction: (()->())? 19 | var subtitle: String? = nil 20 | var backgroundImage: UIImage? = nil 21 | var backgroundColor: UIColor? = nil 22 | 23 | lazy var headerView: Type1HeaderView = { 24 | let view = UINib(nibName: "Type1HeaderView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! Type1HeaderView 25 | 26 | return view 27 | }() 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | view.clipsToBounds = true 33 | 34 | tableView.delegate = self 35 | tableView.dataSource = self 36 | tableView.register(UINib(nibName: "ItemTableViewCell", bundle: Bundle.main), forCellReuseIdentifier: "ItemTableViewCell") 37 | 38 | positionHeaderView() 39 | 40 | headerView.topContainerView.backgroundColor = UIColor(named: "type1color") 41 | if let subtitle = subtitle { 42 | headerView.subtitleLabel.text = subtitle 43 | } 44 | if let backgroundImage = backgroundImage { 45 | headerView.backgroundImage.image = backgroundImage 46 | } 47 | if let backgroundColor = backgroundColor { 48 | coverView.backgroundColor = backgroundColor 49 | } 50 | } 51 | 52 | override func viewDidLayoutSubviews() { 53 | super.viewDidLayoutSubviews() 54 | 55 | headerView.topContainerHeight?.constant = UIScreen.main.bounds.width * 1.272 - 66.0 56 | positionHeaderView() 57 | } 58 | 59 | private func positionHeaderView() { 60 | let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) 61 | headerView.frame.size = size 62 | tableView.tableHeaderView = headerView 63 | } 64 | 65 | } 66 | 67 | extension Type1ViewController: UITableViewDataSource { 68 | 69 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 70 | return 20 71 | } 72 | 73 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 74 | let cell = tableView.dequeueReusableCell(withIdentifier: "ItemTableViewCell", for: indexPath) 75 | 76 | return cell 77 | } 78 | 79 | } 80 | 81 | extension Type1ViewController: UITableViewDelegate { 82 | 83 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 84 | // prevent bouncing when swiping down to close 85 | scrollView.bounces = scrollView.contentOffset.y > 100 86 | 87 | dismissHandler.scrollViewDidScroll(scrollView) 88 | 89 | if scrollView.contentOffset.y < 0 { 90 | scrollView.contentOffset = .zero 91 | } 92 | } 93 | 94 | } 95 | 96 | extension Type1ViewController: CardDetailViewController { 97 | 98 | var cardContentView: UIView { 99 | return headerView 100 | } 101 | 102 | var scrollView: UIScrollView? { 103 | return tableView 104 | } 105 | 106 | func didStartPresentAnimationProgress() { 107 | tableView.contentOffset = CGPoint(x: 0, y: 0) 108 | } 109 | 110 | func didBeginDismissAnimation() { 111 | tableView.setContentOffset(.zero, animated: true) 112 | } 113 | 114 | func didFinishDismissAnimation() { 115 | dismissAnimationFinishedAction?() 116 | } 117 | 118 | func didChangeDismissAnimationProgress(progress: CGFloat) { 119 | var progress = max(0, progress) 120 | progress = min(1, progress) 121 | commentCoverViewTop.constant = -58 * progress 122 | } 123 | 124 | func didCancelDismissAnimation(progress: CGFloat) { 125 | if progress < 1.0 { 126 | commentCoverViewTop.constant = 0 127 | tableView.setContentOffset(.zero, animated: true) 128 | 129 | UIView.animate(withDuration: 0.2) { 130 | self.view.setNeedsLayout() 131 | self.view.layoutIfNeeded() 132 | } 133 | } 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Type2ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Type2ViewController.swift 3 | // AppStoreTransitionAnimation 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AppstoreTransition 11 | 12 | class Type2ViewController: UIViewController { 13 | @IBOutlet weak var contentScrollView: UIScrollView! 14 | @IBOutlet weak var headerView: UIView! 15 | @IBOutlet weak var subtitleLabel: UILabel! 16 | @IBOutlet weak var backgroundImage: UIImageView! 17 | @IBOutlet weak var heightConstraint: NSLayoutConstraint! 18 | 19 | var subtitle: String? = nil 20 | var background: UIImage? = nil 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | view.clipsToBounds = true 26 | contentScrollView.delegate = self 27 | 28 | scrollView?.contentInsetAdjustmentBehavior = .never 29 | 30 | let _ = dismissHandler 31 | if let subtitle = subtitle { 32 | subtitleLabel.text = subtitle 33 | } 34 | if let background = background { 35 | backgroundImage.image = background 36 | } 37 | 38 | heightConstraint.constant = UIScreen.main.bounds.width * 1.272 - 16.0 39 | } 40 | 41 | } 42 | 43 | extension Type2ViewController: UIScrollViewDelegate { 44 | 45 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 46 | // prevent bouncing when swiping down to close 47 | scrollView.bounces = scrollView.contentOffset.y > 100 48 | 49 | dismissHandler.scrollViewDidScroll(scrollView) 50 | } 51 | 52 | } 53 | 54 | extension Type2ViewController: CardDetailViewController { 55 | 56 | var scrollView: UIScrollView? { 57 | return contentScrollView 58 | } 59 | 60 | 61 | var cardContentView: UIView { 62 | return headerView 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // AppStoreTransitionAnimation 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AppstoreTransition 11 | 12 | class ViewController: UIViewController { 13 | @IBOutlet weak var collectionView: UICollectionView! 14 | 15 | private var transition: CardTransition? 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | collectionView.dataSource = self 21 | collectionView.delegate = self 22 | collectionView.register(UINib(nibName: "Type1CollectionViewCell", bundle: Bundle.main), forCellWithReuseIdentifier: "Type1CollectionViewCell") 23 | collectionView.register(UINib(nibName: "Type2CollectionViewCell", bundle: Bundle.main), forCellWithReuseIdentifier: "Type2CollectionViewCell") 24 | 25 | let layout = (collectionView.collectionViewLayout as! UICollectionViewFlowLayout) 26 | let aspect: CGFloat = 1.272 27 | let width = UIScreen.main.bounds.width 28 | layout.itemSize = CGSize(width:width, height: width * aspect) 29 | layout.minimumLineSpacing = 0 30 | layout.minimumInteritemSpacing = 0 31 | } 32 | 33 | } 34 | 35 | extension ViewController: UICollectionViewDataSource { 36 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 37 | return 4 38 | } 39 | 40 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 41 | var cell: UICollectionViewCell 42 | 43 | switch indexPath.row { 44 | case 0: 45 | cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Type1CollectionViewCell", for: indexPath) 46 | case 1: 47 | cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Type2CollectionViewCell", for: indexPath) 48 | case 2: 49 | let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: "Type1CollectionViewCell", for: indexPath) as! Type1CollectionViewCell 50 | customCell.subtitleLabel.text = "You can dismiss from bottom this one" 51 | customCell.backgroundImage.image = UIImage(named: "type1-bg-bottom") 52 | customCell.containerView.backgroundColor = .white 53 | customCell.reviewsLabel.textColor = UIColor(named: "text-color") 54 | customCell.commentImageView.tintColor = UIColor(named: "text-color") 55 | cell = customCell 56 | case 3: 57 | let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: "Type2CollectionViewCell", for: indexPath) as! Type2CollectionViewCell 58 | customCell.subtitleLabel.text = "Bottom dismissible" 59 | customCell.backgroundImage.image = UIImage(named: "type2-bg-bottom") 60 | cell = customCell 61 | default: 62 | fatalError("Invalid cell") 63 | } 64 | 65 | return cell 66 | } 67 | 68 | } 69 | 70 | extension ViewController: UICollectionViewDelegate { 71 | 72 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 73 | switch indexPath.row { 74 | case 0: 75 | showType1(indexPath: indexPath) 76 | case 1: 77 | showType2(indexPath: indexPath) 78 | case 2: 79 | showType3(indexPath: indexPath, bottomDismiss: true) 80 | case 3: 81 | showType4(indexPath: indexPath, bottomDismiss: true) 82 | default: 83 | fatalError("Invalid cell") 84 | } 85 | } 86 | 87 | private func showType1(indexPath: IndexPath, bottomDismiss: Bool = false) { 88 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 89 | let viewController = storyboard.instantiateViewController(withIdentifier: "type1") as! Type1ViewController 90 | 91 | if let cell = collectionView.cellForItem(at: indexPath) as? Type1CollectionViewCell { 92 | cell.bottomContainer.alpha = 0 93 | 94 | viewController.dismissAnimationFinishedAction = { 95 | UIView.animate(withDuration: 0.3, animations: { 96 | cell.bottomContainer.alpha = 1.0 97 | }) 98 | } 99 | } 100 | 101 | // Get tapped cell location 102 | let cell = collectionView.cellForItem(at: indexPath) as! CardCollectionViewCell 103 | 104 | cell.settings.isEnabledBottomClose = bottomDismiss 105 | cell.settings.cardContainerInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 0, right: 8.0) 106 | 107 | transition = CardTransition(cell: cell, settings: cell.settings) 108 | viewController.settings = cell.settings 109 | viewController.transitioningDelegate = transition 110 | 111 | // If `modalPresentationStyle` is not `.fullScreen`, this should be set to true to make status bar depends on presented vc. 112 | //viewController.modalPresentationCapturesStatusBarAppearance = true 113 | viewController.modalPresentationStyle = .custom 114 | 115 | presentExpansion(viewController, cell: cell, animated: true) 116 | } 117 | 118 | private func showType2(indexPath: IndexPath, bottomDismiss: Bool = false) { 119 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 120 | let viewController = storyboard.instantiateViewController(withIdentifier: "type2") as! Type2ViewController 121 | 122 | // Get tapped cell location 123 | let cell = collectionView.cellForItem(at: indexPath) as! CardCollectionViewCell 124 | cell.settings.cardContainerInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 8.0, right: 16.0) 125 | cell.settings.isEnabledBottomClose = bottomDismiss 126 | 127 | transition = CardTransition(cell: cell, settings: cell.settings) 128 | viewController.settings = cell.settings 129 | viewController.transitioningDelegate = transition 130 | 131 | // If `modalPresentationStyle` is not `.fullScreen`, this should be set to true to make status bar depends on presented vc. 132 | //viewController.modalPresentationCapturesStatusBarAppearance = true 133 | viewController.modalPresentationStyle = .custom 134 | 135 | presentExpansion(viewController, cell: cell, animated: true) 136 | } 137 | 138 | private func showType3(indexPath: IndexPath, bottomDismiss: Bool = false) { 139 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 140 | let viewController = storyboard.instantiateViewController(withIdentifier: "type1") as! Type1ViewController 141 | 142 | if let cell = collectionView.cellForItem(at: indexPath) as? Type1CollectionViewCell { 143 | cell.bottomContainer.alpha = 0 144 | 145 | viewController.dismissAnimationFinishedAction = { 146 | UIView.animate(withDuration: 0.3, animations: { 147 | cell.bottomContainer.alpha = 1.0 148 | }) 149 | } 150 | } 151 | 152 | // Get tapped cell location 153 | let cell = collectionView.cellForItem(at: indexPath) as! CardCollectionViewCell 154 | 155 | cell.settings.isEnabledBottomClose = bottomDismiss 156 | cell.settings.cardContainerInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 0, right: 8.0) 157 | 158 | transition = CardTransition(cell: cell, settings: cell.settings) 159 | viewController.settings = cell.settings 160 | viewController.transitioningDelegate = transition 161 | viewController.subtitle = "You can dismiss from bottom this one" 162 | viewController.backgroundImage = UIImage(named: "type1-bg-bottom") 163 | viewController.backgroundColor = .white 164 | 165 | // If `modalPresentationStyle` is not `.fullScreen`, this should be set to true to make status bar depends on presented vc. 166 | //viewController.modalPresentationCapturesStatusBarAppearance = true 167 | viewController.modalPresentationStyle = .custom 168 | 169 | presentExpansion(viewController, cell: cell, animated: true) 170 | } 171 | 172 | private func showType4(indexPath: IndexPath, bottomDismiss: Bool = false) { 173 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 174 | let viewController = storyboard.instantiateViewController(withIdentifier: "type2") as! Type2ViewController 175 | 176 | // Get tapped cell location 177 | let cell = collectionView.cellForItem(at: indexPath) as! CardCollectionViewCell 178 | cell.settings.cardContainerInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 8.0, right: 16.0) 179 | cell.settings.isEnabledBottomClose = bottomDismiss 180 | 181 | transition = CardTransition(cell: cell, settings: cell.settings) 182 | viewController.settings = cell.settings 183 | viewController.transitioningDelegate = transition 184 | viewController.subtitle = "Bottom dismissible" 185 | viewController.background = UIImage(named: "type2-bg-bottom") 186 | 187 | // If `modalPresentationStyle` is not `.fullScreen`, this should be set to true to make status bar depends on presented vc. 188 | //viewController.modalPresentationCapturesStatusBarAppearance = true 189 | viewController.modalPresentationStyle = .custom 190 | 191 | presentExpansion(viewController, cell: cell, animated: true) 192 | } 193 | 194 | } 195 | 196 | extension ViewController: CardsViewController { 197 | 198 | } 199 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Views/Type1HeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Type1HeaderView.swift 3 | // AppStoreTransitionAnimation 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Type1HeaderView: UIView { 12 | @IBOutlet weak var topContainerView: UIView! 13 | @IBOutlet weak var playerImageView: UIImageView! 14 | @IBOutlet weak var subtitleLabel: UILabel! 15 | @IBOutlet weak var backgroundImage: UIImageView! 16 | 17 | @IBOutlet weak var topContainerHeight: NSLayoutConstraint! 18 | @IBOutlet weak var textWidth: NSLayoutConstraint! 19 | 20 | override func awakeFromNib() { 21 | super.awakeFromNib() 22 | 23 | textWidth.constant = UIScreen.main.bounds.width - 32.0 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Example/AppStoreTransitionAnimation/Views/Type1HeaderView.xib: -------------------------------------------------------------------------------- 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 | 40 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 88 | 94 | 100 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '11.0' 3 | 4 | target 'AppStoreTransitionAnimation' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for AppStoreTransitionAnimation 9 | pod 'appstore-card-transition', :path => '../' 10 | 11 | end 12 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - appstore-card-transition (1.0.3) 3 | 4 | DEPENDENCIES: 5 | - appstore-card-transition (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | appstore-card-transition: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | appstore-card-transition: 909d2198d6c22676eea76bac2721e9026265aa90 13 | 14 | PODFILE CHECKSUM: 1ed7eee8a7159fd92c1c137ef930ca0697e46588 15 | 16 | COCOAPODS: 1.7.5 17 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/appstore-card-transition.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appstore-card-transition", 3 | "version": "1.0.3", 4 | "summary": "Appstore card animation transition. UICollectionView and UITableView card expand animated transition for iOS.", 5 | "module_name": "AppstoreTransition", 6 | "swift_versions": "5.0", 7 | "description": "Appstore card animation transition. UICollectionView and UITableView card expand animated transition for iOS. Expandable collectionview and tableview cells. Master detail transition animation.", 8 | "homepage": "https://github.com/appssemble/appstore-card-transition", 9 | "screenshots": [ 10 | "https://github.com/appssemble/appstore-card-transition/blob/master/gif/example1.gif?raw=true", 11 | "https://github.com/appssemble/appstore-card-transition/blob/master/gif/example2.gif?raw=true" 12 | ], 13 | "license": { 14 | "type": "MIT", 15 | "file": "LICENSE" 16 | }, 17 | "authors": { 18 | "Razvan Chelemen": "chelemen.razvan@gmail.com" 19 | }, 20 | "social_media_url": "https://twitter.com/chelemen_razvan", 21 | "platforms": { 22 | "ios": "11.0" 23 | }, 24 | "source": { 25 | "git": "https://github.com/appssemble/appstore-card-transition.git", 26 | "tag": "1.0.3" 27 | }, 28 | "source_files": "Sources/AppstoreTransition/*.{h,m,swift,c}", 29 | "swift_version": "5.0" 30 | } 31 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - appstore-card-transition (1.0.3) 3 | 4 | DEPENDENCIES: 5 | - appstore-card-transition (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | appstore-card-transition: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | appstore-card-transition: 909d2198d6c22676eea76bac2721e9026265aa90 13 | 14 | PODFILE CHECKSUM: 1ed7eee8a7159fd92c1c137ef930ca0697e46588 15 | 16 | COCOAPODS: 1.7.5 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## appstore-card-transition 5 | 6 | MIT License 7 | 8 | Copyright (c) 2019 appssemble 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | Generated by CocoaPods - https://cocoapods.org 29 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | MIT License 18 | 19 | Copyright (c) 2019 appssemble 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | License 40 | MIT 41 | Title 42 | appstore-card-transition 43 | Type 44 | PSGroupSpecifier 45 | 46 | 47 | FooterText 48 | Generated by CocoaPods - https://cocoapods.org 49 | Title 50 | 51 | Type 52 | PSGroupSpecifier 53 | 54 | 55 | StringsTable 56 | Acknowledgements 57 | Title 58 | Acknowledgements 59 | 60 | 61 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_AppStoreTransitionAnimation : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_AppStoreTransitionAnimation 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/appstore-card-transition/AppstoreTransition.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppstoreTransition.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/appstore-card-transition/AppstoreTransition.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppstoreTransition.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | 23 | # Used as a return value for each invocation of `strip_invalid_archs` function. 24 | STRIP_BINARY_RETVAL=0 25 | 26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 29 | 30 | # Copies and strips a vendored framework 31 | install_framework() 32 | { 33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 34 | local source="${BUILT_PRODUCTS_DIR}/$1" 35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 37 | elif [ -r "$1" ]; then 38 | local source="$1" 39 | fi 40 | 41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 42 | 43 | if [ -L "${source}" ]; then 44 | echo "Symlinked..." 45 | source="$(readlink "${source}")" 46 | fi 47 | 48 | # Use filter instead of exclude so missing patterns don't throw errors. 49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 51 | 52 | local basename 53 | basename="$(basename -s .framework "$1")" 54 | binary="${destination}/${basename}.framework/${basename}" 55 | 56 | if ! [ -r "$binary" ]; then 57 | binary="${destination}/${basename}" 58 | elif [ -L "${binary}" ]; then 59 | echo "Destination binary is symlinked..." 60 | dirname="$(dirname "${binary}")" 61 | binary="${dirname}/$(readlink "${binary}")" 62 | fi 63 | 64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 66 | strip_invalid_archs "$binary" 67 | fi 68 | 69 | # Resign the code if required by the build settings to avoid unstable apps 70 | code_sign_if_enabled "${destination}/$(basename "$1")" 71 | 72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 74 | local swift_runtime_libs 75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 76 | for lib in $swift_runtime_libs; do 77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 79 | code_sign_if_enabled "${destination}/${lib}" 80 | done 81 | fi 82 | } 83 | 84 | # Copies and strips a vendored dSYM 85 | install_dsym() { 86 | local source="$1" 87 | if [ -r "$source" ]; then 88 | # Copy the dSYM into a the targets temp dir. 89 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 90 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 91 | 92 | local basename 93 | basename="$(basename -s .framework.dSYM "$source")" 94 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 95 | 96 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 97 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 98 | strip_invalid_archs "$binary" 99 | fi 100 | 101 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 102 | # Move the stripped file into its final destination. 103 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 104 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 105 | else 106 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 107 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 108 | fi 109 | fi 110 | } 111 | 112 | # Copies the bcsymbolmap files of a vendored framework 113 | install_bcsymbolmap() { 114 | local bcsymbolmap_path="$1" 115 | local destination="${BUILT_PRODUCTS_DIR}" 116 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 117 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 118 | } 119 | 120 | # Signs a framework with the provided identity 121 | code_sign_if_enabled() { 122 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 123 | # Use the current code_sign_identity 124 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 125 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 126 | 127 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 128 | code_sign_cmd="$code_sign_cmd &" 129 | fi 130 | echo "$code_sign_cmd" 131 | eval "$code_sign_cmd" 132 | fi 133 | } 134 | 135 | # Strip invalid architectures 136 | strip_invalid_archs() { 137 | binary="$1" 138 | # Get architectures for current target binary 139 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 140 | # Intersect them with the architectures we are building for 141 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 142 | # If there are no archs supported by this binary then warn the user 143 | if [[ -z "$intersected_archs" ]]; then 144 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 145 | STRIP_BINARY_RETVAL=0 146 | return 147 | fi 148 | stripped="" 149 | for arch in $binary_archs; do 150 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 151 | # Strip non-valid architectures in-place 152 | lipo -remove "$arch" -output "$binary" "$binary" 153 | stripped="$stripped $arch" 154 | fi 155 | done 156 | if [[ "$stripped" ]]; then 157 | echo "Stripped $binary of architectures:$stripped" 158 | fi 159 | STRIP_BINARY_RETVAL=1 160 | } 161 | 162 | 163 | if [[ "$CONFIGURATION" == "Debug" ]]; then 164 | install_framework "${BUILT_PRODUCTS_DIR}/appstore-card-transition/AppstoreTransition.framework" 165 | fi 166 | if [[ "$CONFIGURATION" == "Release" ]]; then 167 | install_framework "${BUILT_PRODUCTS_DIR}/appstore-card-transition/AppstoreTransition.framework" 168 | fi 169 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 170 | wait 171 | fi 172 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_AppStoreTransitionAnimationVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_AppStoreTransitionAnimationVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/appstore-card-transition" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/appstore-card-transition/AppstoreTransition.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "AppstoreTransition" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_AppStoreTransitionAnimation { 2 | umbrella header "Pods-AppStoreTransitionAnimation-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AppStoreTransitionAnimation/Pods-AppStoreTransitionAnimation.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/appstore-card-transition" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/appstore-card-transition/AppstoreTransition.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "AppstoreTransition" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/appstore-card-transition/appstore-card-transition-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/appstore-card-transition/appstore-card-transition-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_appstore_card_transition : NSObject 3 | @end 4 | @implementation PodsDummy_appstore_card_transition 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/appstore-card-transition/appstore-card-transition-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/appstore-card-transition/appstore-card-transition-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | #import "AppstoreTransition.h" 14 | 15 | FOUNDATION_EXPORT double AppstoreTransitionVersionNumber; 16 | FOUNDATION_EXPORT const unsigned char AppstoreTransitionVersionString[]; 17 | 18 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/appstore-card-transition/appstore-card-transition.modulemap: -------------------------------------------------------------------------------- 1 | framework module AppstoreTransition { 2 | umbrella header "appstore-card-transition-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/appstore-card-transition/appstore-card-transition.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/appstore-card-transition 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 appssemble 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "AppstoreTransition", 8 | platforms: [.iOS(.v11)], 9 | products: [ 10 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 11 | .library( 12 | name: "AppstoreTransition", 13 | targets: ["AppstoreTransition"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 22 | .target( 23 | name: "AppstoreTransition", 24 | dependencies: []) 25 | ] 26 | ) 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # appstore-card-transition 4 | 5 | [![Version](https://img.shields.io/cocoapods/v/appstore-card-transition.svg)](http://cocoapods.org/pods/appstore-card-transition) 6 | [![License](https://img.shields.io/cocoapods/l/appstore-card-transition.svg)](https://github.com/appssemble/appstore-card-transition/blob/master/LICENSE?raw=true) 7 | ![Xcode 10.0+](https://img.shields.io/badge/Xcode-10.0%2B-blue.svg) 8 | ![iOS 11.0+](https://img.shields.io/badge/iOS-11.0%2B-blue.svg) 9 | ![Swift 5.0+](https://img.shields.io/badge/Swift-5.0%2B-orange.svg) 10 | 11 | Appstore card animation transition. UICollectionView and UITableView card expand animated transition. This library tries to add the appstore transition to your own app. The goal is to be as simple as possible to integrate in an app while keeping the flexibility and customization alive. 12 | 13 | ### Top dismissal 14 | 15 | 16 | 17 | ### Bottom dismissal 18 | 19 | 20 | 21 | ## How it works 22 | 23 | appstore-card-transition uses the `UIViewControllerTransitioningDelegate` to implement the a custom transition animation. The initial frame of the selected cell is taken and the details view controller is animated from that position to the final expanded mode. Making sure that the expansion of the details view controller looks good falls in your responsability. 24 | 25 | For closing the details view controller, gesture recognizers are used and the details view controller is animated back to the size of the cell, note that you can change the position of the cell while the details is shown to provide a more dynamic behaviour. 26 | 27 | Most of the parameteres are customizable and callbacks for each meaningful action is provided. 28 | 29 | ## Installation 30 | 31 | ### CocoaPods 32 | 33 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: 34 | 35 | ```bash 36 | $ gem install cocoapods 37 | ``` 38 | 39 | To integrate appstore-card-transition into your Xcode project using CocoaPods, specify it in your `Podfile`: 40 | 41 | ```ruby 42 | use_frameworks! 43 | 44 | target '' do 45 | pod 'appstore-card-transition' 46 | end 47 | ``` 48 | 49 | Then, run the following command: 50 | 51 | ```bash 52 | $ pod repo update 53 | $ pod install 54 | ``` 55 | 56 | ### Swift Package Manager 57 | 58 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but appstore-card-transition does support its use on supported platforms. 59 | 60 | Once you have your Swift package set up, adding appstore-card-transition as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 61 | 62 | ```swift 63 | dependencies: [ 64 | .package(url: "https://github.com/appssemble/appstore-card-transition.git", from: "1.0.3") 65 | ] 66 | ``` 67 | 68 | ### Manual 69 | 70 | Add the library project as a subproject and set the library as a target dependency. Here is a step by step that we recommend: 71 | 72 | 1. Clone this repo (as a submodule or in a different directory, it's up to you); 73 | 2. Drag `AppstoreTransition.xcodeproj` as a subproject; 74 | 3. In your main `.xcodeproj` file, select the desired target(s); 75 | 4. Go to **Build Phases**, expand Target Dependencies, and add `AppstoreTransition`; 76 | 5. In Swift, `import AppstoreTransition` and you are good to go! 77 | 78 | ## Basic usage guide 79 | 80 | First make sure your cells implement the `CardCollectionViewCell` protocol. 81 | 82 | ```swift 83 | extension YourCollectionViewCell: CardCollectionViewCell { 84 | var cardContentView: UIView { 85 | get { 86 | return containerView 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | If you want your cells to also be responsive to touches, override the following methods: 93 | 94 | ```swift 95 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 96 | super.touchesBegan(touches, with: event) 97 | animate(isHighlighted: true) 98 | } 99 | 100 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 101 | super.touchesEnded(touches, with: event) 102 | animate(isHighlighted: false) 103 | } 104 | 105 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 106 | super.touchesCancelled(touches, with: event) 107 | animate(isHighlighted: false) 108 | } 109 | ``` 110 | 111 | Your cell is now all setup, now go to your details view controller and make it conform to the `CardDetailViewController` protocol. 112 | 113 | ```swift 114 | extension YourDetailsViewController: CardDetailViewController { 115 | 116 | var scrollView: UIScrollView { 117 | return contentScrollView // the scrollview (or tableview) you use in your details view controller 118 | } 119 | 120 | 121 | var cardContentView: UIView { 122 | return headerView // can be just a view at the top of the scrollview or the tableHeaderView 123 | } 124 | 125 | } 126 | ``` 127 | 128 | One more thing you need to hook in your details view controller. Make sure you call the `dismissHandler` in your `scrollViewDidScroll`: 129 | 130 | ```swift 131 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 132 | dismissHandler.scrollViewDidScroll(scrollView) 133 | } 134 | ``` 135 | 136 | Now you are ready to add the actual transition. In your `cellForItemAt` method, after you configured your cell as desired, make sure you set the following: 137 | 138 | ```swift 139 | cell.settings.cardContainerInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 8.0, right: 16.0) //set this only if your cardContentView has some margins relative to the actual cell content view. 140 | 141 | transition = CardTransition(cell: cell, settings: cell.settings) //create the transition 142 | viewController.settings = cell.settings //make sure same settings are used by both the details view controller and the cell 143 | //set the transition 144 | viewController.transitioningDelegate = transition 145 | viewController.modalPresentationStyle = .custom 146 | 147 | //actually present the details view controller 148 | presentExpansion(viewController, cell: cell, animated: true) 149 | ``` 150 | 151 | If you got here you should now have a basic appstore transition. It might not be perfect yet but its definitely a strong start. If something doesn't look well check if the constraints from your details view controller play well with resizing. 152 | 153 | ## Tweaking and troubleshooting 154 | 155 | Playing with the parameters: check the `TransitionSettings` class. 156 | Most common issues are animation glitches. To prevent those, make sure your constraints are properly set (especailly the top ones) and safe areas work as expected. 157 | 158 | Next, make sure your `cardContainerInsets` are properly set and they reflect the actual ones from your cell. 159 | 160 | Lastly, your scrollview might need some scrolling to match the actual cell look (it might need some more top inset than the cell for instance). For this case you can scroll the content as needed in your `viewDidLoad` method and for the dismiss animation you can use the `dismissalScrollViewContentOffset` property from `TransitionSettings`. 161 | 162 | ## Customization 163 | 164 | Most often than not, you'll want to animate some other content alongside the appstore animation. For this purpose action blocks are available. You can implement the following callbacks to receive changes in the transition progress. 165 | 166 | ```swift 167 | extension YourDetailsViewController: CardDetailViewController { 168 | 169 | func didStartPresentAnimationProgress() { ... } 170 | func didFinishPresentAnimationProgress() { ... } 171 | 172 | func didBeginDismissAnimation() { ... } 173 | func didChangeDismissAnimationProgress(progress:CGFloat) { ... } 174 | func didFinishDismissAnimation() { ... } 175 | func didCancelDismissAnimation(progress:CGFloat) { ... } 176 | 177 | } 178 | ``` 179 | 180 | ## Example 181 | 182 | Checkout the demo project to see some examples of what the library can do and how its done. 183 | 184 | ## Contribute 185 | 186 | We're aware that this is far from perfect so any improvement or feature is welcomed. If you made an awesome change don't hesitate to create a pull request. 187 | 188 | This project is inspired from [this project](https://github.com/aunnnn/AppStoreiOS11InteractiveTransition) 189 | 190 | ## Let us help you 191 | 192 | Do you need help with your app development? [Drop us a line](https://appssemble.com) 193 | 194 | 195 | 196 |

197 |
198 |
199 | 200 |

201 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 74C1F3D8228BECEB000B6AF2 /* AppstoreTransition.h in Headers */ = {isa = PBXBuildFile; fileRef = 74C1F3D6228BECEB000B6AF2 /* AppstoreTransition.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 74C1F3E6228BEDE8000B6AF2 /* CardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C1F3E5228BEDE8000B6AF2 /* CardCollectionViewCell.swift */; }; 12 | 74C1F3EC228BEDF9000B6AF2 /* CardTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C1F3E7228BEDF7000B6AF2 /* CardTransition.swift */; }; 13 | 74C1F3ED228BEDF9000B6AF2 /* CardPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C1F3E8228BEDF9000B6AF2 /* CardPresentationController.swift */; }; 14 | 74C1F3EE228BEDF9000B6AF2 /* DismissCardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C1F3E9228BEDF9000B6AF2 /* DismissCardAnimator.swift */; }; 15 | 74C1F3EF228BEDF9000B6AF2 /* CardDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C1F3EA228BEDF9000B6AF2 /* CardDetailViewController.swift */; }; 16 | 74C1F3F0228BEDF9000B6AF2 /* PresentCardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C1F3EB228BEDF9000B6AF2 /* PresentCardAnimator.swift */; }; 17 | 74C1F3F9228BEF2C000B6AF2 /* UIView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C1F3F8228BEF2C000B6AF2 /* UIView+Extension.swift */; }; 18 | 74C1F405228C03FB000B6AF2 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C1F404228C03FB000B6AF2 /* UIViewController+Extension.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 74C1F3D3228BECEB000B6AF2 /* AppstoreTransition.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppstoreTransition.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 74C1F3D6228BECEB000B6AF2 /* AppstoreTransition.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppstoreTransition.h; sourceTree = ""; }; 24 | 74C1F3D7228BECEB000B6AF2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 25 | 74C1F3E5228BEDE8000B6AF2 /* CardCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardCollectionViewCell.swift; sourceTree = ""; }; 26 | 74C1F3E7228BEDF7000B6AF2 /* CardTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardTransition.swift; sourceTree = ""; }; 27 | 74C1F3E8228BEDF9000B6AF2 /* CardPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardPresentationController.swift; sourceTree = ""; }; 28 | 74C1F3E9228BEDF9000B6AF2 /* DismissCardAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissCardAnimator.swift; sourceTree = ""; }; 29 | 74C1F3EA228BEDF9000B6AF2 /* CardDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardDetailViewController.swift; sourceTree = ""; }; 30 | 74C1F3EB228BEDF9000B6AF2 /* PresentCardAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentCardAnimator.swift; sourceTree = ""; }; 31 | 74C1F3F8228BEF2C000B6AF2 /* UIView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extension.swift"; sourceTree = ""; }; 32 | 74C1F404228C03FB000B6AF2 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | 74C1F3D0228BECEB000B6AF2 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | 74C1F3C9228BECEB000B6AF2 = { 47 | isa = PBXGroup; 48 | children = ( 49 | 74C1F3D5228BECEB000B6AF2 /* AppstoreTransition */, 50 | 74C1F3D4228BECEB000B6AF2 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 74C1F3D4228BECEB000B6AF2 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 74C1F3D3228BECEB000B6AF2 /* AppstoreTransition.framework */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 74C1F3D5228BECEB000B6AF2 /* AppstoreTransition */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 74C1F3EA228BEDF9000B6AF2 /* CardDetailViewController.swift */, 66 | 74C1F3E8228BEDF9000B6AF2 /* CardPresentationController.swift */, 67 | 74C1F3E7228BEDF7000B6AF2 /* CardTransition.swift */, 68 | 74C1F3E9228BEDF9000B6AF2 /* DismissCardAnimator.swift */, 69 | 74C1F3EB228BEDF9000B6AF2 /* PresentCardAnimator.swift */, 70 | 74C1F3E5228BEDE8000B6AF2 /* CardCollectionViewCell.swift */, 71 | 74C1F3D6228BECEB000B6AF2 /* AppstoreTransition.h */, 72 | 74C1F3D7228BECEB000B6AF2 /* Info.plist */, 73 | 74C1F3F8228BEF2C000B6AF2 /* UIView+Extension.swift */, 74 | 74C1F404228C03FB000B6AF2 /* UIViewController+Extension.swift */, 75 | ); 76 | path = AppstoreTransition; 77 | sourceTree = ""; 78 | }; 79 | /* End PBXGroup section */ 80 | 81 | /* Begin PBXHeadersBuildPhase section */ 82 | 74C1F3CE228BECEB000B6AF2 /* Headers */ = { 83 | isa = PBXHeadersBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | 74C1F3D8228BECEB000B6AF2 /* AppstoreTransition.h in Headers */, 87 | ); 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | /* End PBXHeadersBuildPhase section */ 91 | 92 | /* Begin PBXNativeTarget section */ 93 | 74C1F3D2228BECEB000B6AF2 /* AppstoreTransition */ = { 94 | isa = PBXNativeTarget; 95 | buildConfigurationList = 74C1F3DB228BECEB000B6AF2 /* Build configuration list for PBXNativeTarget "AppstoreTransition" */; 96 | buildPhases = ( 97 | 74C1F3CE228BECEB000B6AF2 /* Headers */, 98 | 74C1F3CF228BECEB000B6AF2 /* Sources */, 99 | 74C1F3D0228BECEB000B6AF2 /* Frameworks */, 100 | 74C1F3D1228BECEB000B6AF2 /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = AppstoreTransition; 107 | productName = AppstoreTransition; 108 | productReference = 74C1F3D3228BECEB000B6AF2 /* AppstoreTransition.framework */; 109 | productType = "com.apple.product-type.framework"; 110 | }; 111 | /* End PBXNativeTarget section */ 112 | 113 | /* Begin PBXProject section */ 114 | 74C1F3CA228BECEB000B6AF2 /* Project object */ = { 115 | isa = PBXProject; 116 | attributes = { 117 | LastUpgradeCheck = 1020; 118 | ORGANIZATIONNAME = appssemble; 119 | TargetAttributes = { 120 | 74C1F3D2228BECEB000B6AF2 = { 121 | CreatedOnToolsVersion = 10.2; 122 | LastSwiftMigration = 1020; 123 | }; 124 | }; 125 | }; 126 | buildConfigurationList = 74C1F3CD228BECEB000B6AF2 /* Build configuration list for PBXProject "AppstoreTransition" */; 127 | compatibilityVersion = "Xcode 9.3"; 128 | developmentRegion = en; 129 | hasScannedForEncodings = 0; 130 | knownRegions = ( 131 | en, 132 | ); 133 | mainGroup = 74C1F3C9228BECEB000B6AF2; 134 | productRefGroup = 74C1F3D4228BECEB000B6AF2 /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | 74C1F3D2228BECEB000B6AF2 /* AppstoreTransition */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXResourcesBuildPhase section */ 144 | 74C1F3D1228BECEB000B6AF2 /* Resources */ = { 145 | isa = PBXResourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXResourcesBuildPhase section */ 152 | 153 | /* Begin PBXSourcesBuildPhase section */ 154 | 74C1F3CF228BECEB000B6AF2 /* Sources */ = { 155 | isa = PBXSourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | 74C1F405228C03FB000B6AF2 /* UIViewController+Extension.swift in Sources */, 159 | 74C1F3EF228BEDF9000B6AF2 /* CardDetailViewController.swift in Sources */, 160 | 74C1F3F9228BEF2C000B6AF2 /* UIView+Extension.swift in Sources */, 161 | 74C1F3ED228BEDF9000B6AF2 /* CardPresentationController.swift in Sources */, 162 | 74C1F3EE228BEDF9000B6AF2 /* DismissCardAnimator.swift in Sources */, 163 | 74C1F3EC228BEDF9000B6AF2 /* CardTransition.swift in Sources */, 164 | 74C1F3F0228BEDF9000B6AF2 /* PresentCardAnimator.swift in Sources */, 165 | 74C1F3E6228BEDE8000B6AF2 /* CardCollectionViewCell.swift in Sources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXSourcesBuildPhase section */ 170 | 171 | /* Begin XCBuildConfiguration section */ 172 | 74C1F3D9228BECEB000B6AF2 /* Debug */ = { 173 | isa = XCBuildConfiguration; 174 | buildSettings = { 175 | ALWAYS_SEARCH_USER_PATHS = NO; 176 | CLANG_ANALYZER_NONNULL = YES; 177 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 179 | CLANG_CXX_LIBRARY = "libc++"; 180 | CLANG_ENABLE_MODULES = YES; 181 | CLANG_ENABLE_OBJC_ARC = YES; 182 | CLANG_ENABLE_OBJC_WEAK = YES; 183 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 184 | CLANG_WARN_BOOL_CONVERSION = YES; 185 | CLANG_WARN_COMMA = YES; 186 | CLANG_WARN_CONSTANT_CONVERSION = YES; 187 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_INFINITE_RECURSION = YES; 193 | CLANG_WARN_INT_CONVERSION = YES; 194 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 195 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 196 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 197 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 198 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 199 | CLANG_WARN_STRICT_PROTOTYPES = YES; 200 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 201 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 202 | CLANG_WARN_UNREACHABLE_CODE = YES; 203 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 204 | CODE_SIGN_IDENTITY = "iPhone Developer"; 205 | COPY_PHASE_STRIP = NO; 206 | CURRENT_PROJECT_VERSION = 1; 207 | DEBUG_INFORMATION_FORMAT = dwarf; 208 | ENABLE_STRICT_OBJC_MSGSEND = YES; 209 | ENABLE_TESTABILITY = YES; 210 | GCC_C_LANGUAGE_STANDARD = gnu11; 211 | GCC_DYNAMIC_NO_PIC = NO; 212 | GCC_NO_COMMON_BLOCKS = YES; 213 | GCC_OPTIMIZATION_LEVEL = 0; 214 | GCC_PREPROCESSOR_DEFINITIONS = ( 215 | "DEBUG=1", 216 | "$(inherited)", 217 | ); 218 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 219 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 220 | GCC_WARN_UNDECLARED_SELECTOR = YES; 221 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 222 | GCC_WARN_UNUSED_FUNCTION = YES; 223 | GCC_WARN_UNUSED_VARIABLE = YES; 224 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 225 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 226 | MTL_FAST_MATH = YES; 227 | ONLY_ACTIVE_ARCH = YES; 228 | SDKROOT = iphoneos; 229 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 230 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 231 | VERSIONING_SYSTEM = "apple-generic"; 232 | VERSION_INFO_PREFIX = ""; 233 | }; 234 | name = Debug; 235 | }; 236 | 74C1F3DA228BECEB000B6AF2 /* Release */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_ANALYZER_NONNULL = YES; 241 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 243 | CLANG_CXX_LIBRARY = "libc++"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_ENABLE_OBJC_WEAK = YES; 247 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 248 | CLANG_WARN_BOOL_CONVERSION = YES; 249 | CLANG_WARN_COMMA = YES; 250 | CLANG_WARN_CONSTANT_CONVERSION = YES; 251 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 252 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 253 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 254 | CLANG_WARN_EMPTY_BODY = YES; 255 | CLANG_WARN_ENUM_CONVERSION = YES; 256 | CLANG_WARN_INFINITE_RECURSION = YES; 257 | CLANG_WARN_INT_CONVERSION = YES; 258 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 260 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 262 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 263 | CLANG_WARN_STRICT_PROTOTYPES = YES; 264 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 265 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 266 | CLANG_WARN_UNREACHABLE_CODE = YES; 267 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 268 | CODE_SIGN_IDENTITY = "iPhone Developer"; 269 | COPY_PHASE_STRIP = NO; 270 | CURRENT_PROJECT_VERSION = 1; 271 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 272 | ENABLE_NS_ASSERTIONS = NO; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | GCC_C_LANGUAGE_STANDARD = gnu11; 275 | GCC_NO_COMMON_BLOCKS = YES; 276 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 277 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 278 | GCC_WARN_UNDECLARED_SELECTOR = YES; 279 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 280 | GCC_WARN_UNUSED_FUNCTION = YES; 281 | GCC_WARN_UNUSED_VARIABLE = YES; 282 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 283 | MTL_ENABLE_DEBUG_INFO = NO; 284 | MTL_FAST_MATH = YES; 285 | SDKROOT = iphoneos; 286 | SWIFT_COMPILATION_MODE = wholemodule; 287 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 288 | VALIDATE_PRODUCT = YES; 289 | VERSIONING_SYSTEM = "apple-generic"; 290 | VERSION_INFO_PREFIX = ""; 291 | }; 292 | name = Release; 293 | }; 294 | 74C1F3DC228BECEB000B6AF2 /* Debug */ = { 295 | isa = XCBuildConfiguration; 296 | buildSettings = { 297 | CLANG_ENABLE_MODULES = YES; 298 | CODE_SIGN_IDENTITY = ""; 299 | CODE_SIGN_STYLE = Automatic; 300 | DEFINES_MODULE = YES; 301 | DEVELOPMENT_TEAM = WZYAY65GLB; 302 | DYLIB_COMPATIBILITY_VERSION = 1; 303 | DYLIB_CURRENT_VERSION = 1; 304 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 305 | INFOPLIST_FILE = AppstoreTransition/Info.plist; 306 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 307 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 308 | LD_RUNPATH_SEARCH_PATHS = ( 309 | "$(inherited)", 310 | "@executable_path/Frameworks", 311 | "@loader_path/Frameworks", 312 | ); 313 | PRODUCT_BUNDLE_IDENTIFIER = com.appssemble.AppstoreTransition; 314 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 315 | SKIP_INSTALL = YES; 316 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 317 | SWIFT_VERSION = 5.0; 318 | TARGETED_DEVICE_FAMILY = "1,2"; 319 | }; 320 | name = Debug; 321 | }; 322 | 74C1F3DD228BECEB000B6AF2 /* Release */ = { 323 | isa = XCBuildConfiguration; 324 | buildSettings = { 325 | CLANG_ENABLE_MODULES = YES; 326 | CODE_SIGN_IDENTITY = ""; 327 | CODE_SIGN_STYLE = Automatic; 328 | DEFINES_MODULE = YES; 329 | DEVELOPMENT_TEAM = WZYAY65GLB; 330 | DYLIB_COMPATIBILITY_VERSION = 1; 331 | DYLIB_CURRENT_VERSION = 1; 332 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 333 | INFOPLIST_FILE = AppstoreTransition/Info.plist; 334 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 335 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 336 | LD_RUNPATH_SEARCH_PATHS = ( 337 | "$(inherited)", 338 | "@executable_path/Frameworks", 339 | "@loader_path/Frameworks", 340 | ); 341 | PRODUCT_BUNDLE_IDENTIFIER = com.appssemble.AppstoreTransition; 342 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 343 | SKIP_INSTALL = YES; 344 | SWIFT_VERSION = 5.0; 345 | TARGETED_DEVICE_FAMILY = "1,2"; 346 | }; 347 | name = Release; 348 | }; 349 | /* End XCBuildConfiguration section */ 350 | 351 | /* Begin XCConfigurationList section */ 352 | 74C1F3CD228BECEB000B6AF2 /* Build configuration list for PBXProject "AppstoreTransition" */ = { 353 | isa = XCConfigurationList; 354 | buildConfigurations = ( 355 | 74C1F3D9228BECEB000B6AF2 /* Debug */, 356 | 74C1F3DA228BECEB000B6AF2 /* Release */, 357 | ); 358 | defaultConfigurationIsVisible = 0; 359 | defaultConfigurationName = Release; 360 | }; 361 | 74C1F3DB228BECEB000B6AF2 /* Build configuration list for PBXNativeTarget "AppstoreTransition" */ = { 362 | isa = XCConfigurationList; 363 | buildConfigurations = ( 364 | 74C1F3DC228BECEB000B6AF2 /* Debug */, 365 | 74C1F3DD228BECEB000B6AF2 /* Release */, 366 | ); 367 | defaultConfigurationIsVisible = 0; 368 | defaultConfigurationName = Release; 369 | }; 370 | /* End XCConfigurationList section */ 371 | }; 372 | rootObject = 74C1F3CA228BECEB000B6AF2 /* Project object */; 373 | } 374 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/AppstoreTransition.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppstoreTransition.h 3 | // AppstoreTransition 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for AppstoreTransition. 12 | FOUNDATION_EXPORT double AppstoreTransitionVersionNumber; 13 | 14 | //! Project version string for AppstoreTransition. 15 | FOUNDATION_EXPORT const unsigned char AppstoreTransitionVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/CardCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardCollectionViewCell.swift 3 | // Kickster 4 | // 5 | // Created by Razvan Chelemen on 08/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private struct AssociatedKeys { 12 | static var settingsKey: UInt8 = 0 13 | static var disabledHighlightedAnimationKey: UInt8 = 0 14 | } 15 | 16 | public protocol CardCollectionViewCell: UIView { 17 | var cardContentView: UIView { get } 18 | var disabledHighlightedAnimation: Bool { get set } 19 | var settings: TransitionSettings { get } 20 | 21 | func resetTransform() 22 | func freezeAnimations() 23 | func unfreezeAnimations() 24 | 25 | } 26 | 27 | public extension CardCollectionViewCell { 28 | 29 | var disabledHighlightedAnimation:Bool { 30 | get { 31 | if let disabledHighlight = objc_getAssociatedObject(self, &AssociatedKeys.disabledHighlightedAnimationKey) as? Bool { 32 | return disabledHighlight 33 | } else { 34 | self.disabledHighlightedAnimation = false 35 | return disabledHighlightedAnimation 36 | } 37 | } 38 | set { 39 | objc_setAssociatedObject(self, &AssociatedKeys.disabledHighlightedAnimationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 40 | } 41 | } 42 | 43 | var settings:TransitionSettings { 44 | get { 45 | if let settings = objc_getAssociatedObject(self, &AssociatedKeys.settingsKey) as? TransitionSettings { 46 | return settings 47 | } else { 48 | self.settings = TransitionSettings() 49 | return settings 50 | } 51 | } 52 | set { 53 | objc_setAssociatedObject(self, &AssociatedKeys.settingsKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 54 | } 55 | } 56 | 57 | func resetTransform() { 58 | transform = .identity 59 | } 60 | 61 | func freezeAnimations() { 62 | disabledHighlightedAnimation = true 63 | layer.removeAllAnimations() 64 | } 65 | 66 | func unfreezeAnimations() { 67 | disabledHighlightedAnimation = false 68 | } 69 | 70 | func animate(isHighlighted: Bool, completion: ((Bool) -> Void)?=nil) { 71 | if disabledHighlightedAnimation { 72 | return 73 | } 74 | let animationOptions: UIView.AnimationOptions = settings.isEnabledAllowsUserInteractionWhileHighlightingCard 75 | ? [.allowUserInteraction] : [] 76 | if isHighlighted { 77 | UIView.animate(withDuration: 0.5, 78 | delay: 0, 79 | usingSpringWithDamping: 1, 80 | initialSpringVelocity: 0, 81 | options: animationOptions, animations: { 82 | self.transform = .init(scaleX: self.settings.cardHighlightedFactor, y: self.settings.cardHighlightedFactor) 83 | }, completion: completion) 84 | } else { 85 | UIView.animate(withDuration: 0.5, 86 | delay: 0, 87 | usingSpringWithDamping: 1, 88 | initialSpringVelocity: 0, 89 | options: animationOptions, animations: { 90 | self.transform = .identity 91 | }, completion: completion) 92 | } 93 | } 94 | 95 | } 96 | 97 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/CardDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardDetailViewController.swift 3 | // Kickster 4 | // 5 | // Created by Razvan Chelemen on 08/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private struct AssociatedKeys { 12 | static var settingsKey: UInt8 = 0 13 | static var dismissHandlerKey: UInt8 = 0 14 | } 15 | 16 | public protocol CardsViewController { 17 | 18 | } 19 | 20 | public protocol CardDetailViewController: UIViewController { 21 | var cardContentView: UIView { get } 22 | var scrollView: UIScrollView? { get } 23 | var settings: TransitionSettings { get set } 24 | var dismissHandler: CardDismissHandler { get } 25 | 26 | func didStartPresentAnimationProgress() 27 | func didFinishPresentAnimationProgress() 28 | 29 | func didBeginDismissAnimation() 30 | func didChangeDismissAnimationProgress(progress:CGFloat) 31 | func didStartDismissAnimation() 32 | func didFinishDismissAnimation() 33 | func didCancelDismissAnimation(progress:CGFloat) 34 | } 35 | 36 | public extension CardDetailViewController { 37 | 38 | var dismissHandler:CardDismissHandler { 39 | get { 40 | if let settings = objc_getAssociatedObject(self, &AssociatedKeys.dismissHandlerKey) as? CardDismissHandler { 41 | return settings 42 | } else { 43 | self.dismissHandler = CardDismissHandler(source: self) 44 | return dismissHandler 45 | } 46 | } 47 | set { 48 | objc_setAssociatedObject(self, &AssociatedKeys.dismissHandlerKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 49 | } 50 | } 51 | 52 | var settings:TransitionSettings { 53 | get { 54 | if let settings = objc_getAssociatedObject(self, &AssociatedKeys.settingsKey) as? TransitionSettings { 55 | return settings 56 | } else { 57 | self.settings = TransitionSettings() 58 | return settings 59 | } 60 | } 61 | set { 62 | objc_setAssociatedObject(self, &AssociatedKeys.settingsKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 63 | } 64 | } 65 | 66 | var dismissEnabled: Bool { 67 | get { 68 | return dismissHandler.dismissalPanGesture.isEnabled 69 | } 70 | 71 | set { 72 | dismissHandler.dismissalPanGesture.isEnabled = newValue 73 | dismissHandler.dismissalScreenEdgePanGesture.isEnabled = newValue 74 | } 75 | } 76 | 77 | func didStartPresentAnimationProgress() {} 78 | func didFinishPresentAnimationProgress() {} 79 | 80 | func didBeginDismissAnimation() {} 81 | func didChangeDismissAnimationProgress(progress:CGFloat) {} 82 | func didStartDismissAnimation() {} 83 | func didFinishDismissAnimation() {} 84 | func didCancelDismissAnimation(progress:CGFloat) {} 85 | 86 | } 87 | 88 | public final class CardDismissHandler: NSObject { 89 | 90 | public final class DismissalPanGesture: UIPanGestureRecognizer {} 91 | public final class DismissalScreenEdgePanGesture: UIScreenEdgePanGestureRecognizer {} 92 | 93 | public lazy var dismissalPanGesture: DismissalPanGesture = { 94 | let pan = DismissalPanGesture() 95 | pan.maximumNumberOfTouches = 1 96 | return pan 97 | }() 98 | 99 | public lazy var dismissalScreenEdgePanGesture: DismissalScreenEdgePanGesture = { 100 | let pan = DismissalScreenEdgePanGesture() 101 | pan.edges = .left 102 | return pan 103 | }() 104 | 105 | var interactiveStartingPoint: CGPoint? 106 | var dismissalAnimator: UIViewPropertyAnimator? 107 | var draggingDownToDismiss = false 108 | 109 | private let source: CardDetailViewController 110 | 111 | init(source: CardDetailViewController) { 112 | // We require source object in case we need access some properties etc. 113 | self.source = source 114 | 115 | super.init() 116 | 117 | dismissalPanGesture.addTarget(self, action: #selector(handleDismissalPan(gesture:))) 118 | dismissalPanGesture.delegate = self 119 | 120 | dismissalScreenEdgePanGesture.addTarget(self, action: #selector(handleDismissalPan(gesture:))) 121 | dismissalScreenEdgePanGesture.delegate = self 122 | 123 | // Make drag down/scroll pan gesture waits til screen edge pan to fail first to begin 124 | dismissalPanGesture.require(toFail: dismissalScreenEdgePanGesture) 125 | source.scrollView?.panGestureRecognizer.require(toFail: dismissalScreenEdgePanGesture) 126 | 127 | source.loadViewIfNeeded() 128 | source.view.addGestureRecognizer(dismissalPanGesture) 129 | source.view.addGestureRecognizer(dismissalScreenEdgePanGesture) 130 | 131 | checkScrolling(scrollView: source.scrollView) 132 | } 133 | 134 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 135 | checkScrolling(scrollView: scrollView) 136 | } 137 | 138 | private var dismissTop = true 139 | private var lastContentOffset: CGFloat = 0 140 | 141 | // This handles both screen edge and dragdown pan. As screen edge pan is a subclass of pan gesture, this input param works. 142 | @objc func handleDismissalPan(gesture: UIPanGestureRecognizer) { 143 | 144 | // let velocity = gesture.velocity(in: source.view) 145 | //if velocity.y > 0 { return } 146 | 147 | let isScreenEdgePan = gesture.isKind(of: DismissalScreenEdgePanGesture.self) 148 | let canStartDragDownToDismissPan = !isScreenEdgePan && !draggingDownToDismiss 149 | 150 | // Don't do anything when it's not in the drag down mode 151 | if canStartDragDownToDismissPan { return } 152 | 153 | let targetAnimatedView = gesture.view! 154 | let startingPoint: CGPoint 155 | 156 | if let p = interactiveStartingPoint { 157 | startingPoint = p 158 | } else { 159 | // Initial location 160 | startingPoint = gesture.location(in: nil) 161 | interactiveStartingPoint = startingPoint 162 | } 163 | 164 | let currentLocation = gesture.location(in: nil) 165 | 166 | let progress: CGFloat 167 | if (dismissTop) { 168 | progress = isScreenEdgePan ? (gesture.translation(in: targetAnimatedView).x / 100) : (currentLocation.y - startingPoint.y) / 100 169 | } else { 170 | progress = isScreenEdgePan ? (gesture.translation(in: targetAnimatedView).x / 100) : (startingPoint.y - currentLocation.y) / 100 171 | } 172 | 173 | let targetShrinkScale: CGFloat = 0.86 174 | let targetCornerRadius: CGFloat = source.settings.cardCornerRadius 175 | 176 | func createInteractiveDismissalAnimatorIfNeeded() -> UIViewPropertyAnimator { 177 | if let animator = dismissalAnimator { 178 | return animator 179 | } else { 180 | let animator = UIViewPropertyAnimator(duration: 0, curve: .linear, animations: { 181 | targetAnimatedView.transform = .init(scaleX: targetShrinkScale, y: targetShrinkScale) 182 | targetAnimatedView.layer.cornerRadius = targetCornerRadius 183 | }) 184 | animator.isReversed = false 185 | animator.pauseAnimation() 186 | animator.fractionComplete = progress 187 | return animator 188 | } 189 | } 190 | 191 | switch gesture.state { 192 | case .began: 193 | 194 | if let scrollView = source.scrollView { 195 | if (scrollView.contentOffset.y <= 0) { 196 | dismissTop = true 197 | } else if (scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.height && source.settings.isEnabledBottomClose) { 198 | dismissTop = false 199 | } 200 | } else { 201 | dismissTop = true 202 | } 203 | 204 | dismissalAnimator = createInteractiveDismissalAnimatorIfNeeded() 205 | source.didBeginDismissAnimation() 206 | case .changed: 207 | dismissalAnimator = createInteractiveDismissalAnimatorIfNeeded() 208 | 209 | let actualProgress = progress 210 | let isDismissalSuccess = actualProgress >= 1.0 211 | 212 | dismissalAnimator!.fractionComplete = actualProgress 213 | if progress >= 0 && progress <= 1 && dismissTop { 214 | source.scrollView?.contentOffset = CGPoint(x: 0, y: 100 * max(progress, 0)) 215 | } 216 | source.didChangeDismissAnimationProgress(progress: progress) 217 | 218 | if isDismissalSuccess { 219 | dismissalAnimator!.stopAnimation(false) 220 | dismissalAnimator!.addCompletion { [unowned self] (pos) in 221 | switch pos { 222 | case .end: 223 | self.didSuccessfullyDragDownToDismiss() 224 | default: 225 | fatalError("Must finish dismissal at end!") 226 | } 227 | } 228 | dismissalAnimator!.finishAnimation(at: .end) 229 | } 230 | 231 | case .ended, .cancelled: 232 | if dismissalAnimator == nil { 233 | // Gesture's too quick that it doesn't have dismissalAnimator! 234 | print("Too quick there's no animator!") 235 | didCancelDismissalTransition() 236 | return 237 | } 238 | // NOTE: 239 | // If user lift fingers -> ended 240 | // If gesture.isEnabled -> cancelled 241 | 242 | // Ended, Animate back to start 243 | dismissalAnimator!.pauseAnimation() 244 | dismissalAnimator!.isReversed = true 245 | 246 | source.didCancelDismissAnimation(progress: progress) 247 | // Disable gesture until reverse closing animation finishes. 248 | gesture.isEnabled = false 249 | dismissalAnimator!.addCompletion { [unowned self] (pos) in 250 | self.didCancelDismissalTransition() 251 | gesture.isEnabled = true 252 | 253 | //if (!self.dismissTop && self.lastContentOffset < self.source.scrollView.contentOffset.y) { 254 | //self.source.scrollView.setContentOffset(CGPoint(x: 0, y: self.source.scrollView.contentSize.height - self.source.scrollView.bounds.size.height + self.source.scrollView.contentInset.bottom), animated: true) 255 | //} 256 | } 257 | dismissalAnimator!.startAnimation() 258 | default: 259 | fatalError("Impossible gesture state? \(gesture.state.rawValue)") 260 | } 261 | 262 | do { 263 | self.lastContentOffset = source.scrollView?.contentOffset.y ?? 0 264 | } 265 | 266 | source.scrollView?.bounces = (source.scrollView?.contentOffset.y ?? 0) > 100 267 | } 268 | 269 | func didSuccessfullyDragDownToDismiss() { 270 | //cardViewModel = unhighlightedCardViewModel 271 | //source.dismiss(animated: true) 272 | self.source.didStartDismissAnimation() 273 | source.dismiss(animated: true) { 274 | self.source.didFinishDismissAnimation() 275 | } 276 | } 277 | 278 | func didCancelDismissalTransition() { 279 | // Clean up 280 | interactiveStartingPoint = nil 281 | dismissalAnimator = nil 282 | draggingDownToDismiss = false 283 | } 284 | 285 | private func checkScrolling(scrollView: UIScrollView?) { 286 | if (shouldDismiss()) { 287 | draggingDownToDismiss = true 288 | } 289 | 290 | scrollView?.showsVerticalScrollIndicator = !draggingDownToDismiss 291 | } 292 | 293 | func shouldDismiss() -> Bool { 294 | guard let scrollView = source.scrollView else { return true } 295 | 296 | if (source.settings.isEnabledBottomClose) { 297 | return scrollView.contentOffset.y <= 0 || scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.height 298 | } else { 299 | return scrollView.contentOffset.y <= 0 300 | } 301 | } 302 | 303 | } 304 | 305 | extension CardDismissHandler: UIGestureRecognizerDelegate { 306 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 307 | checkScrolling(scrollView: source.scrollView) 308 | return shouldDismiss() 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/CardPresentationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardPresentationController.swift 3 | // Kickster 4 | // 5 | // Created by Razvan Chelemen on 06/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class CardPresentationController: UIPresentationController { 12 | 13 | private lazy var blurView = UIVisualEffectView(effect: nil) 14 | 15 | var settings: TransitionSettings? 16 | 17 | // Default is false. 18 | // And also means you can access only `.to` when present, and `.from` when dismiss (e.g., can touch only 'presented view'). 19 | // 20 | // If true, the presenting view is removed and you have to add it during animation accessing `.from` key. 21 | // And you will have access to both `.to` and `.from` view. (In the typical .fullScreen mode) 22 | override var shouldRemovePresentersView: Bool { 23 | return false 24 | } 25 | 26 | override func presentationTransitionWillBegin() { 27 | let container = containerView! 28 | blurView.translatesAutoresizingMaskIntoConstraints = false 29 | container.addSubview(blurView) 30 | blurView.edges(to: container) 31 | blurView.alpha = 0.0 32 | if let settings = settings { 33 | blurView.backgroundColor = settings.blurColor 34 | if settings.blurEnabled { 35 | blurView.effect = UIBlurEffect(style: .light) 36 | } 37 | } 38 | 39 | presentingViewController.beginAppearanceTransition(false, animated: false) 40 | presentedViewController.transitionCoordinator!.animate(alongsideTransition: { (ctx) in 41 | UIView.animate(withDuration: 0.5, animations: { 42 | self.blurView.alpha = self.settings?.blurAlpha ?? 1.0 43 | }) 44 | }) { (ctx) in } 45 | } 46 | 47 | override func presentationTransitionDidEnd(_ completed: Bool) { 48 | presentingViewController.endAppearanceTransition() 49 | } 50 | 51 | override func dismissalTransitionWillBegin() { 52 | presentingViewController.beginAppearanceTransition(true, animated: true) 53 | presentedViewController.transitionCoordinator!.animate(alongsideTransition: { (ctx) in 54 | self.blurView.alpha = 0.0 55 | }, completion: nil) 56 | } 57 | 58 | override func dismissalTransitionDidEnd(_ completed: Bool) { 59 | presentingViewController.endAppearanceTransition() 60 | if completed { 61 | blurView.removeFromSuperview() 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/CardTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardTransition.swift 3 | // Kickster 4 | // 5 | // Created by Razvan Chelemen on 06/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class TransitionSettings { 12 | public var cardHighlightedFactor: CGFloat = 0.96 13 | public var cardCornerRadius: CGFloat = 8 14 | public var dismissalAnimationDuration = 0.6 15 | public var dismissalScrollViewContentOffset = CGPoint.zero 16 | public var blurEnabled = true 17 | public var blurColor = UIColor.clear 18 | public var blurAlpha: CGFloat = 1.0 19 | 20 | public var cardVerticalExpandingStyle: CardVerticalExpandingStyle = .fromTop 21 | 22 | /// Without this, there'll be weird offset (probably from scrollView) that obscures the card content view of the cardDetailView. 23 | public var isEnabledWeirdTopInsetsFix = true 24 | 25 | /// Swipe from bottom should also closes the detail screen. 26 | public var isEnabledBottomClose = false 27 | 28 | /// If true, will draw borders on animating views. 29 | public var isEnabledDebugAnimatingViews = false 30 | 31 | /// If true, this will add a 'reverse' additional top safe area insets to make the final top safe area insets zero. 32 | public var isEnabledTopSafeAreaInsetsFixOnCardDetailViewController = true 33 | 34 | /// If true, will always allow user to scroll while it's animated. 35 | public var isEnabledAllowsUserInteractionWhileHighlightingCard = true 36 | 37 | public var cardContainerInsets: UIEdgeInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) 38 | 39 | public enum CardVerticalExpandingStyle { 40 | /// Expanding card pinning at the top of animatingContainerView 41 | case fromTop 42 | 43 | /// Expanding card pinning at the center of animatingContainerView 44 | case fromCenter 45 | } 46 | 47 | public init() { 48 | } 49 | 50 | } 51 | 52 | public final class CardTransition: NSObject, UIViewControllerTransitioningDelegate { 53 | 54 | struct Params { 55 | let fromCardFrame: CGRect 56 | let fromCardFrameWithoutTransform: CGRect 57 | let fromCell: CardCollectionViewCell 58 | let settings: TransitionSettings 59 | } 60 | 61 | let cell: CardCollectionViewCell 62 | let settings: TransitionSettings 63 | 64 | public var updatedCardFrame: (()->(CGRect))? 65 | 66 | public init(cell: CardCollectionViewCell, settings: TransitionSettings = TransitionSettings()) { 67 | // Freeze highlighted state (or else it will bounce back) 68 | cell.freezeAnimations() 69 | 70 | self.cell = cell 71 | self.settings = settings 72 | 73 | super.init() 74 | } 75 | 76 | private func params() -> Params { 77 | 78 | // Get current frame on screen 79 | let currentCellFrame = cell.layer.presentation()!.frame 80 | 81 | // Convert current frame to screen's coordinates 82 | let cardPresentationFrameOnScreen = cell.superview!.convert(currentCellFrame, to: nil) 83 | 84 | // Get card frame without transform in screen's coordinates (for the dismissing back later to original location) 85 | let cardFrameWithoutTransform = { () -> CGRect in 86 | let center = cell.center 87 | let size = cell.bounds.size 88 | let r = CGRect( 89 | x: center.x - size.width / 2, 90 | y: center.y - size.height / 2, 91 | width: size.width, 92 | height: size.height 93 | ) 94 | return cell.superview!.convert(r, to: nil) 95 | }() 96 | 97 | let params = CardTransition.Params(fromCardFrame: cardPresentationFrameOnScreen, 98 | fromCardFrameWithoutTransform: cardFrameWithoutTransform, 99 | fromCell: cell, 100 | settings: settings) 101 | 102 | return params 103 | } 104 | 105 | public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 106 | let baseParams = params() 107 | 108 | let params = PresentCardAnimator.Params.init( 109 | fromCardFrame: baseParams.fromCardFrame, 110 | fromCell: baseParams.fromCell, 111 | settings: baseParams.settings 112 | ) 113 | return PresentCardAnimator(params: params) 114 | } 115 | 116 | public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 117 | let baseParams = params() 118 | 119 | let params = DismissCardAnimator.Params.init( 120 | fromCardFrame: baseParams.fromCardFrame, 121 | fromCardFrameWithoutTransform: baseParams.fromCardFrameWithoutTransform, 122 | fromCell: baseParams.fromCell, 123 | settings: baseParams.settings 124 | ) 125 | return DismissCardAnimator(params: params) 126 | } 127 | 128 | public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 129 | return nil 130 | } 131 | 132 | public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 133 | return nil 134 | } 135 | 136 | // IMPORTANT: Must set modalPresentationStyle to `.custom` for this to be used. 137 | public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { 138 | let cardPresentationController = CardPresentationController(presentedViewController: presented, presenting: presenting) 139 | cardPresentationController.settings = settings 140 | 141 | return cardPresentationController 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/DismissCardAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DismissCardAnimator.swift 3 | // Kickster 4 | // 5 | // Created by Razvan Chelemen on 06/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class DismissCardAnimator: NSObject, UIViewControllerAnimatedTransitioning { 12 | 13 | struct Params { 14 | let fromCardFrame: CGRect 15 | let fromCardFrameWithoutTransform: CGRect 16 | let fromCell: CardCollectionViewCell 17 | let settings: TransitionSettings 18 | } 19 | 20 | struct Constants { 21 | static let relativeDurationBeforeNonInteractive: TimeInterval = 0.5 22 | static let minimumScaleBeforeNonInteractive: CGFloat = 0.8 23 | } 24 | 25 | private let params: Params 26 | 27 | init(params: Params) { 28 | self.params = params 29 | super.init() 30 | } 31 | 32 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 33 | return params.settings.dismissalAnimationDuration 34 | } 35 | 36 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 37 | let ctx = transitionContext 38 | let container = ctx.containerView 39 | 40 | let toViewController: CardsViewController! = ctx.viewController(forKey: .to)?.cardsViewController() 41 | 42 | let screens: (cardDetail: CardDetailViewController, home: CardsViewController) = ( 43 | ctx.viewController(forKey: .from)! as! CardDetailViewController, 44 | toViewController 45 | ) 46 | 47 | let cardDetailView = ctx.view(forKey: .from)! 48 | 49 | let animatedContainerView = UIView() 50 | if params.settings.isEnabledDebugAnimatingViews { 51 | animatedContainerView.layer.borderColor = UIColor.yellow.cgColor 52 | animatedContainerView.layer.borderWidth = 4 53 | cardDetailView.layer.borderColor = UIColor.red.cgColor 54 | cardDetailView.layer.borderWidth = 2 55 | } 56 | animatedContainerView.translatesAutoresizingMaskIntoConstraints = false 57 | cardDetailView.translatesAutoresizingMaskIntoConstraints = false 58 | 59 | container.removeConstraints(container.constraints) 60 | 61 | container.addSubview(animatedContainerView) 62 | animatedContainerView.addSubview(cardDetailView) 63 | 64 | // Card fills inside animated container view 65 | cardDetailView.edges(to: animatedContainerView) 66 | 67 | animatedContainerView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true 68 | let animatedContainerTopConstraint = animatedContainerView.topAnchor.constraint(equalTo: container.topAnchor, constant: params.settings.cardContainerInsets.top) 69 | let animatedContainerWidthConstraint = animatedContainerView.widthAnchor.constraint(equalToConstant: cardDetailView.frame.width - (params.settings.cardContainerInsets.left + params.settings.cardContainerInsets.right)) 70 | let animatedContainerHeightConstraint = animatedContainerView.heightAnchor.constraint(equalToConstant: cardDetailView.frame.height - (params.settings.cardContainerInsets.top + params.settings.cardContainerInsets.bottom)) 71 | 72 | NSLayoutConstraint.activate([animatedContainerTopConstraint, animatedContainerWidthConstraint, animatedContainerHeightConstraint]) 73 | 74 | // Fix weird top inset 75 | let topTemporaryFix = screens.cardDetail.cardContentView.topAnchor.constraint(equalTo: cardDetailView.topAnchor) 76 | topTemporaryFix.isActive = params.settings.isEnabledWeirdTopInsetsFix 77 | 78 | container.layoutIfNeeded() 79 | 80 | // Force card filling bottom 81 | let stretchCardToFillBottom = screens.cardDetail.cardContentView.bottomAnchor.constraint(equalTo: cardDetailView.bottomAnchor) 82 | // for tableview header required confilcts with autoresizing mask constraints 83 | stretchCardToFillBottom.priority = .defaultHigh 84 | 85 | func animateCardViewBackToPlace() { 86 | stretchCardToFillBottom.isActive = true 87 | //screens.cardDetail.isFontStateHighlighted = false 88 | // Back to identity 89 | // NOTE: Animated container view in a way, helps us to not messing up `transform` with `AutoLayout` animation. 90 | cardDetailView.transform = CGAffineTransform.identity 91 | animatedContainerTopConstraint.constant = self.params.fromCardFrameWithoutTransform.minY + params.settings.cardContainerInsets.top 92 | animatedContainerWidthConstraint.constant = self.params.fromCardFrameWithoutTransform.width - (params.settings.cardContainerInsets.left + params.settings.cardContainerInsets.right) 93 | animatedContainerHeightConstraint.constant = self.params.fromCardFrameWithoutTransform.height - (params.settings.cardContainerInsets.top + params.settings.cardContainerInsets.bottom) 94 | container.layoutIfNeeded() 95 | } 96 | 97 | func completeEverything() { 98 | let success = !ctx.transitionWasCancelled 99 | animatedContainerView.removeConstraints(animatedContainerView.constraints) 100 | animatedContainerView.removeFromSuperview() 101 | if success { 102 | cardDetailView.removeFromSuperview() 103 | self.params.fromCell.isHidden = false 104 | } else { 105 | //screens.cardDetail.isFontStateHighlighted = true 106 | 107 | // Remove temporary fixes if not success! 108 | topTemporaryFix.isActive = false 109 | stretchCardToFillBottom.isActive = false 110 | 111 | cardDetailView.removeConstraint(topTemporaryFix) 112 | cardDetailView.removeConstraint(stretchCardToFillBottom) 113 | 114 | container.removeConstraints(container.constraints) 115 | 116 | container.addSubview(cardDetailView) 117 | cardDetailView.edges(to: container) 118 | } 119 | ctx.completeTransition(success) 120 | } 121 | 122 | UIView.animate(withDuration: transitionDuration(using: ctx), delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.0, options: [], animations: { 123 | animateCardViewBackToPlace() 124 | }) { (finished) in 125 | completeEverything() 126 | } 127 | 128 | UIView.animate(withDuration: transitionDuration(using: ctx) * 0.4) { 129 | //print("godam") 130 | //screens.cardDetail.scrollView.setContentOffset(self.params.settings.dismissalScrollViewContentOffset, animated: true) 131 | screens.cardDetail.scrollView?.contentOffset = self.params.settings.dismissalScrollViewContentOffset 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/PresentCardAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentCardAnimator.swift 3 | // Kickster 4 | // 5 | // Created by Razvan Chelemen on 06/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | 13 | func cardsViewController() -> CardsViewController? { 14 | if let viewController = self as? CardsViewController { 15 | return viewController 16 | } else if let viewController = self as? UITabBarController { 17 | return viewController.selectedViewController?.cardsViewController() 18 | } else if let viewController = self as? UINavigationController { 19 | return viewController.topViewController?.cardsViewController() 20 | } 21 | 22 | return nil 23 | } 24 | 25 | } 26 | 27 | final class PresentCardAnimator: NSObject, UIViewControllerAnimatedTransitioning { 28 | 29 | private let params: Params 30 | 31 | struct Params { 32 | let fromCardFrame: CGRect 33 | let fromCell: CardCollectionViewCell 34 | let settings: TransitionSettings 35 | } 36 | 37 | private let presentAnimationDuration: TimeInterval 38 | private let springAnimator: UIViewPropertyAnimator 39 | private var transitionDriver: PresentCardTransitionDriver? 40 | 41 | init(params: Params) { 42 | self.params = params 43 | self.springAnimator = PresentCardAnimator.createBaseSpringAnimator(params: params) 44 | self.presentAnimationDuration = springAnimator.duration 45 | super.init() 46 | } 47 | 48 | private static func createBaseSpringAnimator(params: PresentCardAnimator.Params) -> UIViewPropertyAnimator { 49 | // Damping between 0.7 (far away) and 1.0 (nearer) 50 | let cardPositionY = params.fromCardFrame.minY 51 | let distanceToBounce = abs(params.fromCardFrame.minY) 52 | let extentToBounce = cardPositionY < 0 ? params.fromCardFrame.height : UIScreen.main.bounds.height 53 | let dampFactorInterval: CGFloat = 0.3 54 | let damping: CGFloat = 1.0 - dampFactorInterval * (distanceToBounce / extentToBounce) 55 | 56 | // Duration between 0.5 (nearer) and 0.9 (nearer) 57 | let baselineDuration: TimeInterval = 0.5 58 | let maxDuration: TimeInterval = 0.9 59 | let duration: TimeInterval = baselineDuration + (maxDuration - baselineDuration) * TimeInterval(max(0, distanceToBounce)/UIScreen.main.bounds.height) 60 | 61 | let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: .init(dx: 0, dy: 0)) 62 | return UIViewPropertyAnimator(duration: duration, timingParameters: springTiming) 63 | } 64 | 65 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 66 | // 1. 67 | return presentAnimationDuration 68 | } 69 | 70 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 71 | // 2. 72 | transitionDriver = PresentCardTransitionDriver(params: params, 73 | transitionContext: transitionContext, 74 | baseAnimator: springAnimator) 75 | interruptibleAnimator(using: transitionContext).startAnimation() 76 | } 77 | 78 | func animationEnded(_ transitionCompleted: Bool) { 79 | // 4. 80 | transitionDriver = nil 81 | } 82 | 83 | func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { 84 | // 3. 85 | return transitionDriver!.animator 86 | } 87 | } 88 | 89 | final class PresentCardTransitionDriver { 90 | let animator: UIViewPropertyAnimator 91 | init(params: PresentCardAnimator.Params, transitionContext: UIViewControllerContextTransitioning, baseAnimator: UIViewPropertyAnimator) { 92 | let ctx = transitionContext 93 | let container = ctx.containerView 94 | let fromViewController: CardsViewController! = ctx.viewController(forKey: .from)?.cardsViewController() 95 | 96 | let screens: (home: CardsViewController, cardDetail: CardDetailViewController) = ( 97 | fromViewController, 98 | ctx.viewController(forKey: .to)! as! CardDetailViewController 99 | ) 100 | 101 | let cardDetailView = ctx.view(forKey: .to)! 102 | cardDetailView.backgroundColor = .clear 103 | let fromCardFrame = params.fromCardFrame 104 | 105 | // Temporary container view for animation 106 | let animatedContainerView = UIView() 107 | animatedContainerView.translatesAutoresizingMaskIntoConstraints = false 108 | if params.settings.isEnabledDebugAnimatingViews { 109 | animatedContainerView.layer.borderColor = UIColor.yellow.cgColor 110 | animatedContainerView.layer.borderWidth = 4 111 | cardDetailView.layer.borderColor = UIColor.red.cgColor 112 | cardDetailView.layer.borderWidth = 2 113 | } 114 | container.addSubview(animatedContainerView) 115 | 116 | do /* Fix centerX/width/height of animated container to container */ { 117 | let animatedContainerConstraints = [ 118 | animatedContainerView.widthAnchor.constraint(equalToConstant: container.bounds.width - (params.settings.cardContainerInsets.left + params.settings.cardContainerInsets.right)), 119 | animatedContainerView.heightAnchor.constraint(equalToConstant: container.bounds.height - (params.settings.cardContainerInsets.top + params.settings.cardContainerInsets.bottom)), 120 | animatedContainerView.centerXAnchor.constraint(equalTo: container.centerXAnchor) 121 | ] 122 | NSLayoutConstraint.activate(animatedContainerConstraints) 123 | } 124 | 125 | let animatedContainerVerticalConstraint: NSLayoutConstraint = { 126 | switch params.settings.cardVerticalExpandingStyle { 127 | case .fromCenter: 128 | return animatedContainerView.centerYAnchor.constraint( 129 | equalTo: container.centerYAnchor, 130 | constant: (fromCardFrame.height/2 + fromCardFrame.minY) - container.bounds.height/2 131 | ) 132 | case .fromTop: 133 | return animatedContainerView.topAnchor.constraint(equalTo: container.topAnchor, constant: fromCardFrame.minY + params.settings.cardContainerInsets.top) 134 | } 135 | 136 | }() 137 | animatedContainerVerticalConstraint.isActive = true 138 | 139 | animatedContainerView.addSubview(cardDetailView) 140 | cardDetailView.translatesAutoresizingMaskIntoConstraints = false 141 | 142 | do /* Pin top (or center Y) and center X of the card, in animated container view */ { 143 | let verticalAnchor: NSLayoutConstraint = { 144 | switch params.settings.cardVerticalExpandingStyle { 145 | case .fromCenter: 146 | return cardDetailView.centerYAnchor.constraint(equalTo: animatedContainerView.centerYAnchor) 147 | case .fromTop: 148 | return cardDetailView.topAnchor.constraint(equalTo: animatedContainerView.topAnchor) 149 | } 150 | }() 151 | let cardConstraints = [ 152 | verticalAnchor, 153 | cardDetailView.centerXAnchor.constraint(equalTo: animatedContainerView.centerXAnchor), 154 | ] 155 | NSLayoutConstraint.activate(cardConstraints) 156 | } 157 | let cardWidthConstraint = cardDetailView.widthAnchor.constraint(equalToConstant: fromCardFrame.width - (params.settings.cardContainerInsets.left + params.settings.cardContainerInsets.right)) 158 | let cardHeightConstraint = cardDetailView.heightAnchor.constraint(equalToConstant: fromCardFrame.height - (params.settings.cardContainerInsets.top + params.settings.cardContainerInsets.bottom)) 159 | NSLayoutConstraint.activate([cardWidthConstraint, cardHeightConstraint]) 160 | 161 | cardDetailView.layer.cornerRadius = params.settings.cardCornerRadius 162 | 163 | // ------------------------------- 164 | // Final preparation 165 | // ------------------------------- 166 | params.fromCell.isHidden = true 167 | params.fromCell.resetTransform() 168 | 169 | let topTemporaryFix = screens.cardDetail.cardContentView.topAnchor.constraint(equalTo: cardDetailView.topAnchor, constant: 0) 170 | topTemporaryFix.isActive = params.settings.isEnabledWeirdTopInsetsFix 171 | 172 | container.layoutIfNeeded() 173 | 174 | // ------------------------------ 175 | // 1. Animate container bouncing up 176 | // ------------------------------ 177 | func animateContainerBouncingUp() { 178 | animatedContainerVerticalConstraint.constant = 0 179 | container.layoutIfNeeded() 180 | } 181 | 182 | // ------------------------------ 183 | // 2. Animate cardDetail filling up the container 184 | // ------------------------------ 185 | func animateCardDetailViewSizing() { 186 | screens.cardDetail.didStartPresentAnimationProgress() 187 | 188 | cardWidthConstraint.constant = animatedContainerView.bounds.width + (params.settings.cardContainerInsets.left + params.settings.cardContainerInsets.right) 189 | cardHeightConstraint.constant = animatedContainerView.bounds.height + (params.settings.cardContainerInsets.top + params.settings.cardContainerInsets.bottom) 190 | cardDetailView.layer.cornerRadius = 0 191 | container.layoutIfNeeded() 192 | } 193 | 194 | func completeEverything() { 195 | // Remove temporary `animatedContainerView` 196 | animatedContainerView.removeConstraints(animatedContainerView.constraints) 197 | animatedContainerView.removeFromSuperview() 198 | 199 | // Re-add to the top 200 | container.addSubview(cardDetailView) 201 | 202 | cardDetailView.removeConstraints([topTemporaryFix, cardWidthConstraint, cardHeightConstraint]) 203 | 204 | cardDetailView.edges(to: container) 205 | 206 | // No longer need the bottom constraint that pins bottom of card content to its root. 207 | //screens.cardDetail.cardBottomToRootBottomConstraint.isActive = false 208 | screens.cardDetail.scrollView?.isScrollEnabled = true 209 | 210 | let success = !ctx.transitionWasCancelled 211 | ctx.completeTransition(success) 212 | 213 | screens.cardDetail.didFinishPresentAnimationProgress() 214 | } 215 | 216 | baseAnimator.addAnimations { 217 | 218 | // Spring animation for bouncing up 219 | animateContainerBouncingUp() 220 | 221 | // Linear animation for expansion 222 | let cardExpanding = UIViewPropertyAnimator(duration: baseAnimator.duration * 0.6, curve: .linear) { 223 | animateCardDetailViewSizing() 224 | } 225 | cardExpanding.startAnimation() 226 | } 227 | 228 | baseAnimator.addCompletion { (_) in 229 | completeEverything() 230 | } 231 | 232 | self.animator = baseAnimator 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/UIView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extension.swift 3 | // AppstoreTransition 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | /// Constrain 4 edges of `self` to specified `view`. 14 | func edges(to view: UIView, top: CGFloat=0, left: CGFloat=0, bottom: CGFloat=0, right: CGFloat=0) { 15 | NSLayoutConstraint.activate([ 16 | self.leftAnchor.constraint(equalTo: view.leftAnchor, constant: left), 17 | self.rightAnchor.constraint(equalTo: view.rightAnchor, constant: right), 18 | self.topAnchor.constraint(equalTo: view.topAnchor, constant: top), 19 | self.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: bottom) 20 | ]) 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Sources/AppstoreTransition/UIViewController+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extension.swift 3 | // AppstoreTransition 4 | // 5 | // Created by Razvan Chelemen on 15/05/2019. 6 | // Copyright © 2019 appssemble. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIViewController { 12 | 13 | func presentExpansion(_ viewControllerToPresent: UIViewController, cell:CardCollectionViewCell, animated flag: Bool, completion: (() -> Void)? = nil) { 14 | 15 | present(viewControllerToPresent, animated: true, completion: { [unowned cell] in 16 | // Unfreeze 17 | cell.unfreezeAnimations() 18 | }) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /appstore-card-transition.podspec: -------------------------------------------------------------------------------- 1 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 2 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 3 | # 4 | 5 | Pod::Spec.new do |s| 6 | 7 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 8 | # 9 | # These will help people to find your library, and whilst it 10 | # can feel like a chore to fill in it's definitely to your advantage. The 11 | # summary should be tweet-length, and the description more in depth. 12 | # 13 | 14 | s.name = "appstore-card-transition" 15 | s.version = "1.0.4" 16 | s.summary = "Appstore card animation transition. UICollectionView and UITableView card expand animated transition for iOS." 17 | s.module_name = 'AppstoreTransition' 18 | s.swift_version = '5.0' 19 | 20 | # This description is used to generate tags and improve search results. 21 | # * Think: What does it do? Why did you write it? What is the focus? 22 | # * Try to keep it short, snappy and to the point. 23 | # * Write the description between the DESC delimiters below. 24 | # * Finally, don't worry about the indent, CocoaPods strips it! 25 | s.description = <<-DESC 26 | Appstore card animation transition. UICollectionView and UITableView card expand animated transition for iOS. Expandable collectionview and tableview cells. Master detail transition animation. 27 | DESC 28 | 29 | s.homepage = "https://github.com/appssemble/appstore-card-transition" 30 | s.screenshots = "https://github.com/appssemble/appstore-card-transition/blob/master/gif/example1.gif?raw=true", "https://github.com/appssemble/appstore-card-transition/blob/master/gif/example2.gif?raw=true" 31 | 32 | 33 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 34 | # 35 | # Licensing your code is important. See http://choosealicense.com for more info. 36 | # CocoaPods will detect a license file if there is a named LICENSE* 37 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 38 | # 39 | 40 | s.license = { :type => "MIT", :file => "LICENSE" } 41 | 42 | 43 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 44 | # 45 | # Specify the authors of the library, with email addresses. Email addresses 46 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 47 | # accepts just a name if you'd rather not provide an email address. 48 | # 49 | # Specify a social_media_url where others can refer to, for example a twitter 50 | # profile URL. 51 | # 52 | 53 | s.author = { "Razvan Chelemen" => "chelemen.razvan@gmail.com" } 54 | # Or just: s.author = "Razvan Chelemen" 55 | # s.authors = { "Razvan Chelemen" => "chelemen.razvan@gmail.com" } 56 | s.social_media_url = "https://twitter.com/chelemen_razvan" 57 | 58 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 59 | # 60 | # If this Pod runs only on iOS or OS X, then specify the platform and 61 | # the deployment target. You can optionally include the target after the platform. 62 | # 63 | 64 | # When using multiple platforms 65 | s.ios.deployment_target = "11.0" 66 | # s.watchos.deployment_target = "2.0" 67 | # s.tvos.deployment_target = "9.0" 68 | 69 | 70 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 71 | # 72 | # Specify the location from where the source should be retrieved. 73 | # Supports git, hg, bzr, svn and HTTP. 74 | # 75 | 76 | s.source = { :git => "https://github.com/appssemble/appstore-card-transition.git", :tag => "#{s.version}" } 77 | 78 | 79 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 80 | # 81 | # CocoaPods is smart about how it includes source code. For source files 82 | # giving a folder will include any swift, h, m, mm, c & cpp files. 83 | # For header files it will include any header in the folder. 84 | # Not including the public_header_files will make all headers public. 85 | # 86 | 87 | s.source_files = "Sources/AppstoreTransition/*.{h,m,swift,c}" 88 | #s.exclude_files = "Classes/Exclude" 89 | 90 | # s.public_header_files = "Classes/**/*.h" 91 | 92 | 93 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 94 | # 95 | # A list of resources included with the Pod. These are copied into the 96 | # target bundle with a build phase script. Anything else will be cleaned. 97 | # You can preserve files from being cleaned, please don't preserve 98 | # non-essential files like tests, examples and documentation. 99 | # 100 | 101 | # s.resource = "icon.png" 102 | # s.resources = "Resources/*.png" 103 | 104 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave" 105 | 106 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 107 | # 108 | # Link your library with frameworks, or libraries. Libraries do not include 109 | # the lib prefix of their name. 110 | # 111 | 112 | # s.framework = "SomeFramework" 113 | # s.frameworks = "SomeFramework", "AnotherFramework" 114 | 115 | # s.library = 'CommonCrypto' 116 | # s.libraries = "iconv", "xml2" 117 | 118 | # s.ios.vendored_libraries = 'stellarsdk/stellarsdk/libs/**/*' 119 | 120 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 121 | # 122 | # If your library depends on compiler flags you can set them in the xcconfig hash 123 | # where they will only apply to your library. If you depend on other Podspecs 124 | # you can include multiple dependencies to ensure it works. 125 | 126 | # s.requires_arc = true 127 | 128 | # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } 129 | # s.dependency "JSONKit", "~> 1.4" 130 | 131 | end 132 | -------------------------------------------------------------------------------- /gif/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/gif/example1.gif -------------------------------------------------------------------------------- /gif/example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/gif/example2.gif -------------------------------------------------------------------------------- /gif/example3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/gif/example3.gif -------------------------------------------------------------------------------- /gif/example4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/gif/example4.gif -------------------------------------------------------------------------------- /gif/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appssemble/appstore-card-transition/a220fc468f1a9a2b7ff7dba5029b737af4c69422/gif/logo.png --------------------------------------------------------------------------------