8

I'm playing around with custom and interactive view controller transistion, with UIPercentDrivenInteractiveTransition. I'm building an app that present a card (other view controller) modally. I've made custom transistions with UIViewControllerAnimatedTransitioning that is animating the card view controller a lot like the standart model presentation style.

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    {...}
    let fullScreenFrame = transitionContext.finalFrame(for: cardVC)
    let offsetFrame = cardVC.view.frame.offsetBy(dx: 0, dy: cardVC.view.frame.height)
    let finalFrame = presenting ? fullScreenFrame : offsetFrame
    cardVC.view.frame = presenting ? offsetFrame : fullScreenFrame
    containerView.addSubview(cardVC.view)

    UIView.animateKeyframes(
        withDuration: transitionDuration(using: transitionContext),
        delay: 0,
        options: UIViewKeyframeAnimationOptions.calculationModeLinear,
        animations: {
            UIView.addKeyframe(
                withRelativeStartTime: 0,
                relativeDuration: 1,
                animations: { [weak self] in
                    guard let strongSelf = self else { return }
                    backgroundView.alpha = strongSelf.presenting ? 1 : 0
            })

            UIView.addKeyframe(
                withRelativeStartTime: 0,
                relativeDuration: 1,
                animations: {
                    cardVC.view.frame = finalFrame
            })
    }, completion: { finished in
        backgroundView.removeFromSuperview()
        gradientView.alpha = 1
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
    })
}

And then I use a pan gesture recognizer to interactively drive the dismiss animation:

func handleGesture(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
        let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
        var progress = (translation.y / (UIScreen.main.bounds.height - 70))
        progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))

        switch gestureRecognizer.state {
        case .began:
            interactionInProgress = true
            viewController.dismiss(animated: true, completion: nil)

        case .changed:
            shouldCompleteTransition = progress > 0.35
            update(progress)

        case .cancelled:
            interactionInProgress = false
            cancel()

        case .ended:
            interactionInProgress = false
            if !shouldCompleteTransition {
                cancel()
            } else {
                finish()
            }

        default:
            print("Unsupported")
        }
    }

When I dismiss the presented view controller by dragging it down, it doesn't seem to move linearly with the gesture. It seems like the Interaction Controller is using some easeInEaseOut function. Maybe setting UIPercentDrivenInteractiveTransition's timingCurve to make the transition run linearly. Is that posible, or am I getting something wrong?

Wiingaard
  • 4,150
  • 4
  • 35
  • 67
  • What a strange default behavior by Apple. Animations by default use a linear curve. However, percent-driven animations by default adopt what looks like an ease-in-out curve. The remedy is to explicitly define the curve as linear, which you never have to do anywhere except here. Odd. – trndjc Jun 26 '23 at 18:45

3 Answers3

2

You yourself have given the animation a default ease in - ease out timing curve, by not setting the timing curve to anything different in your call to UIView.animateKeyframes. That applies even during interaction.

Note that setting the animation's options to UIViewKeyframeAnimationOptions.calculationModeLinear does not change the timing curve (in case that's what you thought you were accomplishing here). The way to add a linear timing curve to a linear calculation mode keyframe animation is like this:

var opts : UIViewKeyframeAnimationOptions = .calculationModeLinear
let opt2 : UIViewAnimationOptions = .curveLinear
opts.insert(UIViewKeyframeAnimationOptions(rawValue:opt2.rawValue))
// and now use `opts` as your `options`
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • This is amazing because: 1) it works and 2) this is how we have to use Swift so many years later. Thanks @matt. – Ricky Nov 28 '22 at 04:37
2

I was having the same issue, was able to set a custom animation curve for animateKeyframes using UIView.setAnimationCurve like this:

UIView.animateKeyframes(withDuration: 4, delay: 0, options: [], animations: {

    UIView.setAnimationCurve(.linear)

    UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.24, animations: {
        // Animations...
    })

    // Other key frames...
})
Eduardo Arenas Prada
  • 1,072
  • 1
  • 7
  • 14
  • Yeah this is it! Not sure why but using `UIPercentDrivenInteractiveTransition` gives a really "bouncy" interpolation by default. Even though the docs say its curve is linear while interactive. Anyway, thank you – snakeoil Jan 25 '19 at 00:44
1

In my case, It works with interruptibleAnimator(using:) by return UIViewPropertyAnimator

user8637708
  • 3,303
  • 2
  • 13
  • 15