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
}
}
}
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:
if the gesture, state
is .began
, I will:
UIPercentDrivenInteractiveTransition
that the UIViewControllerTransitioningDelegate
will return from interactionControllerForPresentation(using)
; andpresent
/dismiss
UIPercentDrivenInteractiveTransition
when the gesture updates with state
of .changed
, just update
the UIPercentDrivenInteractiveTransition
; and
when it’s done, you either finish
or cancel
it. And cancel
will trigger the reverse animation automatically.
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:
was the velocity
in the direction of the gesture (in a left-to-right “dismiss” gesture, that means that I check to see if it was positive x
velocity) ... 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 velocity
was zero do I check the percent complete (e.g. if they go ¾ the way across, stop, and let go, I’m assuming they want to finish
the 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 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: