Search code examples
iosswiftuiviewcontrollerframeuiviewanimationtransition

UIViewController transition flickers on dismissal with pull-down gesture


I'm presenting a view controller when the user clicks on an annotation in an MKMapView using a custom transition. The transition animates the presented view controller's frame starting from the frame of the annotation to its final full-screen appearance.

When dismissing the presented view controller, the user has two options: She can click a close button in the top-right corner and the dismiss transition will animate the view back down to the frame of the annotation. This part works fine.

But the user can also drag the view down by at least 200 points and when she lifts her finger, I want the same dismiss transition to run, but starting from the "dragged down" state.

To go into a bit more detail, my presented view controller's root view has two subviews: one that I call contentView, that holds all actual contents and is the one that has a UIPanGestureRecognizer defined on it and gets moved down according to the user's gestures; and one that I call pullProgressView that lies beneath the contentView and stays in place as the user drags. It displays a circle that fills up the further down the user has dragged already to indicate at which point releasing the finger would dismiss the view controller.

The essential part of the gesture recognizer looks as follows:

let progress = sender.translation(in: contentView).y
let screen = UIScreen.main.bounds

if sender.state == .began || sender.state == .changed {
    if progress > 0 {
        // Dragged down -> allow movement of the view
        contentView.center = CGPoint(x: contentView.center.x,
                                     y: screen.size.height / 2 + progress)
    }
} else if sender.state == .ended {
    if contentView.frame.origin.y > threshold {
        // Dragged down far enough to dismiss the view controller
        print("contentView frame right before dismissing: \(contentView.frame)")
        self.dismissView()
    } else {
        // Not dragged down far enough, animate the view back
        // to its original position
        UIView.animate(withDuration: 0.3) {
            self.contentView.frame = screen
        }
    }
}

My animateTransition method that takes over as I call dismissView() contains the following code:

let contentView = (transitionContext.viewController(forKey: .from) as! PresentedViewController).contentView!
print("contentView frame going into animateTransition: \(contentView.frame)")

The problem is that as I lift up my finger to dismiss the view controller after having it dragged downward, the view "flickers" back to its original, undragged state. The two print statements from the above code snippets produce the following output:

contentView frame right before dismissing: (0.0, 330.0, 375.0, 667.0)
contentView frame going into animateTransition: (0.0, 0.0, 375.0, 667.0)

So the contentView's frame does not "survive" into the animateTransition method (notice the y parameters). I can set it manually from within animateTransition by asking the UIPanGestureRecognizer for its progress, which works, but there is still an ugly flickering before the contentView is moved back to its dragged down frame.

Does anyone have an idea how to solve this? Or a better approach to this problem? I've read about UIPercentDrivenInteractiveTransition, but is it applicable in my case? The dragging down gesture does not really produce any percentage of the actual dismiss animation (which would be to morph the frame back to the one of the map annotation), so I would not know how to solve the issue using UIPercentDrivenInteractiveTransition.


Solution

  • I found a different solution using view controller containment. Basically I add a child view controller and its view as a subview to the presenting view controller. Additionally I add the pullProgressView as a subview underneath the child view controller's view.

    This way I don't have to rely on a transitioning delegate but can instead handle the dismissing of the child view controller myself after I'm done animating everything.