0

Using Xcode-10.2.1, iOS-12.3, Swift-5.0.1,

I have a strange problem with an unknown background-view when using custom UIViewControllerAnimatedTransitioning animations !

The problem is that whenever the custom-transition happens, there is a coloured left-over view that is not supposed to be there !

In the explosion-view, there is clearly an unknown yellow view visible. The question is why is it there in the first place and where is it coming from ? (I figured that a CircularCustomTranstion is causing the problem - keep on reading...)

enter image description here

Here are two custom-transition videos that illustrate the problem with the unknown yellow view. (the corresponding code can be found further down...):

The backgrounds should be all-black but there is a strange unknown yellow view bothering the animation...

enter image description here

enter image description here

Further investigation lead to the finding that the corrupt yellow view only occurs if I previously use a CircularCustomTransition (see code below - originally taken from here).

I found that even if the ViewControllers using this CircularCustomTransition are long dismissed (and out of memory), any upcoming Transition is still corrupted. (i.e. it is enough to run the CircularCustomTransition and all following transitions will show this nasty unwanted background-view)

My question: Why is the usage of the below CircularCustomTransition corrupting all following CustomTransition backgrounds (in videos) ??

Can anybody help me in finding a solution on how to change the CircularCustomTransition code appropriately ?

Here is the video showing the CircularCostomTransition in action (i.e. is to works but unfortunately causes any later transition to be corrupted with the yellow view):

enter image description here

Here is the code for the CircularCustomTransition (WHAT IS WRONG WITH IT?)

import UIKit

protocol CircleTransitionable {
    var triggerButton: UIButton { get }
    var contentTextView: UITextView { get }
    var mainView: UIView { get }
}

class CircularTransition: CustomAnimator {
    weak var context: UIViewControllerContextTransitioning?

    public override init(duration: TimeInterval = 0.25) {
        super.init(duration: duration)
    }

    // make this zero for now and see if it matters when it comes time to make it interactive
    override func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.0
    }

    override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromVC = transitionContext.viewController(forKey: .from) as? CircleTransitionable,
            let toVC = transitionContext.viewController(forKey: .to) as? CircleTransitionable,
            let snapshot = fromVC.mainView.snapshotView(afterScreenUpdates: false) else {
                transitionContext.completeTransition(false)
                return
        }

        context = transitionContext

        let containerView = transitionContext.containerView

        // Background View With Correct Color
        let backgroundView = UIView()
        backgroundView.frame = toVC.mainView.frame
        backgroundView.backgroundColor = fromVC.mainView.backgroundColor
        containerView.addSubview(backgroundView)

        // Animate old view offscreen
        containerView.addSubview(snapshot)
        fromVC.mainView.removeFromSuperview()
        animateOldTextOffscreen(fromView: snapshot)

        // Growing Circular Mask
        containerView.addSubview(toVC.mainView)
        animate(toView: toVC.mainView, fromTriggerButton: fromVC.triggerButton)

        // Animate Text in with a Fade
        animateToTextView(toTextView: toVC.contentTextView, fromTriggerButton: fromVC.triggerButton)
    }

    func animateOldTextOffscreen(fromView: UIView) {
        UIView.animate(withDuration: 0.25, delay: 0.0, options: [.curveEaseIn], animations: {
            fromView.center = CGPoint(x: fromView.center.x - 1000, y: fromView.center.y + 1500)
            fromView.transform = CGAffineTransform(scaleX: 5.0, y: 5.0)
        }, completion: { (finished) in
            self.context?.completeTransition(finished)
        })
    }
    func animate(toView: UIView, fromTriggerButton triggerButton: UIButton) {
        // Starting Path
        let rect = CGRect(x: triggerButton.frame.origin.x,
                          y: triggerButton.frame.origin.y,
                          width: triggerButton.frame.width,
                          height: triggerButton.frame.width)
        let circleMaskPathInitial = UIBezierPath(ovalIn: rect)

        // Destination Path
        let fullHeight = toView.bounds.height
        let extremePoint = CGPoint(x: triggerButton.center.x,
                                   y: fullHeight)
        let radius = sqrt((extremePoint.x*extremePoint.x) +
            (extremePoint.y*extremePoint.y))
        let circleMaskPathFinal = UIBezierPath(ovalIn: triggerButton.frame.insetBy(dx: -radius,
                                                                                   dy: -radius))

        // Actual mask layer
        let maskLayer = CAShapeLayer()
        maskLayer.path = circleMaskPathFinal.cgPath
        toView.layer.mask = maskLayer

        // Mask Animation
        let maskLayerAnimation = CABasicAnimation(keyPath: "path")
        maskLayerAnimation.fromValue = circleMaskPathInitial.cgPath
        maskLayerAnimation.toValue = circleMaskPathFinal.cgPath
        maskLayerAnimation.delegate = self
        maskLayer.add(maskLayerAnimation, forKey: "path")

        context?.completeTransition(true)
    }

    func animateToTextView(toTextView: UIView, fromTriggerButton: UIButton) {
        // Start toView offscreen a little and animate it to normal
        let originalCenter = toTextView.center
        toTextView.alpha = 0.0
        toTextView.center = fromTriggerButton.center
        toTextView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)

        UIView.animate(withDuration: 0.25, delay: 0.1, options: [.curveEaseOut], animations: {
            toTextView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
            toTextView.center = originalCenter
            toTextView.alpha = 1.0
        }, completion: { (finished) in
            self.context?.completeTransition(finished)
        })
    }
}

extension CircularTransition: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        context?.completeTransition(true)
    }
}

For completeness reasons, here is the code for the two animations shown in the videos above...

class CustomBounceUpAnimationController: CustomAnimator {

    override func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.5
    }

    override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
        let finalFrameForVC = transitionContext.finalFrame(for: toViewController)
        let containerView = transitionContext.containerView
        containerView.bringSubviewToFront(toViewController.view)

        let bounds = UIScreen.main.bounds
        toViewController.view.frame = finalFrameForVC.offsetBy(dx: 0.0, dy: bounds.size.height)
        // toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, -bounds.size.height)
        containerView.addSubview(toViewController.view)

        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear, animations: {
            fromViewController.view.alpha = 0.5
            toViewController.view.frame = finalFrameForVC
        }) {
            finished in
            transitionContext.completeTransition(true)
            fromViewController.view.alpha = 1.0
        }

        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear, animations: {
            fromViewController.view.alpha = 0.5
            toViewController.view.frame = finalFrameForVC
            }, completion: {
                finished in
                transitionContext.completeTransition(true)
                fromViewController.view.alpha = 1.0
        })
    }
}
class Custom3DAnimationController: CustomAnimator {

    var reverse: Bool = false

    override func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 1.5
    }

    override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
        let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
        if let toView = toViewController.view {
            containerView.subviews[0].backgroundColor = .clear
            containerView.bringSubviewToFront(toView)
            let fromView = fromViewController.view
            let direction: CGFloat = reverse ? -1 : 1
            let const: CGFloat = -0.005

            toView.layer.anchorPoint = CGPoint(x: direction == 1 ? 0 : 1, y: 0.5)
            fromView?.layer.anchorPoint = CGPoint(x: direction == 1 ? 1 : 0, y: 0.5)

            var viewFromTransform: CATransform3D = CATransform3DMakeRotation(direction * CGFloat(Double.pi/2), 0.0, 1.0, 0.0)
            var viewToTransform: CATransform3D = CATransform3DMakeRotation(-direction * CGFloat(Double.pi/2), 0.0, 1.0, 0.0)
            viewFromTransform.m34 = const
            viewToTransform.m34 = const

            containerView.transform = CGAffineTransform(translationX: direction * containerView.frame.size.width / 2.0, y: 0)
            toView.layer.transform = viewToTransform
            containerView.addSubview(toView)

            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                containerView.transform = CGAffineTransform(translationX: -direction * containerView.frame.size.width / 2.0, y: 0)
                fromView?.layer.transform = viewFromTransform
                toView.layer.transform = CATransform3DIdentity
            }, completion: {
                finished in
                containerView.transform = .identity
                fromView?.layer.transform = CATransform3DIdentity
                toView.layer.transform = CATransform3DIdentity
                fromView?.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
                toView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)

                if (transitionContext.transitionWasCancelled) {
                    toView.removeFromSuperview()
                } else {
                    fromView?.removeFromSuperview()
                }
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })
        }
    }
}
import UIKit

open class CustomAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    public enum TransitionType {
        case navigation
        case modal
    }

    let duration: TimeInterval

    public init(duration: TimeInterval = 0.25) {
        self.duration = duration        
        super.init()
    }

    open func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return self.duration
    }

    open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        fatalError("You have to implement this method for yourself!")
    }
}
iKK
  • 6,394
  • 10
  • 58
  • 131

1 Answers1

0

I finally found a solution:

If you add the following two lines to the CircularTransition, then it works !!!

// SOLUTION SOLUTION SOLUTION SOLUTION SOLUTION SOLUTION SOLUTION SOLUTION
// THE FOLLOWING TWO LINES HAVE BEEN ADDED !!!!
backgroundView.removeFromSuperview()
snapshot.removeFromSuperview()

(please find the two extra lines in the full code of CircularTransition shown at the very bottom...)

Here are all the transition videos after the two lines have been added to the CircularTransition code. You see that all yellow-views are gone now as expected !!

enter image description here

enter image description here

enter image description here

Here is the final CircularTransition code: (the two extra lines can be found with the SOLUTION comment)

import UIKit

protocol CircleTransitionable {
    var triggerButton: UIButton { get }
    var contentTextView: UITextView { get }
    var mainView: UIView { get }
}

class CircularTransition: CustomAnimator {
    weak var context: UIViewControllerContextTransitioning?

    public override init(duration: TimeInterval = 0.25) {
        super.init(duration: duration)
    }

    // make this zero for now and see if it matters when it comes time to make it interactive
    override func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.0
    }

    override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromVC = transitionContext.viewController(forKey: .from) as? CircleTransitionable,
            let toVC = transitionContext.viewController(forKey: .to) as? CircleTransitionable,
            let snapshot = fromVC.mainView.snapshotView(afterScreenUpdates: false) else {
                transitionContext.completeTransition(false)
                return
        }

        context = transitionContext

        let containerView = transitionContext.containerView

        // Background View With Correct Color
        let backgroundView = UIView()
        backgroundView.frame = toVC.mainView.frame
        backgroundView.backgroundColor = fromVC.mainView.backgroundColor
        containerView.addSubview(backgroundView)

        // Animate old view offscreen
        containerView.addSubview(snapshot)
        fromVC.mainView.removeFromSuperview()
        animateOldTextOffscreen(fromView: snapshot)

        // SOLUTION SOLUTION SOLUTION SOLUTION SOLUTION SOLUTION SOLUTION SOLUTION
        // THE FOLLOWING TWO LINES HAVE BEEN ADDED !!!!
        backgroundView.removeFromSuperview()
        snapshot.removeFromSuperview()


        // Growing Circular Mask
        containerView.addSubview(toVC.mainView)
        animate(toView: toVC.mainView, fromTriggerButton: fromVC.triggerButton)

        // Animate Text in with a Fade
        animateToTextView(toTextView: toVC.contentTextView, fromTriggerButton: fromVC.triggerButton)
    }

    func animateOldTextOffscreen(fromView: UIView) {
        UIView.animate(withDuration: 0.25, delay: 0.0, options: [.curveEaseIn], animations: {
            fromView.center = CGPoint(x: fromView.center.x - 1000, y: fromView.center.y + 1500)
            fromView.transform = CGAffineTransform(scaleX: 5.0, y: 5.0)
        }, completion: nil)
    }
    func animate(toView: UIView, fromTriggerButton triggerButton: UIButton) {
        // Starting Path
        let rect = CGRect(x: triggerButton.frame.origin.x,
                          y: triggerButton.frame.origin.y,
                          width: triggerButton.frame.width,
                          height: triggerButton.frame.width)
        let circleMaskPathInitial = UIBezierPath(ovalIn: rect)

        // Destination Path
        let fullHeight = toView.bounds.height
        let extremePoint = CGPoint(x: triggerButton.center.x,
                                   y: fullHeight)
        let radius = sqrt((extremePoint.x*extremePoint.x) +
            (extremePoint.y*extremePoint.y))
        let circleMaskPathFinal = UIBezierPath(ovalIn: triggerButton.frame.insetBy(dx: -radius,
                                                                                   dy: -radius))

        // Actual mask layer
        let maskLayer = CAShapeLayer()
        maskLayer.path = circleMaskPathFinal.cgPath
        toView.layer.mask = maskLayer

        // Mask Animation
        let maskLayerAnimation = CABasicAnimation(keyPath: "path")
        maskLayerAnimation.fromValue = circleMaskPathInitial.cgPath
        maskLayerAnimation.toValue = circleMaskPathFinal.cgPath
        maskLayerAnimation.delegate = self
        maskLayer.add(maskLayerAnimation, forKey: "path")
    }

    func animateToTextView(toTextView: UIView, fromTriggerButton: UIButton) {
        // Start toView offscreen a little and animate it to normal
        let originalCenter = toTextView.center
        toTextView.alpha = 0.0
        toTextView.center = fromTriggerButton.center
        toTextView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)

        UIView.animate(withDuration: 0.25, delay: 0.1, options: [.curveEaseOut], animations: {
            toTextView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
            toTextView.center = originalCenter
            toTextView.alpha = 1.0
        }, completion: nil)
    }
}

extension CircularTransition: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        context?.completeTransition(true)
    }
}
iKK
  • 6,394
  • 10
  • 58
  • 131