I have implemented a custom interactive transition for detecting the middle screen swipe with the UIPanGestureRecognizer along with UIPercentDrivenInteractiveTransition and animating it with UIViewControllerAnimatedTransitioning.
For now, I have a problem when the UIPercentDrivenInteractiveTransition was cancelled but the UINavigationBar pop animation was still animated like this
Is there anyway to disable or cancel the animation when there are some situation like this?
Here is my code https://github.com/kanottonp/NavBarBugPOC
ViewController.swift
@IBOutlet weak var button: UIButton!
static var count = 1
private var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition!
private var panGestureRecognizer: UIPanGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
addGesture()
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.title = "Hello \(ViewController.count)"
ViewController.count += 1
}
@IBAction func onTouch(_ sender: Any) {
guard let newVC = storyboard?.instantiateViewController(withIdentifier: "ViewController") else {
return
}
self.navigationController?.pushViewController(newVC, animated: true)
}
private func addGesture() {
guard panGestureRecognizer == nil else {
return
}
guard self.navigationController?.viewControllers.count > 1 else {
return
}
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(_:)))
panGestureRecognizer.cancelsTouchesInView = true;
panGestureRecognizer.delaysTouchesBegan = true;
panGestureRecognizer.maximumNumberOfTouches = 1
self.view.addGestureRecognizer(panGestureRecognizer)
self.navigationController?.interactivePopGestureRecognizer?.delegate = panGestureRecognizer as? UIGestureRecognizerDelegate
}
@objc private func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
let percent = max(panGesture.translation(in: view).x, 0) / view.frame.width
switch panGesture.state {
case .began:
navigationController?.delegate = self
if panGesture.velocity(in: view).x > 0 {
_ = navigationController?.popViewController(animated: true)
}
case .changed:
if let percentDrivenInteractiveTransition = percentDrivenInteractiveTransition {
percentDrivenInteractiveTransition.update(percent)
}
case .ended:
let velocity = panGesture.velocity(in: view).x
// Continue if drag more than 50% of screen width or velocity is higher than 300
if let percentDrivenInteractiveTransition = percentDrivenInteractiveTransition {
if percent > 0.5 || velocity > 300 {
percentDrivenInteractiveTransition.finish()
} else {
percentDrivenInteractiveTransition.cancel()
}
}
case .cancelled, .failed:
percentDrivenInteractiveTransition.cancel()
default:
break
}
}
extension ViewController: UINavigationControllerDelegate
public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideAnimatedTransitioning()
}
public func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
navigationController.delegate = nil
if panGestureRecognizer.state == .began && panGestureRecognizer.velocity(in: view).x > 0 {
percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()
percentDrivenInteractiveTransition.completionCurve = .easeInOut
} else {
percentDrivenInteractiveTransition = nil
}
return percentDrivenInteractiveTransition
}
SlideAnimatedTransitioning.swift
extension SlideAnimatedTransitioning: UIViewControllerAnimatedTransitioning
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// use animator to implement animateTransition
let animator = interruptibleAnimator(using: transitionContext)
animator.startAnimation()
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if let propertyAnimator = propertyAnimator {
return propertyAnimator
}
let containerView = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
toView.frame = transitionContext.finalFrame(for: toViewController)
toView.frame = CGRect(x: toView.frame.origin.x, y: toView.frame.origin.y, width: toView.frame.size.width, height: toView.frame.size.height + toView.frame.origin.y)
let width = containerView.frame.width
var offsetLeft = fromView.frame
offsetLeft.origin.x = width
var offscreenRight = toView.frame
offscreenRight.origin.x = -width / 3.33;
toView.frame = offscreenRight;
toView.layer.opacity = 0.9
containerView.insertSubview(toView, belowSubview: fromView)
let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: UICubicTimingParameters(animationCurve: .easeInOut))
animator.addAnimations {
toView.frame = CGRect(x: fromView.frame.origin.x, y: toView.frame.origin.y, width: toView.frame.width, height: toView.frame.height)
fromView.frame = offsetLeft
toView.layer.opacity = 1.0
}
animator.addCompletion { (success) in
toView.layer.opacity = 1.0
fromView.layer.opacity = 1.0
fromViewController.navigationItem.titleView?.layer.opacity = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.propertyAnimator = nil
}
self.propertyAnimator = animator
return animator
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
if transitionContext?.transitionWasCancelled == true { return 0 }
return 2
}