UIView custom transition snaps back on completion - swift

I have implemented a class BubbleAnimator, that should create a bubble-like transition between views and added it through the UIViewControllerTransitioningDelegate-protocol. The presenting animation works fine so far (that's why I haven't added all the code for this part).
But on dismissing the view, the 'fromViewController' flashes up at the very end of the animation. After this very short flash, the correct toViewController is displayed again, but this glitch is very annoying.
The following is the relevant animateTransition-method:
//Get all the necessary views from the context
let containerView = transitionContext.containerView()
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
//Presenting
if self.reverse == false {
//Add the destinationvc as subview
containerView!.addSubview(fromViewController!.view)
containerView!.addSubview(toViewController!.view)
/*...Animating the layer goes here... */
//Dismissing
} else {
containerView!.addSubview(toViewController!.view)
containerView!.addSubview(fromViewController!.view)
//Init the paths
let circleMaskPathInitial = UIBezierPath(ovalInRect: self.originFrame)
let extremePoint = CGPoint(x: originFrame.origin.x , y: originFrame.origin.y - CGRectGetHeight(toViewController!.view.bounds) )
let radius = sqrt((extremePoint.x*extremePoint.x) + (extremePoint.y*extremePoint.y))
let circleMaskPathFinal = UIBezierPath(ovalInRect: CGRectInset(originFrame, -radius, -radius))
//Create a layer
let maskLayer = CAShapeLayer()
maskLayer.path = circleMaskPathFinal.CGPath
fromViewController!.view.layer.mask = maskLayer
//Create and add the animation
let animation = CABasicAnimation(keyPath: "path")
animation.toValue = circleMaskPathInitial.CGPath
animation.fromValue = circleMaskPathFinal.CGPath
animation.duration = self.transitionDuration(transitionContext)
animation.delegate = self
maskLayer.addAnimation(animation, forKey: "path")
}
The cleanup takes place in the delegate method:
override public func animationDidStop(anim: CAAnimation, finished flag: Bool) {
self.transitionContext?.completeTransition(!(self.transitionContext?.transitionWasCancelled())!)
}
I guess, that I am doing something wrong with adding the views to the containerView, but I couldn't figure it out. Another possibility is, that the view's layer mask gets reset, when the function completeTransition is called.

Thanks to this blogpost I have finally been able to solve this problem. Short explanation:
The CAAnimation only manipulates the presentation-layer of the view, but does not change the model-layer. When the animation now finishes, it's value snaps back to the original and unchanged value of the model-layer.
Short and simply workaround:
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
The better solution, as it doesn't prevent the animation from being removed is to manually set the final value of the layer's position before the animation starts. This way, the model-layer is assigned the correct value:
maskLayer.path = circleMaskPathInitial.CGPath
//Create and add the animation below..

Related

How to animate sublayer contents?

I have a layer and want to create an animation for this layer which will update contents of one of sublayers. CAAnimation keyPath has a notation, like sublayers.layerName.propertyName to update some values of a sublayer but seems like it doesn't work with .contents property.
func rightStepAfter(_ t: Double) -> CAAnimation {
let rightStep = CAKeyframeAnimation(keyPath: "sublayers.right.contents")
rightStep.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
rightStep.keyTimes = [0, 1]
rightStep.values = [UIImage(named:"leftfoot")!.cgImage!, UIImage(named:"rightfoot")!.cgImage!]
rightStep.beginTime = t
rightStep.duration = stepDuration
rightStep.fillMode = .forwards
return rightStep
}
func leftStepAfter(_ t: Double) -> CAAnimation {
let leftStep = CAKeyframeAnimation(keyPath: "sublayers.left.opacity")
leftStep.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
leftStep.keyTimes = [0, 1]
leftStep.values = [0, 1]
leftStep.beginTime = t
leftStep.duration = stepDuration
leftStep.fillMode = .forwards
return leftStep
}
Here leftStepAfter creates correct animation which updates opacity of a sublayer and rightStepAfter doesn't update contents of a sublayer. If you remove sublayers.right. from the keyPath - animation will correctly change contents of a CURRENT layer. Project to check it and the original project.
Why my animation doesn't work and how to fix it?
Did not find an answer to my original question (via keypath), but solved my problem by creating several animations for each sublayer.

Animating CALayer [filters, compositionFilter, backFilters]

I need to animate one of [filters, compositionFilter, backFilters] on a CALayer in video composition I tried the following code on NSView layer to test if the code works in the first place
view.wantsLayer = true
view.layerUsesCoreImageFilters = true
if let filter = CIFilter.init(name: "CIGaussianBlur", parameters: ["inputRadius": 50]) {
let animation = CABasicAnimation.init(keyPath: "filters")
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.duration = 2 //for testing
animation.autoreverses = false
animation.fromValue = NSArray.init(array: [])
animation.toValue = NSArray.init(array: [filter])
view.layer.add(animation, forKey: "CIGaussianBlurAnimation")
}
but nothing happens, so I need to know what is the proper way to animate one/all of [filters, compositionFilter, backFilters] properties of CALayer, and whether or not it is supported on AVVideoComposition(on iOS 'I know it is not supported on iOS UIView CALayers')
I found 1/2 the solution
BUT the main question remains: "is there any way to enable CIFilter on CALayer in video composition on iOS"
it is indicated in the documentation, to update any attribute of the filter after is applied to the layer, you should use
layer.setValue(1, forKeyPath: "backgroundFilters.myFilter.inputRadius")
So it means to animate any filter, we should update the filter properties directly in the animation, so the following should be done
for compositingFilter property:
let animation = CABasicAnimation(keyPath: "compositingFilter.inputRadius")
and for a filter in filters property:
filter.name = "filterName"
layer.filters = [someFilters, filter, otherFilters]
let animation = CABasicAnimation(keyPath: "filters.filterName.inputRadius")
and for a filter in backgroundFilters property:
filter.name = "filterName"
layer.backgroundFilters = [someFilters, filter, otherFilters]
let animation = CABasicAnimation(keyPath: "backgroundFilters.filterName.inputRadius")
and set the animation properties, then add it to the layer layer.add(animation, forKey: "animationKey")
NOTE: animationKey could be any value, even a nil value, but reusing the value will remove the previous animation; even when is running

UIView replicate CAAnimation of another view, in real time?

So I've got a background view with a gradient sublayer, animating continuously to change the colors slowly. I'm doing it with a CATransaction, because I need to animate other properties as well:
CATransaction.begin()
gradientLayer.add(colorAnimation, forKey: "colors")
// other animations
CATransaction.setCompletionBlock({
// start animation again, loop forever
}
CATransaction.commit()
Now I want to replicate this gradient animation, let's say, for the title of a button for instance.
Note 1: I can't just "make a hole" in the button, if such a thing is possible, because I might have other opaque views between the button and the background.
Note 2: The gradient position on the button is not important. I don't want the text gradient to replicate the exact colors underneath, but rather to mimic the "mood" of the background.
So when the button is created, I add its gradient sublayer to a list of registered layers, that the background manager will update as well:
func register(layer: CAGradientLayer) {
let pointer = Unmanaged.passUnretained(layer).toOpaque()
registeredLayers.addPointer(pointer)
}
So while it's easy to animate the text gradient at the next iteration of the animation, I would prefer that the button starts animating as soon as it's added, since the animation usually takes a few seconds. How can I copy the background animation, i.e. set the text gradient to the current state of the background animation, and animate it with the right duration left and timing function?
The solution was indeed to use the beginTime property, as suggested by #Shivam Gaur's comment. I implemented it as follows:
// The background layer, with the original animation
var backgroundLayer: CAGradientLayer!
// The animation
var colorAnimation: CABasicAnimation!
// Variable to store animation begin time
var animationBeginTime: CFTimeInterval!
// Registered layers replicating the animation
private var registeredLayers: NSPointerArray = NSPointerArray.weakObjects()
...
// Somewhere in our code, the setup function
func setup() {
colorAnimation = CABasicAnimation(keyPath: "colors")
// do the animation setup here
...
}
...
// Called by an external class when we add a view that should replicate the background animation
func register(layer: CAGradientLayer) {
// Store a pointer to the layer in our array
let pointer = Unmanaged.passUnretained(layer).toOpaque()
registeredLayers.addPointer(pointer)
layer.colors = colorAnimation.toValue as! [Any]?
// HERE'S THE KEY: We compute time elapsed since the beginning of the animation, and start the animation at that time, using 'beginTime'
let timeElapsed = CACurrentMediaTime() - animationBeginTime
colorAnimation.beginTime = -timeElapsed
layer.add(colorAnimation, forKey: "colors")
colorAnimation.beginTime = 0
}
// The function called recursively for an endless animation
func animate() {
// Destination layer
let toLayer = newGradient() // some function to create a new color gradient
toLayer.frame = UIScreen.main.bounds
// Setup animation
colorAnimation.fromValue = backgroundLayer.colors;
colorAnimation.toValue = toLayer.colors;
// Update background layer
backgroundLayer.colors = toLayer.colors
// Update registered layers (iterate is a custom function I declared as an extension of NSPointerArray)
registeredLayers.iterate() { obj in
guard let layer = obj as? CAGradientLayer else { return }
layer.colors = toLayer.colors
}
CATransaction.begin()
CATransaction.setCompletionBlock({
animate()
})
// Add animation to background
backgroundLayer.add(colorAnimation, forKey: "colors")
// Store starting time
animationBeginTime = CACurrentMediaTime();
// Add animation to registered layers
registeredLayers.iterate() { obj in
guard let layer = obj as? CAGradientLayer else { return }
layer.add(colorAnimation, forKey: "colors")
}
CATransaction.commit()
}

How do I remove a CAShapeLayer and CABasicAnimation from my UIView?

This is my code:
#objc func drawForm() {
i = Int(arc4random_uniform(UInt32(formNames.count)))
var drawPath = actualFormNamesFromFormClass[i]
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 6
shapeLayer.frame = CGRect(x: -115, y: 280, width: 350, height: 350)
var paths: [UIBezierPath] = drawPath()
let shapeBounds = shapeLayer.bounds
let mirror = CGAffineTransform(scaleX: 1,
y: -1)
let translate = CGAffineTransform(translationX: 0,
y: shapeBounds.size.height)
let concatenated = mirror.concatenating(translate)
for path in paths {
path.apply(concatenated)
}
guard let path = paths.first else {
return
}
paths.dropFirst()
.forEach {
path.append($0)
}
shapeLayer.transform = CATransform3DMakeScale(0.6, 0.6, 0)
shapeLayer.path = path.cgPath
self.view.layer.addSublayer(shapeLayer)
strokeEndAnimation.duration = 30.0
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
shapeLayer.add(strokeEndAnimation, forKey: nil)
}
This code animates the drawing of the shapeLayer path, however I can't find anything online about removing this layer and stopping this basic animation or removing the cgPath that gets drawn... Any help would be greatly appreciated!
You said:
I can't find anything online about removing this layer ...
It is removeFromSuperlayer().
shapeLayer.removeFromSuperlayer()
You go on to say:
... and stopping this basic animation ...
It is removeAllAnimations:
shapeLayer.removeAllAnimations()
Note, this will immediately change the strokeEnd (or whatever property you were animating) back to its previous value. If you want to "freeze" it where you stopped it, you have to grab the presentation layer (which captures the layer's properties as they are mid-animation), save the appropriate property, and then update the property of the layer upon which you are stopping the animation:
if let strokeEnd = shapeLayer.presentation()?.strokeEnd {
shapeLayer.removeAllAnimations()
shapeLayer.strokeEnd = strokeEnd
}
Finally, you go on to say:
... or removing the cgPath that gets drawn.
Just set it to nil:
shapeLayer.path = nil
By the way, when you're browsing for the documentation for CAShapeLayer and CABasicAnimation, don't forget to check out the documentation for their superclasses, namely and CALayer and CAAnimation ยป CAPropertyAnimation, respectively. Bottom line, when digging around looking for documentation on properties or methods for some particular class, you often will have to dig into the superclasses to find the relevant information.
Finally, the Core Animation Programming Guide is good intro and while its examples are in Objective-C, all of the concepts are applicable to Swift.
You can use an animation delegate CAAnimationDelegate to execute additional logic when an animation starts or ends. For example, you may want to remove a layer from its parent once a fade out animation has completed.
Below code taken from a class that implements CAAnimationDelegate on the layer, when you call The fadeOut function animates the opacity of that layer and, once the animation has completed, animationDidStop(_:finished:) removes it from its superlayer.
extension CALayer : CAAnimationDelegate {
func fadeOut() {
let fadeOutAnimation = CABasicAnimation()
fadeOutAnimation.keyPath = "opacity"
fadeOutAnimation.fromValue = 1
fadeOutAnimation.toValue = 0
fadeOutAnimation.duration = 0.25
fadeOutAnimation.delegate = self
self.add(fadeOutAnimation,
forKey: "fade")
}
public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
self.removeFromSuperlayer()
}
}

Swift: Cant change CALayer background color in array

I am simply trying to change the background color of the last element of a CALayer array. Here is my entire View Class, however its only 2-3 lines that I actually try to access the last element of the CALayer.
Here is my progressViewClass and I put comments to where exactly my problem is:
class ProgressBarView: UIView {
//Variables for progress bar
var holdGesture = UILongPressGestureRecognizer()
let animation = CABasicAnimation(keyPath: "bounds.size.width")
var layerHolder = [CALayer]()
var widthIndex = CGPoint(x: 0, y: 0)
var nextXOffset = CGFloat(0.0)
var checkIfFull = CGFloat()
var newLayer : CALayer?
var progressBarAnimationDuration : CFTimeInterval = (MainController.sharedInstance.totalMiliSeconsToRecord / 10)
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
}
func startProgressBar(){
if(RecordingViewController().currentCameraMode == .recordingMode || RecordingViewController().currentCameraMode == .record1stClipMode) {
newLayer = CALayer()
newLayer?.frame = CGRect(x: nextXOffset, y: 0, width: 0, height: self.bounds.height)
newLayer?.backgroundColor = UIColor(red:0.82, green:0.01, blue:0.11, alpha:1.0).cgColor
//print("before \(nextXOffset)")
newLayer?.anchorPoint = widthIndex
animation.fromValue = 0
animation.toValue = self.bounds.width - nextXOffset
animation.duration = progressBarAnimationDuration - ((MainController.sharedInstance.miliSecondsPassed) / 10)
self.layer.addSublayer(newLayer!)
//print("Long Press Began")
newLayer?.add(animation, forKey: "bounds.size.width")
}
else{
stopProgressBar()
}
}
func stopProgressBar(){
if(RecordingViewController().currentCameraMode != .recordingMode){
pauseLayer(layer: newLayer!)
newLayer?.frame = (newLayer?.presentation()!.frame)!
nextXOffset = (newLayer?.frame.maxX)!
layerHolder.append(newLayer!)
print("Layerholder has elements : \(layerHolder.count)")
}
}
// HERE IS MY PROBLEM
func highlightLastLayer(){
print("in highlight last layer Layerholder has elements : \(layerHolder.count)")
// I CAN HIDE THE CALAYER SO I BELIEVE IM ACCESSING THE CORRECT LAYER
// layerHolder.last?.isHidden = true
// This is suppose to change the last element background color to blue but doesnt
layerHolder.last?.backgroundColor = UIColor.blue.cgColor
}
// ALSO MY PROBLEM
func unhighlightLastLayer(){
print("inside unhighlight last layer")
// I CAN HIDE THE CALAYER SO I BELIEVE IM ACCESSING THE CORRECT LAYER
//layerHolder.last?.isHidden = false
// Changes CALayer back to red
layerHolder.last?.backgroundColor = UIColor(red:0.82, green:0.01, blue:0.11, alpha:1.0).cgColor
}
//Function to pause the Progress Bar
func pauseLayer(layer : CALayer){
let pausedTime : CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
}
Simply put, I create a progressView object in my viewController and then call those functions based on certain button input. This view is essentially a progress bar that you'd see in many video recording applications to show how much you have recorded. In the highlightLastLayer, I am trying to grab the last element of the "layerHolder" array and change its color to blue. Simple right? Doesn't work. Any ideas?
Where are you calling highlight and unhighlight. I am pretty sure you are doing this when you are "stopped" or "paused" because thats the only time you add anything to the layerHolder Array. You can only do this when you are not animating because when you are animating the presentation layer is shown instead of the "real" layer. Instead of setting the speed to zero, setup your layer to look like the current animation state and call layer. removeAllAnimations to kill the presentation layer and show the actual layer for your view instead. Now you can make all the changes that you want and they will actually show up.