-
-
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 onpresentAnimation
and 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.