Development environment:
- Swift Language Version: Swift 5.0
- Xcode: Version 11.1
- Deployment Target: 11.2
Idea:
Today’s article I will share about how to use custom transition animation for subviews (also called sub views). We will use container view for animations to avoid potential errors when we animating subviews and mainview simultaneously.
- Create snapshots of the subviews from “from” view and then hide those subviews.
- Create snapshots of the “views” subviews and then hide those subviews.
- Convert all frame’s values to container view coordinates and add all snapshots to the container view.
- Start “to” snapshots with alpha = 0 (fade in)
- Animate the changes to “to” snapshots from alpha = 0 to 1.
- Also animate the “from” snapshots to the final position of the “to” view and animate their alpha values from 1 to 0 (fade out). Combine with step 4 to create a cross dissolve effect.
- Once all the above steps have been done, remove all snapshots and unhide the subviews that their snapshots are animated.
Step 1: Initialize the screen
Step 2: Initialize protocols
1 2 3 4 5 6 7 8 |
protocol CustomTransitionOriginator { var fromAnimatedSubviews: [UIView] { get } } protocol CustomTransitionDestination { var toAnimatedSubviews: [UIView] { get } } |
- fromAnimatedSubviews : contains subviews of “from” views that are animated.
- toAnimatedSubviews : contains subviews of “to” views that are animated.
Step 3: Extension UIView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
extension UIView { func zo_snapshot() -> UIImage? { UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, UIScreen.main.scale) let context = UIGraphicsGetCurrentContext() if let context = context { layer.render(in: context) } let snapshot = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return snapshot } func snapshotView() -> UIView? { if let snapshotImage = zo_snapshot() { return UIImageView(image: snapshotImage) } else { return nil } } } |
- The zo_snapshot () function returns a snapshot of type UIImage.
- The snapshotView () function returns the snapshotView of type UIView.
Step 4: Subviews animation transition
UIKit allows customizing view controller’s presentation through UIViewControllerAnimatedTransitioning delegate with two main functions:
- transitionDuration (using:): The function takes information about the time the transition animations takes place. The return value must be the same as the value you used to configure animations in the animateTransition function (using:) .
- animateTransition (using:): Function that allows config transition animations and is called when presenting or dismissing view controller.
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 |
class SubviewAnimationController: NSObject, UIViewControllerAnimatedTransitioning { enum TransitionType { case present case dismiss } let type: TransitionType init(type: TransitionType) { self.type = type super.init() } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 1.0 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let fromVC = transitionContext.viewController(forKey: .from) as! CustomTransitionOriginator & UIViewController let toVC = transitionContext.viewController(forKey: .to) as! CustomTransitionDestination & UIViewController let container = transitionContext.containerView // add the "to" view to the hierarchy if type == .present { container.addSubview(toVC.view) } else { container.insertSubview(toVC.view, belowSubview: fromVC.view) } toVC.view.layoutIfNeeded() // Create snapshots of label being animated let fromSnapshots = fromVC.fromAnimatedSubviews.map { subview -> UIView in // Create snapshot let snapshot = subview.snapshotView(afterScreenUpdates: false)! // We're putting it in container, so convert original frame into container's coordinate space snapshot.frame = container.convert(subview.frame, from: subview.superview) return snapshot } let toSnapshots = toVC.toAnimatedSubviews.map { subview -> UIView in // Create snapshot let snapshot = subview.snapshotView()! // we're putting it in container, so convert original frame into container's coordinate space snapshot.frame = container.convert(subview.frame, from: subview.superview) return snapshot } // save the "to" and "from" frames let frames = zip(fromSnapshots, toSnapshots).map { ($0.frame, $1.frame) } // move the "to" snapshots to where where the "from" views were, but hide them for now zip(toSnapshots, frames).forEach { snapshot, frame in snapshot.frame = frame.0 snapshot.alpha = 0 container.addSubview(snapshot) } // add "from" snapshots, too, but hide the subviews that we just snapshotted // associated textfield so we only see animated snapshots; we'll unhide these // original views when the animation is done. fromSnapshots.forEach { container.addSubview($0) } fromVC.fromAnimatedSubviews.forEach { $0.alpha = 0 } toVC.toAnimatedSubviews.forEach { $0.alpha = 0 } // I'm going to push the the main view from the right and dim the "from" view a bit, // but you'll obviously do whatever you want for the main view, if anything if type == .present { toVC.view.transform = .init(translationX: toVC.view.frame.width, y: 0) } else { toVC.view.alpha = 0.5 } // do the animation UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { // animate the snapshots of the textfield zip(toSnapshots, frames).forEach { snapshot, frame in snapshot.frame = frame.1 snapshot.alpha = 1 } zip(fromSnapshots, frames).forEach { snapshot, frame in snapshot.frame = frame.1 snapshot.alpha = 0 } // I'm now animating the "to" view into place, but you'd do whatever you want here if self.type == .present { toVC.view.transform = .identity fromVC.view.alpha = 0.5 } else { fromVC.view.transform = .init(translationX: fromVC.view.frame.width, y: 0) toVC.view.alpha = 1 } }, completion: { _ in // get rid of snapshots and re-show the original labels fromSnapshots.forEach { $0.removeFromSuperview() } toSnapshots.forEach { $0.removeFromSuperview() } fromVC.fromAnimatedSubviews.forEach { $0.alpha = 1 } toVC.toAnimatedSubviews.forEach { $0.alpha = 1 } // clean up "to" and "from" views as necessary, in my case, just restore "from" view's alpha fromVC.view.alpha = 1 fromVC.view.transform = .identity // complete the transition transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) } } |
Inside:
- fromView : is the view that appears at the beginning of the transition, or at the end of the cancel transition. For example, when presenting, fromView is the view of viewController1, while when dismissing, fromView is the view of viewController2.
- toView : is the view that appears at the end of the completed transition.
- container : acts as a superview containing the views involved in the transition.
- TransitionType : enum represents types of transitions.
- completeTransition () : The function informs the system that the transition animation has completed.
Step 5: Custom navigation controller
1 2 3 4 5 6 7 8 9 10 11 |
class CustomNavigationController: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if operation == .push { return SubviewAnimationController(type: .present) } else { return SubviewAnimationController(type: .dismiss) } } } |
- The navigationController (_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) will return the view controller’s presentation in the event of a push or dismiss.
Step 6: MainViewController and DetailViewController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
final class ViewController: UIViewController { @IBOutlet weak var textField1: UITextField! override func viewDidLoad() { super.viewDidLoad() } @IBAction func handleTapButton(_ sender: Any) { let vc = storyboard?.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController navigationController?.pushViewController(vc, animated: true) } } extension ViewController: CustomTransitionOriginator { var fromAnimatedSubviews: [UIView] { return [textField1] } } extension ViewController: CustomTransitionDestination { var toAnimatedSubviews: [UIView] { return [textField1] } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
final class DetailViewController: UIViewController { @IBOutlet weak var textField2: UITextField! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } } extension DetailViewController: CustomTransitionDestination { var toAnimatedSubviews: [UIView] { return [textField2] } } extension DetailViewController: CustomTransitionOriginator { var fromAnimatedSubviews: [UIView] { return [textField2] } } |
The two viewController apply CustomTransitionOriginator and CustomTransitionDestination to select subviews that are animated when custom transitions are executed.
Result
References
https://stackoverflow.com/questions/46596481/view-controller-transition-animate-subview-position
Link github
https://github.com/oNguyenDucHuyB/CustomTransitionVC/tree/navigationTransition