I've implemented a Navigation controller to incorporate an rotating-disc type of layout (imagine each VC laid out in a circle, that rotates as a whole, into view sequentially. The navigation controller is configured to use a custom transition class, as below :-
import UIKit class RotaryTransition: NSObject, UIViewControllerAnimatedTransitioning { let isPresenting :Bool let duration :TimeInterval = 0.5 let animationDuration: TimeInterval = 0.7 let delay: TimeInterval = 0 let damping: CGFloat = 1.4 let spring: CGFloat = 6.0 init(isPresenting: Bool) { self.isPresenting = isPresenting super.init() } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { //Get references to the view hierarchy let fromViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)! let toViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)! let sourceRect: CGRect = transitionContext.initialFrame(for: fromViewController) let containerView: UIView = transitionContext.containerView if self.isPresenting { // Push //1. Settings for the fromVC ............................ // fromViewController.view.frame = sourceRect fromViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3); fromViewController.view.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3); //2. Setup toVC view........................... containerView.insertSubview(toViewController.view, belowSubview:fromViewController.view) toViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3); toViewController.view.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3); toViewController.view.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI / 180)); //3. Perform the animation............................... UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: { fromViewController.view.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI / 180)); toViewController.view.transform = CGAffineTransform(rotationAngle: 0); }, completion: { (animated: Bool) -> () in transitionContext.completeTransition(true) }) } else { // Pop //1. Settings for the fromVC ............................ fromViewController.view.frame = sourceRect fromViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3); fromViewController.view.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3); //2. Setup toVC view........................... // toViewController.view.frame = transitionContext.finalFrame(for: toViewController) toViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3); toViewController.view.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3); toViewController.view.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI / 180)); containerView.insertSubview(toViewController.view, belowSubview:fromViewController.view) //3. Perform the animation............................... UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: { fromViewController.view.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI / 180)); toViewController.view.transform = CGAffineTransform(rotationAngle: 0); }, completion: { //When the animation is completed call completeTransition (animated: Bool) -> () in transitionContext.completeTransition(true) }) } } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration; } }
A representation of how the views move is show in the illustration below... The two red areas are the problems, as explained later.
The presenting (push) translation works fine - 2 moves to 1 and 3 moves to 2. However, the dismissing (pop) translation does not, whereby the dismissing VC moves out of view seemingly correctly (2 moving to 3), but the presenting (previous) VC arrives either in the wrong place, or with an incorrectly sized frame...
With the class as-is, the translation results in 2 moving to 3 (correctly) but 1 then moves to 4, the view is correctly sized, yet seems offset, by a seemingly arbitrary distance, from the intended location. I have since tried a variety of different solutions.
In the pop section I tried adding the following line (commented in the code) :-
toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
...but the VC now ends up being shrunk (1 moves to 5). I hope someone can see the likely stupid error I'm making. I tried simply duplicating the push section to the pop section (and reversing everything), but it just doesn't work!
FYI... Those needing to know how to hookup the transition to a UINavigationController - Add the UINavigationControllerDelegate to your nav controller, along with the following function...
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { let transition: SwingTransition = SwingTransition.init(isPresenting: ( operation == .push ? true : false )) return transition; }
The diagram below shows how all views would share the same originating point (for the translation). The objective is to give the illusion of a revolver barrel moving each VC into view. The top centre view represents the viewing window, showing the third view in the stack. Apologies for the poor visuals...