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
}
}
}
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
completeTransitionwhether it completed or not: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:
if the gesture,
stateis.began, I will:UIPercentDrivenInteractiveTransitionthat theUIViewControllerTransitioningDelegatewill return frominteractionControllerForPresentation(using); andpresent/dismissUIPercentDrivenInteractiveTransitionwhen the gesture updates with
stateof.changed, justupdatetheUIPercentDrivenInteractiveTransition; andwhen it’s done, you either
finishorcancelit. Andcancelwill trigger the reverse animation automatically.For example, my left to right “dismiss” gesture ends up looking like:
Personally, for
finishvs.cancellogic, I check whether either:was the
velocityin the direction of the gesture (in a left-to-right “dismiss” gesture, that means that I check to see if it was positivexvelocity) ... that means that if a user flicks, even a small percent of the screen, I’ll still consider that an intent to completely the transition;only if
velocitywas zero do I check the percent complete (e.g. if they go ¾ the way across, stop, and let go, I’m assuming they want tofinishthe gesture, but if they only go ¼ the way across and stop and let go, I’m assuming they intended to cancel the gesture.needless to say, if they reverse the direction of their gesture, I consider that a cancelation of their intent to complete transition
But as you can see, I don’t do anything but
canceland the animation reverses automatically. So, here I start the dismissal transition from the second view controller, back to the first, butcanceltheUIPercentDrivenInteractiveTransition(in this case, by reversing the direction of the gesture and letting go) and my animator will, in its completion handler, pass the appropriateBoolvalue tocompleteTransitionin its completion handler and it automatically animates back with no work on my part: