Changing a CALayer contents before animation finished - ios

I work on a UI component that implements flip-card clock animation. All works fine, but when I change a top CALayer contents to new image, the old image stays visible before changeding. It creates confusion effect. For better explanation I place the gif animation bellow:
This is code with changing a CALayer contents:
firstTopLayer.contents = secondTopLayer.contents
let bottomAnim = CABasicAnimation(keyPath: "transform")
bottomAnim.duration = animDuration/2
bottomAnim.repeatCount = 1
bottomAnim.fromValue = NSValue.init(caTransform3D:
CATransform3DMakeRotation((CGFloat)(M_PI_2), 1, 0, 0))
bottomAnim.toValue = NSValue.init(caTransform3D:
CATransform3DMakeRotation(0, 1, 0, 0))
bottomAnim.isRemovedOnCompletion = true
bottomAnim.timingFunction = CAMediaTimingFunction.init(name: kCAMediaTimingFunctionEaseIn)
firstBottomLayer.add(bottomAnim, forKey: "bottom")
firstBottomLayer.contents = self.bufferContents
For more information I place a link to the repository

I found a solution. Top animation must have this configuration
topAnim.fillMode = kCAFillModeForwards
topAnim.isRemovedOnCompletion = false
and after each start this animation.
firstTopLayer.removeAnimation(forKey: kTopAnimaton)
With this configuration the top layer stays in it last frame animation position

Related

Mosaic light show CAReplicatorLayer animation

I'm trying to achieve this mosaic light show effect for my background view with the CAReplicatorLayer object:
https://downloops.com/stock-footage/mosaic-light-show-blue-illuminated-pixel-grid-looping-background/
Each tile/CALayer is a single image that was replicated horizontally & vertically. That part I have done.
It seems to me this task is broken into at least 4 separate parts:
Pick a random tile
Select a random range of color offset for the selected tile
Apply that color offset over a specified duration in seconds
If the random color offset exceeds a specific threshold then apply a glow effect with the color offset animation.
But I'm not actually sure this would be the correct algorithm.
My current code was taken from this tutorial:
https://www.swiftbysundell.com/articles/ca-gems-using-replicator-layers-in-swift/
Animations are not my strong suite & I don't actually know how to apply continuous/repeating animation on all tiles. Here is my current code:
#IBOutlet var animationView: UIView!
func cleanUpAnimationView() {
self.animationView.layer.removeAllAnimations()
self.animationView.layer.sublayers?.removeAll()
}
/// Start a background animation with a replicated pattern image in tiled formation.
func setupAnimationView(withPatternImage patternImage: UIImage, animate: Bool = true) {
// Tutorial: https://www.swiftbysundell.com/articles/ca-gems-using-replicator-layers-in-swift/
let imageSize = patternImage.size.halve
self.cleanUpAnimationView()
// Animate pattern image
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame.size = self.animationView.frame.size
replicatorLayer.masksToBounds = true
self.animationView.layer.addSublayer(replicatorLayer)
// Give the replicator layer a sublayer to replicate
let imageLayer = CALayer()
imageLayer.contents = patternImage.cgImage
imageLayer.frame.size = imageSize
replicatorLayer.addSublayer(imageLayer)
// Tell the replicator layer how many copies (or instances) of the image needs to be rendered. But we won't see more than one since they are, per default, all rendered/stacked on top of each other.
let instanceCount = self.animationView.frame.width / imageSize.width
replicatorLayer.instanceCount = Int(ceil(instanceCount))
// Instance offsets & transforms is needed to move them
// 'CATransform3D' transform will be used on each instance: shifts them to the right & reduces the red & green color component of each instance's tint color.
// Shift each instance by the width of the image
replicatorLayer.instanceTransform = CATransform3DMakeTranslation(imageSize.width, 0, 0)
// Reduce the red & green color component of each instance, effectively making each copy more & more blue while horizontally repeating the gradient pattern
let colorOffset = -1 / Float(replicatorLayer.instanceCount)
replicatorLayer.instanceRedOffset = colorOffset
replicatorLayer.instanceGreenOffset = colorOffset
//replicatorLayer.instanceBlueOffset = colorOffset
//replicatorLayer.instanceColor = UIColor.random.cgColor
// Extend the original pattern to also repeat vertically using another tint color gradient
let verticalReplicatorLayer = CAReplicatorLayer()
verticalReplicatorLayer.frame.size = self.animationView.frame.size
verticalReplicatorLayer.masksToBounds = true
verticalReplicatorLayer.instanceBlueOffset = colorOffset
self.animationView.layer.addSublayer(verticalReplicatorLayer)
let verticalInstanceCount = self.animationView.frame.height / imageSize.height
verticalReplicatorLayer.instanceCount = Int(ceil(verticalInstanceCount))
verticalReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, imageSize.height, 0)
verticalReplicatorLayer.addSublayer(replicatorLayer)
guard animate else { return }
// Set both the horizontal & vertical replicators to add a slight delay to all animations applied to the layer they're replicating
let delay = TimeInterval(0.1)
replicatorLayer.instanceDelay = delay
verticalReplicatorLayer.instanceDelay = delay
// This will make the image layer change color
let animColor = CABasicAnimation(keyPath: "instanceRedOffset")
animColor.duration = animationDuration
animColor.fromValue = verticalReplicatorLayer.instanceRedOffset
animColor.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
animColor.autoreverses = true
animColor.repeatCount = .infinity
replicatorLayer.add(animColor, forKey: "colorshift")
let animColor1 = CABasicAnimation(keyPath: "instanceGreenOffset")
animColor1.duration = animationDuration
animColor1.fromValue = verticalReplicatorLayer.instanceGreenOffset
animColor1.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
animColor1.autoreverses = true
animColor1.repeatCount = .infinity
replicatorLayer.add(animColor1, forKey: "colorshift1")
let animColor2 = CABasicAnimation(keyPath: "instanceBlueOffset")
animColor2.duration = animationDuration
animColor2.fromValue = verticalReplicatorLayer.instanceBlueOffset
animColor2.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
animColor2.autoreverses = true
animColor2.repeatCount = .infinity
replicatorLayer.add(animColor2, forKey: "colorshift2")
}
let imageSize = patternImage.size.halve
and
animColor.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
both generated errors.
I removed the halve and commented-out the animColor lines and the code runs and animates. I could not get ANY replicator layer to display or animate at all (not even the most basic apple or tutorial code) until I used your code. Thank you so much!

Animate View with multiple arguments

I am trying to animate the opacity of a CALayer but the values are more than one value to begin with and one to end with.
For example: I want it to animate throw these values: 0.0, 0.7, 0.3, 1.0, 0.5, 0.0
I also want the animation to repeat with auto reverse.
This is what I have for now:
let redLineAnimation = CABasicAnimation(keyPath: "opacity")
redLineAnimation.duration = 0.4
redLineAnimation.autoreverses = true
redLineAnimation.fromValue = 0.0
redLineAnimation.toValue = 1.0
redLineAnimation.repeatCount = Float.infinity
movingRedLineLayer.addAnimation(redLineAnimation, forKey: nil)
I am new to iOS development. What can I do? Thanks!
Take a look at CAKeyframeAnimation. That will give you what you want.
And if what you are animating is a view's layer, you could use the easier-to-use UIView keyframe based animation methods. (Core Animation is pretty tricky, and the docs are spotty)

How do I make this animation go counterclockwise?

I have already got part of my animation completed. What I am trying to do is create the line border animation like this, where the line travels across the top and right side of the rectangle. What I need in addition to this is creating another line that mirrors the line shown in the video (traveling across the left side then bottom of the rectangle),counterclockwise. I tried doing this using the -bezierPathByReversingPath(_:) function, but it made the origin for the line begin in the bottom left of the rectangle. I am not sure what to do, any help would be appreciated!
Here’s my code:
// Create layer
rightLine = CAShapeLayer()
rightLine.bounds = centerButton.bounds
rightLine.position = view.center
rightLine.path = UIBezierPath(rect: rightLine.bounds).CGPath
rightLine.lineWidth = 3
rightLine.strokeColor = UIColor(hex: "3D424E").CGColor
rightLine.fillColor = UIColor.clearColor().CGColor
rightLine.strokeStart = 0
rightLine.strokeEnd = 0
// Create animation
let rightStart = CABasicAnimation(keyPath: "strokeStart")
rightStart.toValue = 0
let rightEnd = CABasicAnimation(keyPath: "strokeEnd")
rightEnd.toValue = 0.5
rightLineGroup = CAAnimationGroup()
rightLineGroup.animations = [rightStart, rightEnd]
rightLineGroup.duration = 1.5
rightLineGroup.autoreverses = true
rightLineGroup.repeatCount = HUGE // repeat forever
self.view.layer.addSublayer(rightLine)
rightLine.addAnimation(rightLineGroup, forKey: nil)
Animating strokeStart from zero to zero does nothing.
You probably want to leave strokeEnd at 1.0 (the default) and animate strokeStart from 1.0 to 0.5. That should have the desired effect.

Keeping a background image in the center on animation

I have animation setup to resize the image to about 1.3 times it's original size. The animations and everything are working without a problem but the image is moving towards the top left. Which means that the position of the image is not centering upon resize. How do i solve this problem
These are the animations I setup
var borderWidth:CABasicAnimation = CABasicAnimation(keyPath: "borderWidth")
borderWidth.fromValue = 0
borderWidth.toValue = 5
borderWidth.repeatCount = Float.infinity
sender.layer.borderWidth = 0
var increaseButtonHeight:CABasicAnimation = CABasicAnimation(keyPath: "bounds.size.height")
increaseButtonHeight.fromValue = sender.frame.size.height
increaseButtonHeight.toValue = sender.frame.size.height * 1.3
var increaseButtonWidth: CABasicAnimation = CABasicAnimation(keyPath: "bounds.size.width")
increaseButtonWidth.fromValue = sender.frame.size.width
increaseButtonWidth.toValue = sender.frame.size.width * 1.3
var boom:CAAnimationGroup = CAAnimationGroup()
boom.animations = [borderWidth,increaseButtonWidth, increaseButtonHeight]
boom.repeatCount = Float.infinity
boom.duration = 0.5
boom.autoreverses = true
sender.layer.addAnimation(boom, forKey: "boom")
Do I need to setup a new animation for centering the button continuously as the animation happens?
Please help
Nikhil
Set the property contentsGravity of the layer to kCAGravityCenter

CATransaction: a view flashes on completion

I'm writing a little bit complex animation, which goes in 2 steps:
Change opacity to 0 of UIViews that are not need to be visible and move a UIImageView (which has alpha = 1) to another CGPoint (position).
Change opacity of another UIView to 1 and the opacity of the UIImageView from the previous step to 0, and then after the animation of this step is finished, remove UIImageView from superview.
I've done it this way:
The first step is done without an explicit CATransaction. These 2 animations just have beginTime set to CACurrentMediaTime(). And I'm applying changes to the views right after layer.addAnimation(...) call.
Everything works fine here.
In the second step implementation I call CATransaction.begin() at the beginning.
Inside begin/commit calls to CATransaction I create and add 2 CABasicAnimations to 2 different layers: one for changing the opacity from 0 to 1 (for UIView), and one for changing the opacity from 1 to 0 (for UIImageView). Both animations have beginTime set to CACurrentMediaTime() + durationOfThePreviousStep.
And right after CATransaction.begin() I call CATransaction.setCompletionBlock({...}), and in this completion block I apply changes to these two views: set their new alphas and remove UIImageView from superview.
The problem is, at the end of this whole animation the UIView that has alpha animated to 1 flashes, which means its alpha sets back to 0 (though I've set its alpha to 1 in the completion block) and right after this the completion block executes and its alpha goes up to 1 again.
Well, the question is, how to get rid of this flashing? Maybe this animation can be done in better way?
P.S. I'm not using UIView animations because I'm interested in custom timing functions for these animations.
EDIT 1:
Here's the code. I've deleted UIImageView alpha animation because it's not really necessary.
var totalDuration: CFTimeInterval = 0.0
// Alpha animations.
let alphaAnimation = CABasicAnimation()
alphaAnimation.keyPath = "opacity"
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0
alphaAnimation.beginTime = CACurrentMediaTime()
alphaAnimation.duration = 0.15
let alphaAnimationName = "viewsFadeOut"
view1.layer.addAnimation(alphaAnimation, forKey: alphaAnimationName)
view1.alpha = 0
view2.layer.addAnimation(alphaAnimation, forKey: alphaAnimationName)
view2.alpha = 0
view3.layer.addAnimation(alphaAnimation, forKey: alphaAnimationName)
view3.alpha = 0
view4.layer.addAnimation(alphaAnimation, forKey: alphaAnimationName)
view4.alpha = 0
// Image View moving animation.
// Add to total duration.
let rect = /* getting rect */
let newImagePosition = view.convertPoint(CGPoint(x: CGRectGetMidX(rect), y: CGRectGetMidY(rect)), fromView: timeView)
let imageAnimation = CABasicAnimation()
imageAnimation.keyPath = "position"
imageAnimation.fromValue = NSValue(CGPoint: imageView!.layer.position)
imageAnimation.toValue = NSValue(CGPoint: newImagePosition)
imageAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
imageAnimation.beginTime = CACurrentMediaTime()
imageAnimation.duration = 0.3
imageView!.layer.addAnimation(imageAnimation, forKey: "moveImage")
imageView!.center = newImagePosition
totalDuration += imageAnimation.duration
// Time View alpha.
CATransaction.begin()
CATransaction.setCompletionBlock {
self.timeView.alpha = 1
self.imageView!.removeFromSuperview()
self.imageView = nil
}
let beginTime = CACurrentMediaTime() + totalDuration
let duration = 0.3
alphaAnimation.fromValue = 0
alphaAnimation.toValue = 1
alphaAnimation.beginTime = beginTime
alphaAnimation.duration = duration
timeView.layer.addAnimation(alphaAnimation, forKey: "timeViewFadeIn")
/* imageView alpha animation is not necessary, so I removed it */
CATransaction.commit()
EDIT 2: Piece of code that cause the problem:
CATransaction.begin()
CATransaction.setCompletionBlock {
self.timeView.alpha = 1
}
let duration = 0.3
let alphaAnimation = CABasicAnimation()
alphaAnimation.keyPath = "opacity"
alphaAnimation.fromValue = 0.0
alphaAnimation.toValue = 1.0
alphaAnimation.duration = duration
timeView.layer.addAnimation(alphaAnimation, forKey: "timeViewFadeIn")
CATransaction.commit()
Maybe the problem is because the timeView has a UITextField and a UIScrollView with 4 subviews. I've tried to replace timeView with a snapshot of timeView (UIImageView), but that didn't help.
EDIT 3:
New code. With this code, timeView always has alpha = 1, and it also animates from 0 to 1.
CATransaction.begin()
CATransaction.setCompletionBlock {
self.imageView!.removeFromSuperview()
self.imageView = nil
}
let alphaAnimation = CABasicAnimation()
alphaAnimation.keyPath = "opacity"
alphaAnimation.fromValue = 0.0
alphaAnimation.toValue = 1.0
alphaAnimation.duration = 0.3
alphaAnimation.beginTime = beginTime
timeView.layer.addAnimation(alphaAnimation, forKey: "timeViewFadeIn")
timeView.alpha = 1.0
CATransaction.commit()
Just looking at your code, I would expect it to flash. Why? Because you have animated timeView's layer's opacity from 0 to 1, but you have not set it to 1 (except in the completion handler, which will happen later). Thus, we animate the presentation layer from 0 to 1 and then the animation ends and it is revealed that the opacity of the real layer was 0 all along.
So, set timeView's layer's opacity to 1 before your animation gets going. Also, since you are using a delayed beginTime, you will need to set your animation's fillMode to "backwards".
I was able to get good results by modifying your test code to be self-contained and to look like this; there is a delay, the view fades in, and there is no flash at the end:
CATransaction.begin()
let beginTime = CACurrentMediaTime() + 1.0 // arbitrary, just testing
let alphaAnimation = CABasicAnimation()
alphaAnimation.keyPath = "opacity"
alphaAnimation.fromValue = 0.0
alphaAnimation.toValue = 1.0
alphaAnimation.duration = 1.0 // arbitrary, just testing
alphaAnimation.fillMode = "backwards"
alphaAnimation.beginTime = beginTime
timeView.layer.addAnimation(alphaAnimation, forKey: "timeViewFadeIn")
timeView.layer.opacity = 1.0
CATransaction.commit()
There are some other things about your code that I find rather odd. It is somewhat risky using a transaction completion block in this way; I don't see why you don't give your animation a delegate. Also, you are reusing alphaAnimation multiple times; I can't recommend that. I would create a new CABasicAnimation for each animation, if I were you.

Resources