Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animating changes to presentingViewController during interactive dismissal

I'm working on a UIPresentationController subclass similar to Mail.app's open draft behavior. When a view controller is presented, it doesn't go all the way to the top and the presenting view controller scales down as if it were falling back.

The basic gist of it is below:

class CustomPresentationController : UIPresentationController {

    // Create a 40pt space above the view.
    override func frameOfPresentedViewInContainerView() -> CGRect {
        let frame = super.frameOfPresentedViewInContainerView()
        let insets = UIEdgeInsets(top: 40, left: 0, bottom: 0, right: 0)
        return UIEdgeInsetsInsetRect(frame, insets)
    }

    // Scale down when expanded is true, otherwise identity.
    private func setScale(expanded expanded: Bool) {

        if expanded {
            let fromMeasurement = presentingViewController.view.bounds.width
            let fromScale = (fromMeasurement - 30) / fromMeasurement
            presentingViewController.view.transform = CGAffineTransformMakeScale(fromScale, fromScale)
        } else {
            presentingViewController.view.transform = CGAffineTransformIdentity
        }

    }

    // Scale down alongside the presentation.
    override func presentationTransitionWillBegin() {
        presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ context in
            self.setScale(expanded: true)
        }, completion: { context in
            self.setScale(expanded: !context.isCancelled())
        })
    }

    // Scale up alongside the dismissal.
    override func dismissalTransitionWillBegin() {
        presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ context in
            self.setScale(expanded: false)
        }, completion: { context in
            self.setScale(expanded: context.isCancelled())
        })
    }

    // Fix the scaled view's frame on orientation change.
    override func containerViewWillLayoutSubviews() {
        super.containerViewWillLayoutSubviews()
        guard let bounds = containerView?.bounds else { return }
        presentingViewController.view.bounds = bounds
    }
}

This works fine for a non-interactive presentation or dismissal. When performing an interactive dismissal, however, all animations on presentingViewController.view run non-interactively. That is, the scaling will happen in the ~300ms it normally takes to dismiss rather than staying at 3% complete when 3% dismissed.

You can see this in a sample project is available on GitHub. and a video of the issue is on YouTube.

I've tried the following approaches but they all yield the same result:

  • A parallel animation as seen above.
  • Animating in the UIViewControllerAnimatedTransitioning.
  • Using a CABasicAnimation an manually adjusting the timing of the container view's layer.
like image 576
Brian Nickel Avatar asked Feb 17 '26 06:02

Brian Nickel


1 Answers

The problem was that presentingViewController is not a descendent of the presentation's containerView. UIPercentDrivenInteractiveTransition works by setting containerView.layer.speed to zero and setting containerView.layer.timeOffset to reflect the percent complete. Since the view in question was not part of the hierarchy, its speed stayed at 1 and it completed as normal.

This is explicitly stated in the documentation for animateAlongsideTransition(_:,completion:):

Use this method to perform animations that are not handled by the animator objects themselves. All of the animations you specify must occur inside the animation context’s container view (or one of its descendants). Use the containerView property of the context object to get the container view. To perform animations in a view that does not descend from the container view, use the animateAlongsideTransitionInView:animation:completion: method instead.

As the documentation indicates, switching to animateAlongsideTransitionInView(_:,animation:,completion:) fixes the problem:

// Scale up alongside the dismissal.
override func dismissalTransitionWillBegin() {
    presentingViewController.transitionCoordinator()?.animateAlongsideTransitionInView(presentingViewController.view, animation: { context in
        self.setScale(expanded: false)
    }, completion: { context in
        self.setScale(expanded: context.isCancelled())
    })
}

The comment on that method in the header is a lot more direct about this than the documentation:

// This alternative API is needed if the view is not a descendent of the container view AND you require this animation to be driven by a UIPercentDrivenInteractiveTransition interaction controller.

like image 104
Brian Nickel Avatar answered Feb 18 '26 19:02

Brian Nickel



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!