Xcode 10 Sim: Problem with Custom Transitions

Running Xcode 10 seed (10A254a).


I have VC A that modally presents another VC B.

VC B is a TabBarVC with a UITableVC as its default (first) tab.

I have a custom transition between VC A and VC B that allows users to use a pan gesture to swipe between the two VCs - swipe left on VC A to show B and vice-versa.

Problem is that on Xcode 10, on any simulator (tried with iPhone XR, X and 6), and with any OS (tried iOS 12 & 10.3) at some point transition from B back to A stops working (dismissing VC B not working) and all UI is stuck. It won't happen consistently but after some back-and-forth is gets stuck.

  • 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 disbaled
    • on view debugger main view - I can see VC A's subviews drawn
    • on view debugger left tree view - I can see the view hierarcgy of VC B.

Seems like the custom transition is in some kind of wierd state.

I haven't been able to recreate on actual device - seems all is working OK but haven't been able to test on XR with iOS 12 of course.

Anyone encounter something similar?

OK, Have narrowed it down to this:


in my custom UIViewControllerAnimatedTransitioning class that animates VC B to move to the right and reveal VC A I have the following code for the animateTransition function:

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: {
            //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
        })
        
    }

(I added the prints just for debug).

Problem is that the UIView.animate(withDuration:animations:completion) never completes - the completion closure is never called.

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 transition 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.

I have the same issue on one of my open source libraries: https://github.com/andreamazz/BubbleTransition

As described in this issue: https://github.com/andreamazz/BubbleTransition/issues/49 the controller is dismissed, you see no changes n screen, but if you inspect the views, everything seems fine. Not sure how to solve this.

Xcode 10 Sim: Problem with Custom Transitions
 
 
Q