-
-
Save eoghain/7e9afdd43d1357fb8824126e0cbd491d to your computer and use it in GitHub Desktop.
| class TutorialAnimation: NSObject, UIViewControllerAnimatedTransitioning { | |
| // MARK: - Animations | |
| // Basic push in animation, override for specifics | |
| func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
| guard let fromVC = transitionContext.viewController(forKey: .from) as? TutorialViewController, | |
| let toVC = transitionContext.viewController(forKey: .to) as? TutorialViewController else { | |
| return transitionContext.completeTransition(false) | |
| } | |
| let container = transitionContext.containerView | |
| // Force views to layout | |
| toVC.view.setNeedsLayout() | |
| toVC.view.layoutIfNeeded() | |
| fromVC.view.setNeedsLayout() | |
| fromVC.view.layoutIfNeeded() | |
| // Transformations | |
| let distance = container.frame.width | |
| let offScreenRight = CGAffineTransform(translationX: distance, y: 0) | |
| let offScreenLeft = CGAffineTransform(translationX: -distance, y: 0) | |
| var toStartTransform = offScreenRight | |
| var fromEndTransform = offScreenLeft | |
| if toVC.pageIndex < fromVC.pageIndex { | |
| toStartTransform = offScreenLeft | |
| fromEndTransform = offScreenRight | |
| } | |
| toVC.view.transform = toStartTransform | |
| // add views to our view controller | |
| container.addSubview(fromVC.view) | |
| container.addSubview(toVC.view) | |
| // get the duration of the animation | |
| let duration = transitionDuration(using: transitionContext) | |
| // perform the animation! | |
| UIView.animate(withDuration: duration, animations: { | |
| toVC.view.transform = .identity | |
| fromVC.view.transform = fromEndTransform | |
| }, completion: { _ in | |
| if transitionContext.transitionWasCancelled { | |
| toVC.view.removeFromSuperview() | |
| } else { | |
| fromVC.view.removeFromSuperview() | |
| } | |
| transitionContext.completeTransition(transitionContext.transitionWasCancelled == false) | |
| }) | |
| } | |
| // return how many seconds the transiton animation will take | |
| func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { | |
| return 0.5 | |
| } | |
| // Helper method to generate transform between 2 rects | |
| func transform(from: CGRect, toRect to: CGRect, keepAspectRatio: Bool) -> CGAffineTransform { | |
| var transform = CGAffineTransform.identity | |
| let xOffset = to.midX-from.midX | |
| let yOffset = to.midY-from.midY | |
| transform = transform.translatedBy(x: xOffset, y: yOffset) | |
| if keepAspectRatio { | |
| let fromAspectRatio = from.size.width/from.size.height | |
| let toAspectRatio = to.size.width/to.size.height | |
| if fromAspectRatio > toAspectRatio { | |
| transform = transform.scaledBy(x: to.size.height/from.size.height, y: to.size.height/from.size.height) | |
| } else { | |
| transform = transform.scaledBy(x: to.size.width/from.size.width, y: to.size.width/from.size.width) | |
| } | |
| } else { | |
| transform = transform.scaledBy(x: to.size.width/from.size.width, y: to.size.height/from.size.height) | |
| } | |
| return transform | |
| } | |
| #if DEBUG | |
| // MARK: - Debugging | |
| private var debugView = [UIView]() | |
| private var displayLink: CADisplayLink? | |
| @objc func animationDidUpdate(displayLink: CADisplayLink) { | |
| debugView.forEach { (view) in | |
| if let presentationLayer = view.layer.presentation() { | |
| print("🎦 \(view)\n👉 currentPosition: (midX: \(presentationLayer.frame.midX), midY: \(presentationLayer.frame.midY))\n👉 currentSize: \(presentationLayer.frame.size)") | |
| } | |
| } | |
| } | |
| func debug(_ view: UIView) { | |
| debugView.append(view) | |
| } | |
| func startDebugging() { | |
| let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate)) | |
| if #available(iOS 10.0, *) { | |
| displayLink.preferredFramesPerSecond = 60 | |
| } else { | |
| displayLink.frameInterval = 1 | |
| } | |
| displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.default) | |
| } | |
| func stopDebugging() { | |
| debugView.removeAll() | |
| displayLink?.remove(from: RunLoop.main, forMode: RunLoop.Mode.default) | |
| } | |
| #endif | |
| } |
| class TutorialViewController: UIViewController { | |
| // MARK: - Properties | |
| var pageIndex: Int = 0 | |
| var presentAnimation: UIViewControllerAnimatedTransitioning? = TutorialAnimation() | |
| var dismissAnimation: UIViewControllerAnimatedTransitioning? = TutorialAnimation() | |
| // MARK: IBOutlets | |
| @IBOutlet weak var pageControl: UIPageControl! | |
| @IBOutlet weak var backgroundImage: UIView! | |
| // MARK: - Initialization | |
| override func awakeFromNib() { | |
| super.awakeFromNib() | |
| setup() | |
| } | |
| func setup() { | |
| // Override me to set pageIndex, prevAnimationCoordinator, and nextAnimationCoordiantor | |
| } | |
| // MARK: - View Lifecycle | |
| override func viewWillAppear(_ animated: Bool) { | |
| super.viewWillAppear(animated) | |
| self.pageControl.currentPage = pageIndex | |
| // Hack to fix rotation issues | |
| self.rotateTopView(view: view) | |
| } | |
| // MARK: - Navigation | |
| func showNext() { | |
| performSegue(withIdentifier: "next", sender: self) | |
| } | |
| func showPrevious() { | |
| performSegue(withIdentifier: "unwind", sender: self) | |
| } | |
| override func prepare(for segue: UIStoryboardSegue, sender: Any?) { | |
| if segue.identifier == "next" { | |
| if let destinationVC = segue.destination as? TutorialViewController { | |
| destinationVC.delegate = delegate | |
| } | |
| } | |
| super.prepare(for: segue, sender: sender) | |
| } | |
| // MARK: - IBActions | |
| @IBAction func changePage(_ sender: UIPageControl) { | |
| let index = sender.currentPage | |
| if index <= self.pageIndex { | |
| showPrevious() | |
| } else { | |
| showNext() | |
| } | |
| } | |
| @IBAction func unwindToPreviousTutorial(_ sender: UIStoryboardSegue) { | |
| } | |
| } |
| // MARK: - | |
| // HACK to fix rotation with custom animations | |
| // https://forums.developer.apple.com/thread/11612 | |
| // Call in viewWillAppear of affected view controllers | |
| extension UIViewController { | |
| func rotateTopView(view:UIView) { | |
| if let superview = view.superview { | |
| rotateTopView(view: superview) | |
| } else { | |
| view.frame = UIWindow().frame | |
| } | |
| } | |
| } |
Could you provide an example of how a ViewController would implement the InteractiveNavigation protocol ?
Just added TutorialViewController.swift and TutorialAnimation.swift files to show how I've used this. No guarantee that they work since they are just copy/pasted from a project I implemented them in.
@eoghain awesome, thanks a lot !
I just set my root view controller on CustomInteractiveAnimationNavigationController. So now can you please let me know, on root view controller how can i push my second with just using swipe. I don't want to use segue or button click to go into next view controller ?
@MiteshiOS The CustomInteractiveAnimationNavigationController will call either the showPrevious or showNext methods when the user swipes. So you'll need to implement those methods to do the correct pushing/popping of the viewControllers. I used Segues because when I build this it was easiest to setup all of my screens in IB and just link them all together via segues. You should be able to do programatic push/pop calls just like any other NavigationController in those methods.
@ eoghain Ok thanks for the information. Just let me know is there any way so i can derived showPrevious or showNext methods into my root view controller ?
@eoghain this works really well, thank you.
However, if you have say a UITableView on the underlying view controller, then cell row swipes will not work. Ideally you want the cell swipe (actually a pan) to be recognized and handled before the nav controller. I've experimented with shouldBeRequiredtoFailBy etc. but without luck. Thoughts on how to support this?
My code doesn't have this line
animation = currentViewController.presentAnimation!. So I'm guessing that is code that you wrote, and the problem that you are having is that you are doing a force unwrap onpresentAnimationand in your currentViewController its nil. The!(force unwrap) operator in Swift is very dangerous and will cause this crash if the property you are unwrapping is actually nil.