Last active
August 11, 2024 09:49
-
-
Save eoghain/7e9afdd43d1357fb8824126e0cbd491d to your computer and use it in GitHub Desktop.
UINavigationController that implements swipe to push/pop in an interactive animation. Just implement the InteractiveNavigation protocol on your ViewControllers you add to the nav stack to get custom transitions. Or implement a single animation and return it instead of the nil's in the UIViewControllerTransitioningDelegate and all transitions wil…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} | |
} | |
} |
@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?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@ 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 ?