UIViewControllerAnimatedTransitioning动画错误导致的剩余视图

时间:2019-05-19 10:52:56

标签: animation uiviewcontroller uikit transition navigationcontroller

使用Xcode-10.2.1,iOS-12.3,Swift-5.0.1,

使用自定义UIViewControllerAnimatedTransitioning动画时,背景视图未知时我遇到一个奇怪的问题!

问题在于,每当自定义转换发生时,就会有一个彩色的剩余视图,该视图不应该存在!

在爆炸视图中,显然有一个未知的黄色视图可见。问题是为什么它首先出现在哪里,它从哪里来? (我认为是CircularCustomTranstion引起了问题-请继续阅读...)

enter image description here

以下是两个自定义转换视频,这些视频说明了未知黄色视图的问题。 (相应的代码可以在下面找到...):

背景应该是全黑的,但是有一个奇怪的未知黄色视图困扰着动画...

enter image description here

enter image description here

进一步的调查导致发现,只有当我以前使用CircularCustomTransition时,才会出现黄色的损坏视图(请参见下面的代码-originally taken from here)。

我发现,即使长时间使用此CircularCustomTransition的ViewController被解雇(并且内存不足),任何即将到来的Transition仍然会损坏。 (即,运行CircularCustomTransition就足够了,随后的所有过渡都会显示这种讨厌的不需要的背景视图)

我的问题:为什么使用以下CircularCustomTransition会破坏所有以下CustomTransition背景(在视频中)??

有人可以帮助我找到有关如何适当更改CircularCustomTransition代码的解决方案吗?

以下是展示正在运行的CircularCostomTransition的视频(即可以正常运行,但不幸的是,使用黄色视图会破坏以后的任何过渡):

enter image description here

这是CircularCustomTransition的代码(它错在哪里?)

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)
    }
}

出于完整性的原因,这是上面视频中显示的两个动画的代码...

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!")
    }
}

1 个答案:

答案 0 :(得分:0)

我终于找到了解决方法:

如果将以下两行添加到CircularTransition中,则可以正常使用!!!

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

(请在最下面显示的CircularTransition完整代码中找到另外两行...)

以下是将这两行添加到CircularTransition代码后的所有转换视频。您会看到所有黄色视图都按预期消失了!!

enter image description here

enter image description here

enter image description here

这是最终的TransitionTransition代码: (可以在SOLUTION注释中找到另外两行)

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)
    }
}