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