├── .DS_Store ├── Custom Transitions ├── Assets.xcassets │ ├── Contents.json │ ├── .DS_Store │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── .DS_Store ├── Info.plist ├── AppDelegate.swift ├── Base.lproj │ ├── Main.storyboard │ └── LaunchScreen.storyboard ├── SceneDelegate.swift ├── Logistical - No code samples here │ ├── ControllerSetup.swift │ └── PresentationScenario.swift ├── TransitionDelegate.swift ├── ViewController.swift ├── TransitionAnimator.swift └── TransitionPresentationController.swift ├── Custom Transitions.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── project.pbxproj ├── LICENSE ├── README.md └── .gitignore /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DreamingInBinary/Custom-ViewController-Transitions/HEAD/.DS_Store -------------------------------------------------------------------------------- /Custom Transitions/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Custom Transitions/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DreamingInBinary/Custom-ViewController-Transitions/HEAD/Custom Transitions/.DS_Store -------------------------------------------------------------------------------- /Custom Transitions/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DreamingInBinary/Custom-ViewController-Transitions/HEAD/Custom Transitions/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /Custom Transitions.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Custom Transitions/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Custom Transitions.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Custom Transitions/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jordan Morgan 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 | -------------------------------------------------------------------------------- /Custom Transitions/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Custom Transitions 4 | // 5 | // Created by Jordan Morgan on 9/30/21. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Custom Transitions/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 | -------------------------------------------------------------------------------- /Custom Transitions/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 | -------------------------------------------------------------------------------- /Custom Transitions/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom View Controller Transitions 2 | This project explains and shows how to make custom view controller transitions in the most simple way possible. Each example builds off of each other. 3 | 4 | ## Objects you make 5 | 6 | 1. Transitioning delegate (Required) 7 | An object whose sole job is to vend either custom animators, presentation 8 | controllers, interactive animators or all of those to UIKit. Setting a custom transitioning delegate for a controllers' 9 | transitioningDelegate property is what tells UIKit you want to perform a 10 | custom controller transition. 11 | 12 | 2. Animator _or_ both an Animator and Interactive Animator (Optional) 13 | This decides the duration of the custom transition and performs the actual 14 | animations of the presented view during both presentation and dismissal. It does 15 | *not* decide the final frame of the presented view. Presentation controllers do. 16 | If it an interactive animator is created, it's only job is to calculate how much 17 | of the transition has been completed. 18 | 19 | 3. Presentation Controller (Optional) 20 | This manages chrome outside of the presented or presenting 21 | view controllers and can animate those, such as a dimmer view. It also decides 22 | the final size of the presented view controller's view. It also can respond 23 | to changes occur in the app’s environment. 24 | 25 | ## Objects UIKit makes 26 | 27 | 1. Tranistion Context 28 | Contains all key components of the transition, like to the to and from view controllers. 29 | 30 | 2. Transition Coordinator 31 | Used to hook into transitions and their animations to perform any other animations or 32 | changes on existing controllers. For example, if you wanted to animate the deselection 33 | of a table row in a root view controller on a navigation stack, you'd hook into the controller's 34 | transition coordinator to animate alongside a popped controller's presentation and dismissal. 35 | Much the same way, a presentation controller animates chrome using the presented controller's 36 | transition coordinator too. 37 | 38 | ## Performing the Custom Transition 39 | 40 | 1. Initialize the view controller to be presented. 41 | 2. Assign your transitioning delegate to the controller’s transitioningDelegate property. Using it, return either 42 | 2a) Custom animator or interactive animator. 43 | 2b) A custom presentation controller. Requires that the controller's modal presentation style is set to custom. 44 | 2c) Or both. 45 | 3. Call presentViewController:animated:completion. 46 | -------------------------------------------------------------------------------- /Custom Transitions/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Custom Transitions 4 | // 5 | // Created by Jordan Morgan on 9/30/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Custom Transitions/Logistical - No code samples here/ControllerSetup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControllerSetup.swift 3 | // Custom Transitions 4 | // 5 | // Created by Jordan Morgan on 10/1/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | typealias TableDataSource = UITableViewDiffableDataSource 12 | 13 | class Source: TableDataSource { 14 | override func tableView(_ tableView: UITableView, titleForFooterInSection 15 | section: Int) -> String? { 16 | return PresentationScenario.all[section].info 17 | } 18 | } 19 | 20 | extension ViewController { 21 | func createDemoController() -> UIViewController { 22 | let demoVC = UIViewController() 23 | let demoView = demoVC.view! 24 | demoView.backgroundColor = .blue 25 | 26 | let dismiss = UIButton(configuration: .borderedProminent(), primaryAction: UIAction() { _ in 27 | demoVC.dismiss(animated: true) 28 | }) 29 | dismiss.setTitle("Dismiss", for: .normal) 30 | demoView.addSubview(dismiss) 31 | dismiss.frame.size = CGSize(width: 100, height: 54) 32 | dismiss.frame.origin = CGPoint(x: demoView.bounds.size.width/2 - 50, 33 | y: demoView.bounds.size.height/2 - 27) 34 | 35 | 36 | return demoVC 37 | } 38 | 39 | func setupViews() { 40 | tv.frame = view.bounds 41 | tv.delegate = self 42 | view.addSubview(tv) 43 | tv.autoresizingMask = [.flexibleWidth, .flexibleHeight] 44 | tv.register(UITableViewCell.classForCoder(), forCellReuseIdentifier: "cell") 45 | 46 | let allItems = PresentationScenario.all 47 | 48 | var snapshot = datasource.snapshot() 49 | snapshot.appendSections(allItems.map{ return allItems.firstIndex(of: $0)! }) 50 | for (idx, val) in allItems.enumerated() { 51 | snapshot.appendItems([val], toSection: idx) 52 | } 53 | 54 | datasource.apply(snapshot) 55 | } 56 | } 57 | 58 | extension ViewController: UITableViewDelegate { 59 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 60 | switch indexPath.section { 61 | case 0: 62 | self.hookIntoExistingCoordinator() 63 | case 1: 64 | self.onlyUseCustomAnimator() 65 | case 2: 66 | self.onlyPresentationController() 67 | case 3: 68 | self.animatorAndPresentationController() 69 | case 4: 70 | self.interactiveAnimatorAndPresentationController() 71 | default: 72 | return 73 | } 74 | tableView.deselectRow(at: indexPath, animated: true) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Custom Transitions/TransitionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionDelegate.swift 3 | // Custom Transitions 4 | // 5 | // Created by Jordan Morgan on 9/30/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | enum UseCase { 12 | case justAnimator 13 | case justPresentationController 14 | case animatorAndPresentationController 15 | case interactiveAnimatorAndPresentationController 16 | } 17 | 18 | class TransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { 19 | var demoedUseCase: UseCase = .justAnimator 20 | let interactiveAnimator: UIPercentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition() 21 | 22 | // MARK: Animators. Sometimes called animation controllers. 23 | 24 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 25 | return createAnimator(isPresenting: true) 26 | } 27 | 28 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 29 | return createAnimator(isPresenting: false) 30 | } 31 | 32 | // MARK: Interactive animators. Sometimes called interactive animation controllers. 33 | 34 | func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 35 | guard demoedUseCase == .interactiveAnimatorAndPresentationController else { return nil } 36 | return interactiveAnimator 37 | } 38 | 39 | // MARK: Presentation Controllers 40 | 41 | func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { 42 | switch demoedUseCase { 43 | case .justAnimator: 44 | return nil 45 | case .justPresentationController: 46 | return TransitionPresentationController(presentedViewController: presented, presenting: presenting) 47 | case .animatorAndPresentationController: 48 | return TransitionPresentationController(presentedViewController: presented, presenting: presenting) 49 | case .interactiveAnimatorAndPresentationController: 50 | let presentationController = TransitionPresentationController(presentedViewController: presented, presenting: presenting) 51 | presentationController.transitioningDelegateWantsInteractiveDismissal = true 52 | 53 | return presentationController 54 | } 55 | } 56 | 57 | // MARK: Private 58 | 59 | private func createAnimator(isPresenting:Bool) -> UIViewControllerAnimatedTransitioning? { 60 | let animator = TransitionAnimator() 61 | animator.isPresenting = isPresenting 62 | 63 | switch demoedUseCase { 64 | case .justAnimator, .animatorAndPresentationController: 65 | return animator 66 | case .justPresentationController: 67 | return nil 68 | case .interactiveAnimatorAndPresentationController: 69 | return animator 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Custom Transitions/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Custom Transitions 4 | // 5 | // Created by Jordan Morgan on 9/30/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | let customTransitionDelegate: TransitionDelegate = TransitionDelegate() 12 | let tv = UITableView(frame: .zero, style: .insetGrouped) 13 | lazy var datasource = Source(tableView: tv, cellProvider: { (tableView, indexPath, model) -> UITableViewCell? in 14 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 15 | cell.textLabel?.text = model.name 16 | return cell 17 | }) 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | view.backgroundColor = .systemBackground 22 | setupViews() 23 | } 24 | 25 | // MARK: Custom animation options 26 | 27 | func hookIntoExistingCoordinator() { 28 | // This doesn't use any custom transition. It simply hooks into the 29 | // Ones already occuring. 30 | let vc = createDemoController() 31 | present(vc, animated: true, completion: nil) 32 | 33 | transitionCoordinator?.animate(alongsideTransition: { context in 34 | self.tv.backgroundColor = .red 35 | }, completion: { context in 36 | if context.isCancelled { 37 | self.tv.backgroundColor = .systemGroupedBackground 38 | } else { 39 | UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { 40 | self.tv.backgroundColor = .systemGroupedBackground 41 | } completion: { finished in 42 | 43 | } 44 | } 45 | }) 46 | } 47 | 48 | func onlyUseCustomAnimator() { 49 | customTransitionDelegate.demoedUseCase = .justAnimator 50 | 51 | let vc = createDemoController() 52 | vc.transitioningDelegate = customTransitionDelegate 53 | vc.modalPresentationStyle = .overFullScreen 54 | present(vc, animated: true, completion: nil) 55 | } 56 | 57 | func onlyPresentationController() { 58 | customTransitionDelegate.demoedUseCase = .justPresentationController 59 | 60 | let vc = createDemoController() 61 | vc.transitioningDelegate = customTransitionDelegate 62 | vc.modalPresentationStyle = .custom 63 | present(vc, animated: true, completion: nil) 64 | } 65 | 66 | func animatorAndPresentationController() { 67 | customTransitionDelegate.demoedUseCase = .animatorAndPresentationController 68 | 69 | let vc = createDemoController() 70 | vc.transitioningDelegate = customTransitionDelegate 71 | vc.modalPresentationStyle = .custom 72 | present(vc, animated: true, completion: nil) 73 | } 74 | 75 | func interactiveAnimatorAndPresentationController() { 76 | customTransitionDelegate.demoedUseCase = .interactiveAnimatorAndPresentationController 77 | 78 | let vc = createDemoController() 79 | vc.transitioningDelegate = customTransitionDelegate 80 | vc.modalPresentationStyle = .custom 81 | vc.view.subviews.forEach { $0.removeFromSuperview() } 82 | present(vc, animated: true, completion: nil) 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /Custom Transitions/TransitionAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionAnimator.swift 3 | // Custom Transitions 4 | // 5 | // Created by Jordan Morgan on 9/30/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { 12 | var transitionDuration: TimeInterval = 1.0 13 | var isPresenting: Bool = true 14 | 15 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 16 | return transitionDuration 17 | } 18 | 19 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 20 | // Views and controllers 21 | let containerView = transitionContext.containerView 22 | let fromVC = transitionContext.viewController(forKey: .from) 23 | let toVC = transitionContext.viewController(forKey: .to) 24 | let fromView = transitionContext.view(forKey: .from) 25 | let toView = transitionContext.view(forKey: .to) 26 | 27 | // Rects 28 | let containerFrame: CGRect = containerView.frame 29 | var toViewBeginningFrame: CGRect = .zero 30 | var toViewEndingFrame: CGRect = .zero 31 | var fromViewEndingFrame: CGRect = .zero 32 | 33 | if let destinationView = toView, let presentedVC = toVC, let presentingVC = fromVC { 34 | containerView.addSubview(destinationView) 35 | toViewBeginningFrame = transitionContext.initialFrame(for: presentedVC) 36 | toViewEndingFrame = transitionContext.finalFrame(for: presentedVC) 37 | fromViewEndingFrame = transitionContext.finalFrame(for: presentingVC) 38 | } 39 | 40 | if isPresenting { 41 | toViewBeginningFrame.origin = CGPoint(x: containerFrame.size.width, y: containerFrame.size.width) 42 | toViewBeginningFrame.size = toViewEndingFrame.size 43 | } else { 44 | fromViewEndingFrame = CGRect(x:containerFrame.size.width, 45 | y:containerFrame.size.height, 46 | width:toView?.frame.size.width ?? 0, 47 | height:toView?.frame.size.height ?? 0); 48 | } 49 | 50 | toView?.frame = toViewBeginningFrame 51 | 52 | // For interactive animations, you might want to avoid nonlinear effects in the animations themselves. These usually decouple the touch location of events. 53 | UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.9, options: .curveEaseOut) { 54 | if self.isPresenting { 55 | toView?.frame = toViewEndingFrame 56 | fromVC?.view.frame = containerFrame 57 | } else { 58 | fromView?.frame = fromViewEndingFrame 59 | } 60 | } completion: { done in 61 | let succeeded = !transitionContext.transitionWasCancelled 62 | let failedPresenting = (self.isPresenting && !succeeded) 63 | let didDismiss = (!self.isPresenting && succeeded) 64 | 65 | if (failedPresenting || didDismiss) { 66 | toView?.removeFromSuperview() 67 | } 68 | 69 | transitionContext.completeTransition(succeeded) 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Custom Transitions/Logistical - No code samples here/PresentationScenario.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentationScenario.swift 3 | // Custom Transitions 4 | // 5 | // Created by Jordan Morgan on 10/1/21. 6 | // 7 | 8 | import Foundation 9 | 10 | // Future examples: UINavigationController, subclassed UIPercentDrivenInteractiveTransition and UIViewPropertyAnimator 11 | struct PresentationScenario: Hashable { 12 | let id = UUID() 13 | let name: String 14 | let info: String 15 | static let all: [PresentationScenario] = [PresentationScenario(name: "Just Coordinator", info: "This hooks into this controller's UIViewControllerTransitionCoordinator to do some animation alongside another controller's presentation. In this case, it'll animate the background turning red as the new controller presents.\n\nTL;DR This lets you do arbitrary animations in lockstep with another presentation or dismissal."), 16 | PresentationScenario(name: "Just Animator", info: "Here, we have our transitioning delegate vend only an animator - which conforms to UIViewControllerAnimatedTransitioning. With it, we get what the final frame should be of the presented view - but are free to animate it to that CGRect any way we want. Also, we can set a starting rect. Here, we make it animate from the bottom right, up to center. It dismisses the same way.\n\nTL;DR This decides how to animate the presented view to its final CGRect."), 17 | PresentationScenario(name: "Just Presentation Controller", info: "In this case, a custom UIPresentationController is used. A presentation controller is typically used for two things (though it can do more); A) Animating \"chrome\" outside of the two controllers such as a dimming view and B) Calculating the final frame of the presented controller's view. So here, our transitioning delegate only vends a presentation controller which then tells UIKit that the presented controller's frame should be half of the width of the container view, positioned on the right half of the screen. It also animates a dimming view behind it all.\n\nTL;DR This can size the presented view, but doesn't control its animations, but it *can* animate chrome outside of the presenting or presented controller."), 18 | PresentationScenario(name: "Animator and Presentation Controller", info: "This is the most common use of the API. Here, the transitioning delegate returns both a custom animator object and a presentation controller. This means we can fully control every aspect of the transition. The presentation controller will tell the animator where to put the presented view, the animator will be in charge of animating it there and the presentation controller will also manage a dimming view (i.e. extra \"chrome\").\n\nTL;DR This is the most common way to do custom transitions. Create a custom transitioning delegate which tells UIKit which animator and presentation controller to use."), 19 | PresentationScenario(name: "Add Interactive Animator", info: "This is the same exact scenario as above, save for the fact that we can now use a custom UIGestureRecognizer to drive the completion percentage of the animation via UIViewControllerInteractiveTransitioning. This protocol is actually what makes a stock UINavigationController pop and push transition interactive. \n\nThis custom transition works the same way as above, except now using a UIPercentDrivenInteractiveTransition (which implements the aforementioned protocol for us) object vended in the transitioning delegate, we set up the event handling code needed to determine a completion percentage for the transition and update it accordingly. To use this, swipe down on the dimming view after presentation to kick off an interactive dismissmal.\n\nTL;DR This uses all the objects we've made so far, but adds one more object to calculate the completion percentage of a dismissal from a pan gesture, which drives the dismissal itself.")] 20 | } 21 | -------------------------------------------------------------------------------- /Custom Transitions/TransitionPresentationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionPresentationController.swift 3 | // Custom Transitions 4 | // 5 | // Created by Jordan Morgan on 9/30/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class TransitionPresentationController: UIPresentationController { 11 | var transitioningDelegateWantsInteractiveDismissal: Bool = false 12 | private var dimmingView: UIView = UIView(frame: .zero) 13 | private var panGesture: UIPanGestureRecognizer? = nil 14 | 15 | override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { 16 | super.init(presentedViewController: presentedViewController, presenting: presentingViewController) 17 | dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.3) 18 | dimmingView.alpha = 0.0 19 | } 20 | 21 | // MARK: Chrome Animations 22 | 23 | override func presentationTransitionWillBegin() { 24 | guard let container = containerView else { return } 25 | 26 | dimmingView.frame = container.bounds 27 | dimmingView.alpha = 0.0 28 | container.insertSubview(dimmingView, at: 0) 29 | 30 | presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] context in 31 | self?.dimmingView.alpha = 1.0 32 | }, completion: { [weak self] context in 33 | if context.isCancelled { 34 | self?.dimmingView.alpha = 0.0 35 | } 36 | }) 37 | } 38 | 39 | override func presentationTransitionDidEnd(_ completed: Bool) { 40 | guard let container = containerView, 41 | transitioningDelegateWantsInteractiveDismissal == true, 42 | let _ = presentedViewController.transitioningDelegate as? TransitionDelegate else { return } 43 | 44 | // Attach a gesture recognizer to the dimming view to allow for a swipe to dismiss 45 | // Once we've confirmed that our transition delegate wants one 46 | panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleSwipeToDismiss(pan:))) 47 | panGesture?.maximumNumberOfTouches = 1 48 | container.addGestureRecognizer(panGesture!) 49 | } 50 | 51 | override func dismissalTransitionWillBegin() { 52 | guard let coordinator = presentedViewController.transitionCoordinator else { return } 53 | 54 | coordinator.animate { [weak self] context in 55 | self?.dimmingView.alpha = 0.0 56 | } completion: { [weak self] context in 57 | if context.isCancelled { 58 | self?.dimmingView.alpha = 1.0 59 | } 60 | } 61 | 62 | } 63 | 64 | override func dismissalTransitionDidEnd(_ completed: Bool) { 65 | if completed { 66 | dimmingView.removeFromSuperview() 67 | } 68 | } 69 | 70 | // MARK: Sizing 71 | 72 | override var frameOfPresentedViewInContainerView: CGRect { 73 | var presentedViewFrame = CGRect.zero 74 | guard let containerBounds = containerView?.bounds else { return .zero } 75 | 76 | presentedViewFrame.size = CGSize(width: (containerBounds.size.width/2), height: containerBounds.size.height) 77 | presentedViewFrame.origin.x = containerBounds.size.width - presentedViewFrame.size.width; 78 | return presentedViewFrame; 79 | } 80 | 81 | // MARK: Private 82 | 83 | @objc private func handleSwipeToDismiss(pan:UIPanGestureRecognizer) { 84 | guard let container = containerView, 85 | let customTransitionDelegate = presentedViewController.transitioningDelegate as? TransitionDelegate else { return } 86 | 87 | let percentDriver = customTransitionDelegate.interactiveAnimator 88 | let translation = pan.translation(in: container) 89 | let percentComplete = abs(translation.y/container.bounds.height) 90 | let completionThreshold = percentComplete * 0.80 91 | 92 | func bailOut(withCancel: Bool) { 93 | if withCancel { 94 | percentDriver.cancel() 95 | } else { 96 | percentDriver.finish() 97 | container.removeGestureRecognizer(pan) 98 | } 99 | } 100 | 101 | switch pan.state { 102 | case .began: 103 | pan.setTranslation(.zero, in: container) 104 | presentedViewController.dismiss(animated: true) // Kicks off interactive transition 105 | case .changed: 106 | percentDriver.update(percentComplete) 107 | case .ended: 108 | if completionThreshold > 0.5 || pan.velocity(in: container).y > 0 { 109 | percentDriver.completionSpeed = 1.0 110 | bailOut(withCancel: false) 111 | } else { 112 | // Smooth out cancellation 113 | percentDriver.completionSpeed = 1.0 * percentComplete 114 | bailOut(withCancel: true) 115 | } 116 | case .cancelled: 117 | bailOut(withCancel: true) 118 | case .failed: 119 | bailOut(withCancel: false) 120 | default: 121 | bailOut(withCancel: false) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Custom Transitions.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 339A9DF02706B8E4006EC9D0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339A9DEF2706B8E4006EC9D0 /* AppDelegate.swift */; }; 11 | 339A9DF22706B8E4006EC9D0 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339A9DF12706B8E4006EC9D0 /* SceneDelegate.swift */; }; 12 | 339A9DF42706B8E4006EC9D0 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339A9DF32706B8E4006EC9D0 /* ViewController.swift */; }; 13 | 339A9DF72706B8E4006EC9D0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 339A9DF52706B8E4006EC9D0 /* Main.storyboard */; }; 14 | 339A9DF92706B8E5006EC9D0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 339A9DF82706B8E5006EC9D0 /* Assets.xcassets */; }; 15 | 339A9DFC2706B8E5006EC9D0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 339A9DFA2706B8E5006EC9D0 /* LaunchScreen.storyboard */; }; 16 | 339A9E042706B955006EC9D0 /* TransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339A9E032706B955006EC9D0 /* TransitionAnimator.swift */; }; 17 | 339A9E062706B960006EC9D0 /* TransitionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339A9E052706B960006EC9D0 /* TransitionDelegate.swift */; }; 18 | 339A9E082706B976006EC9D0 /* TransitionPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339A9E072706B976006EC9D0 /* TransitionPresentationController.swift */; }; 19 | 339A9E0E2706BAA4006EC9D0 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 339A9E0D2706BAA4006EC9D0 /* README.md */; }; 20 | 339A9E112707C4B7006EC9D0 /* PresentationScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339A9E102707C4B7006EC9D0 /* PresentationScenario.swift */; }; 21 | 339A9E132707C4DF006EC9D0 /* ControllerSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339A9E122707C4DF006EC9D0 /* ControllerSetup.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 339A9DEC2706B8E4006EC9D0 /* Custom Transitions.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Custom Transitions.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 339A9DEF2706B8E4006EC9D0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 27 | 339A9DF12706B8E4006EC9D0 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 28 | 339A9DF32706B8E4006EC9D0 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 29 | 339A9DF62706B8E4006EC9D0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 30 | 339A9DF82706B8E5006EC9D0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 31 | 339A9DFB2706B8E5006EC9D0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 32 | 339A9DFD2706B8E5006EC9D0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 33 | 339A9E032706B955006EC9D0 /* TransitionAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionAnimator.swift; sourceTree = ""; }; 34 | 339A9E052706B960006EC9D0 /* TransitionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionDelegate.swift; sourceTree = ""; }; 35 | 339A9E072706B976006EC9D0 /* TransitionPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionPresentationController.swift; sourceTree = ""; }; 36 | 339A9E0D2706BAA4006EC9D0 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 37 | 339A9E102707C4B7006EC9D0 /* PresentationScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationScenario.swift; sourceTree = ""; }; 38 | 339A9E122707C4DF006EC9D0 /* ControllerSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerSetup.swift; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 339A9DE92706B8E4006EC9D0 /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | 339A9DE32706B8E4006EC9D0 = { 53 | isa = PBXGroup; 54 | children = ( 55 | 339A9E0D2706BAA4006EC9D0 /* README.md */, 56 | 339A9DEE2706B8E4006EC9D0 /* Custom Transitions */, 57 | 339A9DED2706B8E4006EC9D0 /* Products */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | 339A9DED2706B8E4006EC9D0 /* Products */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 339A9DEC2706B8E4006EC9D0 /* Custom Transitions.app */, 65 | ); 66 | name = Products; 67 | sourceTree = ""; 68 | }; 69 | 339A9DEE2706B8E4006EC9D0 /* Custom Transitions */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 339A9E0F2707C49A006EC9D0 /* Logistical - No code samples here */, 73 | 339A9DEF2706B8E4006EC9D0 /* AppDelegate.swift */, 74 | 339A9DF12706B8E4006EC9D0 /* SceneDelegate.swift */, 75 | 339A9DF32706B8E4006EC9D0 /* ViewController.swift */, 76 | 339A9DF52706B8E4006EC9D0 /* Main.storyboard */, 77 | 339A9DF82706B8E5006EC9D0 /* Assets.xcassets */, 78 | 339A9DFA2706B8E5006EC9D0 /* LaunchScreen.storyboard */, 79 | 339A9DFD2706B8E5006EC9D0 /* Info.plist */, 80 | 339A9E032706B955006EC9D0 /* TransitionAnimator.swift */, 81 | 339A9E052706B960006EC9D0 /* TransitionDelegate.swift */, 82 | 339A9E072706B976006EC9D0 /* TransitionPresentationController.swift */, 83 | ); 84 | path = "Custom Transitions"; 85 | sourceTree = ""; 86 | }; 87 | 339A9E0F2707C49A006EC9D0 /* Logistical - No code samples here */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 339A9E102707C4B7006EC9D0 /* PresentationScenario.swift */, 91 | 339A9E122707C4DF006EC9D0 /* ControllerSetup.swift */, 92 | ); 93 | path = "Logistical - No code samples here"; 94 | sourceTree = ""; 95 | }; 96 | /* End PBXGroup section */ 97 | 98 | /* Begin PBXNativeTarget section */ 99 | 339A9DEB2706B8E4006EC9D0 /* Custom Transitions */ = { 100 | isa = PBXNativeTarget; 101 | buildConfigurationList = 339A9E002706B8E5006EC9D0 /* Build configuration list for PBXNativeTarget "Custom Transitions" */; 102 | buildPhases = ( 103 | 339A9DE82706B8E4006EC9D0 /* Sources */, 104 | 339A9DE92706B8E4006EC9D0 /* Frameworks */, 105 | 339A9DEA2706B8E4006EC9D0 /* Resources */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = "Custom Transitions"; 112 | productName = "Custom Transitions"; 113 | productReference = 339A9DEC2706B8E4006EC9D0 /* Custom Transitions.app */; 114 | productType = "com.apple.product-type.application"; 115 | }; 116 | /* End PBXNativeTarget section */ 117 | 118 | /* Begin PBXProject section */ 119 | 339A9DE42706B8E4006EC9D0 /* Project object */ = { 120 | isa = PBXProject; 121 | attributes = { 122 | BuildIndependentTargetsInParallel = 1; 123 | LastSwiftUpdateCheck = 1300; 124 | LastUpgradeCheck = 1300; 125 | TargetAttributes = { 126 | 339A9DEB2706B8E4006EC9D0 = { 127 | CreatedOnToolsVersion = 13.0; 128 | }; 129 | }; 130 | }; 131 | buildConfigurationList = 339A9DE72706B8E4006EC9D0 /* Build configuration list for PBXProject "Custom Transitions" */; 132 | compatibilityVersion = "Xcode 13.0"; 133 | developmentRegion = en; 134 | hasScannedForEncodings = 0; 135 | knownRegions = ( 136 | en, 137 | Base, 138 | ); 139 | mainGroup = 339A9DE32706B8E4006EC9D0; 140 | productRefGroup = 339A9DED2706B8E4006EC9D0 /* Products */; 141 | projectDirPath = ""; 142 | projectRoot = ""; 143 | targets = ( 144 | 339A9DEB2706B8E4006EC9D0 /* Custom Transitions */, 145 | ); 146 | }; 147 | /* End PBXProject section */ 148 | 149 | /* Begin PBXResourcesBuildPhase section */ 150 | 339A9DEA2706B8E4006EC9D0 /* Resources */ = { 151 | isa = PBXResourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | 339A9DFC2706B8E5006EC9D0 /* LaunchScreen.storyboard in Resources */, 155 | 339A9DF92706B8E5006EC9D0 /* Assets.xcassets in Resources */, 156 | 339A9E0E2706BAA4006EC9D0 /* README.md in Resources */, 157 | 339A9DF72706B8E4006EC9D0 /* Main.storyboard in Resources */, 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXResourcesBuildPhase section */ 162 | 163 | /* Begin PBXSourcesBuildPhase section */ 164 | 339A9DE82706B8E4006EC9D0 /* Sources */ = { 165 | isa = PBXSourcesBuildPhase; 166 | buildActionMask = 2147483647; 167 | files = ( 168 | 339A9DF42706B8E4006EC9D0 /* ViewController.swift in Sources */, 169 | 339A9E062706B960006EC9D0 /* TransitionDelegate.swift in Sources */, 170 | 339A9E082706B976006EC9D0 /* TransitionPresentationController.swift in Sources */, 171 | 339A9E132707C4DF006EC9D0 /* ControllerSetup.swift in Sources */, 172 | 339A9E042706B955006EC9D0 /* TransitionAnimator.swift in Sources */, 173 | 339A9E112707C4B7006EC9D0 /* PresentationScenario.swift in Sources */, 174 | 339A9DF02706B8E4006EC9D0 /* AppDelegate.swift in Sources */, 175 | 339A9DF22706B8E4006EC9D0 /* SceneDelegate.swift in Sources */, 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | }; 179 | /* End PBXSourcesBuildPhase section */ 180 | 181 | /* Begin PBXVariantGroup section */ 182 | 339A9DF52706B8E4006EC9D0 /* Main.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | 339A9DF62706B8E4006EC9D0 /* Base */, 186 | ); 187 | name = Main.storyboard; 188 | sourceTree = ""; 189 | }; 190 | 339A9DFA2706B8E5006EC9D0 /* LaunchScreen.storyboard */ = { 191 | isa = PBXVariantGroup; 192 | children = ( 193 | 339A9DFB2706B8E5006EC9D0 /* Base */, 194 | ); 195 | name = LaunchScreen.storyboard; 196 | sourceTree = ""; 197 | }; 198 | /* End PBXVariantGroup section */ 199 | 200 | /* Begin XCBuildConfiguration section */ 201 | 339A9DFE2706B8E5006EC9D0 /* Debug */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | CLANG_ANALYZER_NONNULL = YES; 206 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 208 | CLANG_CXX_LIBRARY = "libc++"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_ENABLE_OBJC_WEAK = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 217 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 218 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 219 | CLANG_WARN_EMPTY_BODY = YES; 220 | CLANG_WARN_ENUM_CONVERSION = YES; 221 | CLANG_WARN_INFINITE_RECURSION = YES; 222 | CLANG_WARN_INT_CONVERSION = YES; 223 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 225 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 228 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 229 | CLANG_WARN_STRICT_PROTOTYPES = YES; 230 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 231 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 232 | CLANG_WARN_UNREACHABLE_CODE = YES; 233 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 234 | COPY_PHASE_STRIP = NO; 235 | DEBUG_INFORMATION_FORMAT = dwarf; 236 | ENABLE_STRICT_OBJC_MSGSEND = YES; 237 | ENABLE_TESTABILITY = YES; 238 | GCC_C_LANGUAGE_STANDARD = gnu11; 239 | GCC_DYNAMIC_NO_PIC = NO; 240 | GCC_NO_COMMON_BLOCKS = YES; 241 | GCC_OPTIMIZATION_LEVEL = 0; 242 | GCC_PREPROCESSOR_DEFINITIONS = ( 243 | "DEBUG=1", 244 | "$(inherited)", 245 | ); 246 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 247 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 248 | GCC_WARN_UNDECLARED_SELECTOR = YES; 249 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 250 | GCC_WARN_UNUSED_FUNCTION = YES; 251 | GCC_WARN_UNUSED_VARIABLE = YES; 252 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 253 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 254 | MTL_FAST_MATH = YES; 255 | ONLY_ACTIVE_ARCH = YES; 256 | SDKROOT = iphoneos; 257 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 258 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 259 | }; 260 | name = Debug; 261 | }; 262 | 339A9DFF2706B8E5006EC9D0 /* Release */ = { 263 | isa = XCBuildConfiguration; 264 | buildSettings = { 265 | ALWAYS_SEARCH_USER_PATHS = NO; 266 | CLANG_ANALYZER_NONNULL = YES; 267 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 268 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 269 | CLANG_CXX_LIBRARY = "libc++"; 270 | CLANG_ENABLE_MODULES = YES; 271 | CLANG_ENABLE_OBJC_ARC = YES; 272 | CLANG_ENABLE_OBJC_WEAK = YES; 273 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 274 | CLANG_WARN_BOOL_CONVERSION = YES; 275 | CLANG_WARN_COMMA = YES; 276 | CLANG_WARN_CONSTANT_CONVERSION = YES; 277 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 278 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 279 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 280 | CLANG_WARN_EMPTY_BODY = YES; 281 | CLANG_WARN_ENUM_CONVERSION = YES; 282 | CLANG_WARN_INFINITE_RECURSION = YES; 283 | CLANG_WARN_INT_CONVERSION = YES; 284 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 285 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 286 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 288 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 289 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 290 | CLANG_WARN_STRICT_PROTOTYPES = YES; 291 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 292 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 293 | CLANG_WARN_UNREACHABLE_CODE = YES; 294 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 295 | COPY_PHASE_STRIP = NO; 296 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 297 | ENABLE_NS_ASSERTIONS = NO; 298 | ENABLE_STRICT_OBJC_MSGSEND = YES; 299 | GCC_C_LANGUAGE_STANDARD = gnu11; 300 | GCC_NO_COMMON_BLOCKS = YES; 301 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 302 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 303 | GCC_WARN_UNDECLARED_SELECTOR = YES; 304 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 305 | GCC_WARN_UNUSED_FUNCTION = YES; 306 | GCC_WARN_UNUSED_VARIABLE = YES; 307 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 308 | MTL_ENABLE_DEBUG_INFO = NO; 309 | MTL_FAST_MATH = YES; 310 | SDKROOT = iphoneos; 311 | SWIFT_COMPILATION_MODE = wholemodule; 312 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 313 | VALIDATE_PRODUCT = YES; 314 | }; 315 | name = Release; 316 | }; 317 | 339A9E012706B8E5006EC9D0 /* Debug */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 321 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 322 | CODE_SIGN_STYLE = Automatic; 323 | CURRENT_PROJECT_VERSION = 1; 324 | DEVELOPMENT_TEAM = G6E6N7P5UN; 325 | GENERATE_INFOPLIST_FILE = YES; 326 | INFOPLIST_FILE = "Custom Transitions/Info.plist"; 327 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 328 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 329 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 330 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 331 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 332 | LD_RUNPATH_SEARCH_PATHS = ( 333 | "$(inherited)", 334 | "@executable_path/Frameworks", 335 | ); 336 | MARKETING_VERSION = 1.0; 337 | PRODUCT_BUNDLE_IDENTIFIER = "com.jordanMorgan.Custom-Transitions"; 338 | PRODUCT_NAME = "$(TARGET_NAME)"; 339 | SWIFT_EMIT_LOC_STRINGS = YES; 340 | SWIFT_VERSION = 5.0; 341 | TARGETED_DEVICE_FAMILY = "1,2"; 342 | }; 343 | name = Debug; 344 | }; 345 | 339A9E022706B8E5006EC9D0 /* Release */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 349 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 350 | CODE_SIGN_STYLE = Automatic; 351 | CURRENT_PROJECT_VERSION = 1; 352 | DEVELOPMENT_TEAM = G6E6N7P5UN; 353 | GENERATE_INFOPLIST_FILE = YES; 354 | INFOPLIST_FILE = "Custom Transitions/Info.plist"; 355 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 356 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 357 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 358 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 359 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 360 | LD_RUNPATH_SEARCH_PATHS = ( 361 | "$(inherited)", 362 | "@executable_path/Frameworks", 363 | ); 364 | MARKETING_VERSION = 1.0; 365 | PRODUCT_BUNDLE_IDENTIFIER = "com.jordanMorgan.Custom-Transitions"; 366 | PRODUCT_NAME = "$(TARGET_NAME)"; 367 | SWIFT_EMIT_LOC_STRINGS = YES; 368 | SWIFT_VERSION = 5.0; 369 | TARGETED_DEVICE_FAMILY = "1,2"; 370 | }; 371 | name = Release; 372 | }; 373 | /* End XCBuildConfiguration section */ 374 | 375 | /* Begin XCConfigurationList section */ 376 | 339A9DE72706B8E4006EC9D0 /* Build configuration list for PBXProject "Custom Transitions" */ = { 377 | isa = XCConfigurationList; 378 | buildConfigurations = ( 379 | 339A9DFE2706B8E5006EC9D0 /* Debug */, 380 | 339A9DFF2706B8E5006EC9D0 /* Release */, 381 | ); 382 | defaultConfigurationIsVisible = 0; 383 | defaultConfigurationName = Release; 384 | }; 385 | 339A9E002706B8E5006EC9D0 /* Build configuration list for PBXNativeTarget "Custom Transitions" */ = { 386 | isa = XCConfigurationList; 387 | buildConfigurations = ( 388 | 339A9E012706B8E5006EC9D0 /* Debug */, 389 | 339A9E022706B8E5006EC9D0 /* Release */, 390 | ); 391 | defaultConfigurationIsVisible = 0; 392 | defaultConfigurationName = Release; 393 | }; 394 | /* End XCConfigurationList section */ 395 | }; 396 | rootObject = 339A9DE42706B8E4006EC9D0 /* Project object */; 397 | } 398 | --------------------------------------------------------------------------------