Xcode 10: Custom Animated Transitions Stuck

1k views Asked by At

My app has two View Controllers (VC A & B) and a custom transition between them.

When using a left-directional pan gesture on VC A, an interactive animated transition modally presents VC B over A sliding in from the right (right-to-left). To dismiss VC B, user can:

  • Use right-directional pan gesture: will trigger an interactive transition that will slide VC B back to the right and uncover VC A. The position of VC B is determined interactively by the pan gesture state. The interaction is 'driven' by a UIPercentDrivenInteractiveTransition object.
  • Use a "close" button on VC B navbar. This will trigger the custom transition (slide to right) with no interaction (just animated).

Problem is that testing on Xcode 10 Seed (build 10A254a) + iOS 12 Simulator (X or XR or XS) I can easily get to a state where the custom transition never completes and the UI is left hanging in a weird state:

  • UI is stuck on VC B and no gestures or taps work.
  • The app isn't stuck - I can see conosle logs still rolling and netwrok activity is working (no errors in the log)
  • Pausing the app in this stuck state I can see com.apple.main-thread is not stuck.
  • When I hit "Debug View Hierarchy" something weird happens: on sim screen I can still see VC B and all UI is disabled. On view debugger main view - I can see VC A's subviews drawn as if the transition is done. On view debugger left tree view - I can see the view hierarcgy of VC B.

This problem never appeared on any previous version of Xcode and/or iOS pre Xcode 10/iOS12.

This is my animateTransition method in my custom UIViewControllerAnimatedTransitioning

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    guard let fromVC = transitionContext.viewController(forKey: .from), let toVC = transitionContext.viewController(forKey: .to) else {
        transitionContext.completeTransition(false)
        return
    }

    let containterView = transitionContext.containerView
    containterView.insertSubview(toVC.view, belowSubview: fromVC.view)

    let bounds = fromVC.view.bounds
    var xOffsetMultiplier : CGFloat = 0.0
    var yOffsetMultiplier : CGFloat = 0.0

    switch direction {
    case .up:
        yOffsetMultiplier  = -1.0
    case .right:
        xOffsetMultiplier  = 1.0
    case .left:
        xOffsetMultiplier  = -1.0
    case .down:
        yOffsetMultiplier  = 1.0
    }

    print(xOffsetMultiplier,bounds.size.width,bounds.size.height )
    UIView.animate(withDuration: duration, animations: {
        print("animating...")
        //fromVC.navigationController?.navigationBar.alpha = 0.0
        fromVC.view.frame = fromVC.view.frame.offsetBy(dx: xOffsetMultiplier * bounds.size.width, dy: yOffsetMultiplier * bounds.size.height)
    }, completion: { finished in
        print("completed animation")
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        //fromVC.navigationController?.navigationBar.alpha = 1.0
    })

}

The prints are there just for debug.

This is the sequence that easily recreates the problem:

  1. Use pan gesture to start interactive transition from B back to A but never complete it - this will call cancel() on the UIPercentDrivenInteractiveTransition object + I can verify the animation is completed.
  2. Tap the 'close' button to invoke the non-interactive transition to dismiss B. B never dismisses and the custom animation never completes!

On a device I couldn't recreate this issue at all (yet) - and all transitions working as expected.

2

There are 2 answers

0
Shai Ben-Tovim On BEST ANSWER

So found the source of why transitions where stuck between VC B and A but still don't really understand what's different between Xcode9/iOS11 and Xcode10/iOS12 that produced the different behaviour.

To keep it short:

  • When using a pan gesture to initiate an interactive transition to dismiss VC B I allocate a UIPercentDrivenInteractiveTransition, call dismiss(animated:completion:) on the VC and update it according to the pan progress. In some cases, when the pan didn't traverse enough "ground" my gesture handler deems the transitions canceled and calls the cancel() method of UIPercentDrivenInteractiveTransition
  • After such a cancel, tapping the close button initiates a new dismiss(animated:completion:) but because the UIPercentDrivenInteractiveTransition is still allocated it is returned by my transition delegate and the OS actually attempts an interactive dismissal although that wasn't the intent. This is a bug on my part as after calling cancel I should also make sure the transition delegate doesn't attempt an interactive transition in this case (although on Xcode9/iOS11 it didn't).
  • The reason the transition is 'stuck' is because its an interactive transition with no updates (no gesture updates when tapping 'close'. I verified this by forcing a finish() on the mistakenly allocated UIPercentDrivenInteractiveTransition so it completes and everything is back to normal.

Making sure the dismiss transition is either interactive or not based on the user interaction, especially after canceling an interactive one, fixed the problem.

What I don't understand is why isn't this consistent behaviour between Xcode/iOS versions. This problem never ever happened to me before on any device or simulator. There's something different in the way custom animations/transitions are handled - nothing in the Apple docs that could explain this - perhaps in the internal implementation of the transition context.

From a naive "eye-test" it seems that transition animations on the Xcode10 simulator are slower in reaction time and less smooth than before but still doesn't fully explain it.

1
jose saad On

Try setting wantsInteractiveStart = false in your UIPercentDrivenInteractiveTransition