iosuiviewanimationtransition

Trigger animation on Cancel when UIPercentDrivenInteractiveTransition ends half


Is it a good practice to force custom transition back to original position for a view controller if the interaction ends and animation is cancelled. I have the following implementation which decides when to dismiss the view controller on pan gesture. If the pan gesture ends earlier, I was expecting to animate back to the original position like how it was presented before considering the duration to be proportional to the progress value on pan gesture

protocol AnimationControllerDelegate: AnyObject {
  func shouldHandlePanelInteractionGesture() -> Bool
}

typealias PanGestureHandler = AnimationControllerDelegate & UIViewController & Animatable

final class CustomInteractionController: UIPercentDrivenInteractiveTransition, UIGestureRecognizerDelegate {
    var interactionInProgress: Bool = false
    private var shouldCompleteTransition: Bool = false
    private var startTransitionY: CGFloat = 0
    private var panGestureRecognizer: UIPanGestureRecognizer?
    private weak var viewController: PanGestureHandler?

    func wireToViewController(viewController: Any) {
        guard let viewControllerDelegate = viewController as? PanGestureHandler else {
            return
        }

        self.viewController = viewControllerDelegate

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGestureRecognizer(_:)))

        panGestureRecognizer = panGesture
        panGestureRecognizer?.delegate  = self

        self.viewController?.view.addGestureRecognizer(panGesture)
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                           shouldRecognizeSimultaneouslyWith
        otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }

    @objc
    func handlePanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) {
        guard let childView = gestureRecognizer.view,
            let parentView = childView.superview,
            let panGestureHandler = viewController else {
            return
        }

        switch gestureRecognizer.state {
        case .began:
            break
        case .changed:
            let translation = gestureRecognizer.translation(in: parentView)
            let velocity = gestureRecognizer.velocity(in: parentView)
            let state = gestureRecognizer.state

            if !panGestureHandler.shouldHandlePanelInteractionGesture() && percentComplete == 0 {
                return
            }

            let verticalMovement = translation.y / childView.bounds.height
            let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
            let downwardMovementPercent = fminf(downwardMovement, 1.0)
            let progress = CGFloat(downwardMovementPercent)

            let alphaValue = (1 - progress) * 0.4

            panGestureHandler.shadowView.backgroundColor = Safety.Colors.backgroundViewColor(for: alphaValue)

            if abs(velocity.x) > abs(velocity.y) && state == .began {
                return
            }

            if !interactionInProgress {
                interactionInProgress = true
                startTransitionY = translation.y
                viewController?.dismiss(animated: true, completion: nil)
            } else {
                shouldCompleteTransition = progress > 0.3
                update(progress)
            }
        case .cancelled:
            interactionInProgress = false
            startTransitionY = 0
            cancel()
        case .ended:
            interactionInProgress = false
            startTransitionY = 0
            if !shouldCompleteTransition {
                // Can I call a custom transition here back to original position?
                cancel()
            } else {
                finish()
            }
        case .failed:
            interactionInProgress = false
            startTransitionY = 0
            cancel()
        default:
            break
        }
    }
}

Solution

  • Is it a good practice to force custom transition back to original position for a view controller if the interaction ends and animation is cancelled.

    Yes, I think it is good practice to reverse it if the user cancels their gesture. But you don’t have to “force” it. You complete the transition, simply indicating whether you want it to complete or reverse. So, if you cancel the animation, it automatically reverses and goes back to where it was automatically for you. You don’t have to do anything but cancel.

    This presumes, of course, that in your animation completion block in your animator, that you indicate in your completeTransition whether it completed or not:

    UIView.animate(withDuration: 0.25, animations: {
        ...
    }, completion: { _ in
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
    })
    

    Thus, the animation will be completed if it wasn’t canceled. But it will reverse if it was canceled.


    Personally, in my gesture recognizer, I generally do the following:

    For example, my left to right “dismiss” gesture ends up looking like:

    @objc func handleLeftToRightPan(_ gesture: UIPanGestureRecognizer) {
        let percent = (gesture.translation(in: gesture.view).x) / gesture.view!.bounds.width
    
        switch gesture.state {
        case .began:
            interactionController = UIPercentDrivenInteractiveTransition()
            dismiss(animated: true)
            interactionController?.update(percent)
    
        case .changed:
            interactionController?.update(percent)
    
        case .ended, .cancelled:
            let velocity = gesture.velocity(in: gesture.view).x
            if velocity > 0 || (velocity == 0 && percent > 0.5) {
                interactionController?.finish()
            } else {
                interactionController?.cancel()
            }
            interactionController = nil
    
        default:
            break
        }
    }
    

    Personally, for finish vs. cancel logic, I check whether either:

    But as you can see, I don’t do anything but cancel and the animation reverses automatically. So, here I start the dismissal transition from the second view controller, back to the first, but cancel the UIPercentDrivenInteractiveTransition (in this case, by reversing the direction of the gesture and letting go) and my animator will, in its completion handler, pass the appropriate Bool value to completeTransition in its completion handler and it automatically animates back with no work on my part:

    enter image description here