swiftcore-animationcalayercabasicanimationuiviewanimationtransition

Using UIPercentDrivenInteractiveTransition with CABasicAnimation has weird glitch


I'm implementing custom transition using CABasicAnimation and UIView.animate both. Also need to implement a custom interactive transition using UIPercentDrivenInteractiveTransition which exactly copies the behavior of the native iOS swipe back. Animation without a back swipe gesture (when I'm pushing and popping by the back arrow) works fine and smoothly. Moreover, swipe back also works smoothly, except when the gesture velocity is more than 900

Gesture Recognition function:

@objc func handleBackGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
        guard animationTransition != nil else { return }
        switch gesture.state {
        case .began:
            interactionController = TransparentNavigationControllerTransitionInteractor(duration: anumationDuration)
            popViewController(animated: true)

        case .changed:
            guard let view = gesture.view?.superview else { return }
            let translation = gesture.translation(in: view)
            var percentage = translation.x / view.bounds.size.width
            percentage = min(1.0, max(0.0, percentage))
            shouldCompleteTransition = percentage > 0.5
            interactionController?.update(percentage)

        case .cancelled, .failed, .possible:
            if let interactionController = self.interactionController {
                isInteractiveStarted = false
                interactionController.cancel()
            }

        case .ended:
            interactionController?.completionSpeed = 0.999
            let greaterThanMaxVelocity = gesture.velocity(in: view).x > 800
            let canFinish = shouldCompleteTransition || greaterThanMaxVelocity
            canFinish ? interactionController?.finish() : interactionController?.cancel()
            interactionController = nil

        @unknown default: assertionFailure()
        }
    }

UIPercentDrivenInteractiveTransition class. Here I'm synchronizing layer animation.

final class TransparentNavigationControllerTransitionInteractor: UIPercentDrivenInteractiveTransition {

    // MARK: - Private Properties

    private var context: UIViewControllerContextTransitioning?
    private var pausedTime: CFTimeInterval = 0
    private let animationDuration: TimeInterval

    // MARK: - Initialization

    init(duration: TimeInterval) {
        self.animationDuration = duration * 0.4 // I dk why but layer duration should be less 
        super.init()
    }

    // MARK: - Public Methods

    override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
        super.startInteractiveTransition(transitionContext)
        context = transitionContext

        pausedTime = transitionContext.containerView.layer.convertTime(CACurrentMediaTime(), from: nil)
        transitionContext.containerView.layer.speed = 0
        transitionContext.containerView.layer.timeOffset = pausedTime
    }

    override func finish() {
        restart(isFinishing: true)
        super.finish()
    }

    override func cancel() {
        restart(isFinishing: false)
        super.cancel()
    }

    override func update(_ percentComplete: CGFloat) {
        super.update(percentComplete)
        guard let transitionContext = context else { return }
        let progress = CGFloat(animationDuration) * percentComplete
        transitionContext.containerView.layer.timeOffset = pausedTime + Double(progress)
    }

    // MARK: - Private Methods 

    private func restart(isFinishing: Bool) {
        guard let transitionLayer = context?.containerView.layer else { return }
        transitionLayer.beginTime = transitionLayer.convertTime(CACurrentMediaTime(), from: nil)
        transitionLayer.speed = isFinishing ? 1 : -1
    }
}

And here is my Dismissal animation function in UIViewControllerAnimatedTransitioning class


private func runDismissAnimationFrom(
        _ fromView: UIView,
        to toView: UIView,
        in transitionContext: UIViewControllerContextTransitioning) {

        guard let toViewController = transitionContext.viewController(forKey: .to) else { return }
        toView.frame = toView.frame.offsetBy(dx: -fromView.frame.width / 3, dy: 0)

        let toViewFinalFrame = transitionContext.finalFrame(for: toViewController)
        let fromViewFinalFrame = fromView.frame.offsetBy(dx: fromView.frame.width, dy: 0)

        // Create mask to hide bottom view with sliding
        let slidingMask = CAShapeLayer()
        let initialMaskPath = UIBezierPath(rect: CGRect(
            x: fromView.frame.width / 3,
            y: 0,
            width: 0,
            height: toView.frame.height)
        )
        let finalMaskPath = UIBezierPath(rect: toViewFinalFrame)
        slidingMask.path = initialMaskPath.cgPath
        toView.layer.mask = slidingMask
        toView.alpha = 0

        let slidingAnimation = CABasicAnimation(keyPath: "path")
        slidingAnimation.fromValue = initialMaskPath.cgPath
        slidingAnimation.toValue = finalMaskPath.cgPath
        slidingAnimation.timingFunction = .init(name: .linear)
        slidingMask.path = finalMaskPath.cgPath
        slidingMask.add(slidingAnimation, forKey: slidingAnimation.keyPath)

        UIView.animate(
            withDuration: duration,
            delay: 0,
            options: animationOptions,
            animations: {
                fromView.frame = fromViewFinalFrame
                toView.frame = toViewFinalFrame
                toView.alpha = 1
        },
            completion: { _ in
                toView.layer.mask = nil
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }

I note that glitch occurs only when a swipe has a grand velocity. Here a video with the result of smooth animation at normal speed and not smooth at high speed - https://youtu.be/1d-kTPlhNvE

UPD: I've already tried to use UIViewPropertyAnimator combine with interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating

But the result is another type of glitching.


Solution

  • I've solved the issue, just change a part of restart function:

            transitionLayer.beginTime = 
    transitionLayer.convertTime(CACurrentMediaTime(), from: nil) - transitionLayer.timeOffset
            transitionLayer.speed = 1
    

    I don't really understand why, but looks like timeOffset subtraction works!