-
-
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 | |
} | |
} | |
} |
@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?
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 ?