0
votes

I'm trying to build a custom view controller transition which is interactive and interruptible with these APIs:

  • UIViewControllerAnimatedTransitioning
  • UIPercentDrivenInteractiveTransition
  • UIViewControllerTransitioningDelegate
  • UIViewPropertyAnimator

What I want to achieve is that I can present a view controller modally, and then use UIPanGestureRecognizer to dismiss the presented view controller by dragging it downward. If I release my finger in the upper half of the screen, the transition should be cancelled, otherwise the transition will be completed successfully.

Here is the code about the problem:

    func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) {
        let translation = gestureRecognizer.translation(in: presentedViewController.view)
        switch gestureRecognizer.state {
        case .began:
            interacting = true
            presentingViewController.dismiss(animated: true) {
                print("Dismissal Completion Callback is Called.")
                // How can I know the dismissal is successful or cancelled.
            }
        case .changed:
            let fraction = (translation.y / UIScreen.main.bounds.height)
            update(fraction)
        case .ended, .cancelled:
            interacting = false
            if (percentComplete > 0.5) {
                finish()
            } else {
                cancel()
            }
        default:
            break
        }
    }

My code works great on the aspect of UI and interaction, but I don't understand the behavior of function func dismiss(animated flag: Bool, completion: (() -> Void)? = nil).

In the .began case of Pan Gesture, presentingViewController.dismiss(animated: true) { ... } is called, so the custom transition starts. But the completion callback is always called no mater the dismissal transition is cancelled or not.

I watched these videos of WWDC:

enter image description here

They use an example code to demonstrate custom transition with UINavigationController and do not mention the dismissal callback.

presentingViewController.dismiss(animated: true) {
        debugPrint("Dismissal Completion Called")
        debugPrint("[ presentedViewController.transitionCoordinator?.isCancelled \(self.presentedViewController.transitionCoordinator?.isCancelled) ]")
}

In the document about the completion parameter:

completion

The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.

The Question

What is the real meaning of Completion since it is always called after custom transition is cancelled or finished ?

When I use custom transition with presentation and dismissal, what's best practice of handling the real dismissal completion to update UI and data ?

2

2 Answers

0
votes

After a little research and testing -- yeah, I'd say this is a bit confusing.

The completion block is NOT called after the VC has been dismissed. Rather, it is called after the function returns.

So, assuming you are implementing UIPercentDrivenInteractiveTransition, .dismiss() triggers your transition code, and returns when you cancel() or finish() -- but its completion block has no knowledge of what you actually did with the transition.

I'm sure there are a various approaches to this... but my first thought would be to put your "completion code" in case .ended, .cancelled: where you are (already) determining whether or not to remove the VC (whether to call .cancel() or .finish()).

0
votes

Finally, I find something helpful in the document of Apple:

At the end of a transition animation, it is critical that you call the completeTransition: method. Calling that method tells UIKit that the transition is complete and that the user may begin to use the presented view controller. Calling that method also triggers a cascade of other completion handlers, including the one from the presentViewController:animated:completion: method and the animator object’s own animationEnded: method. The best place to call the completeTransition: method is in the completion handler of your animation block.

Because transitions can be canceled, you should use the return value of the transitionWasCancelled method of the context object to determine what cleanup is required. When a presentation is canceled, your animator must undo any modifications it made to the view hierarchy. A successful dismissal requires similar actions.

So the completion callback of present(_:animated:completion:) and dismiss(animated:completion:) do not have any parameters to indicate whether the transition is finished or cancelled. They are both called if the transitionContext.completeTransition(_:) method is called while transition is finished or cancelled. Such behavior is deliberately designed.