Within an AnimateKeyFrames block, the wrong keyframe always executes first - ios

I've been working on this simple coin toss iOS app in Swift. I have an animateKeyFrames block to animate the motion of the coin.
The first keyframe causes the coin to go up, the second one assigns the image of a coin depending on the result of a random calculation and the third one brings the coin down.
However, for some reason, the image of the coin always changes first before even going up. It's like the second keyframe is executed first, and I have tried all permutations to no avail. I believe I've even entered the withRelativeStartTime parameter correctly.
What could be happening here?
let animationDuration = 1.5
UIView.animateKeyframes(withDuration: animationDuration, delay: 0.0, options: [.calculationModeLinear], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.45, animations: {
self.imageView.center.y = self.imageView.center.y - 200
self.imageView.alpha = 0.0
})
UIView.addKeyframe(withRelativeStartTime: 3/7, relativeDuration: 0.1, animations: {
self.imageView.image = UIImage(named: tossRes)
})
UIView.addKeyframe(withRelativeStartTime: 4/7, relativeDuration: 0.45, animations: {
self.imageView.center.y = self.imageView.center.y + 200
self.imageView.alpha = 1.0
})
})

Related

UIView.animate with limited repeat and autoreverse glitches on a real device

I'm trying to make a simple scale animation of a button 1.0 -> 1.4, repeat 3 times and then stop, but I'm experiencing a glitch on a real device.
The code below works smoothly on a simulator, but on a real device (iPhone 12 Pro with iOS 16) it glitches and I can see the jump to scale 1.4 after animation is done, and then it jumps to scale 1.0 on completion where I set transform = .identity.
I've suspected UIView.modifyAnimations, but the same problem happens with the now deprecated UIView.setAnimationRepeatCount(3).
Here is the video of the glitch on the real device:
https://i.imgur.com/x1LJ9Ox.mp4
UIView.animate(withDuration: 0.5, delay: 0, animations: {
UIView.modifyAnimations(withRepeatCount: 3, autoreverses: true) {
self.titleButton.transform = CGAffineTransformMakeScale(1.4, 1.4)
// self.titleButton.layoutIfNeeded() // doesn't change anything
}
}, completion: { _ in
self.titleButton.transform = .identity
})
I've tested on my iphone 13 iOS 16, it glitches like you said above.
Maybe I'm wrong or misunderstand something but looks like something is changing in modifyAnimations(withRepeatCount:autoreverses:animations:).
In simulator, after the animation in modifyAnimations is done, it immediately go to completion. If in completion do nothing with the view, it will turn back to CGAffineTransformMakeScale(1.4, 1.4). But if the view has transform anything, it will turn to that transform.
But in real device, after the animation in modifyAnimations is done, the view will immediately turn back to the final of animation which here is CGAffineTransformMakeScale(1.4, 1.4) ( the simulator doesn't have this step) then after that go to completion. This cause the glitches like you said.
I have a workaround for this situation
UIView.animate(withDuration: 0.5, animations: {
// stop at the half of the final repeat means no autoreverses accur at the end
UIView.modifyAnimations(withRepeatCount: 2.5, autoreverses: true) {
self.secondView.transform = CGAffineTransformMakeScale(1.4, 1.4)
}
}, completion: { _ in
// make final scale back here
UIView.animate(withDuration: 0.5, animations: {
self.secondView.transform = .identity
})
})
It looks like iOS 16 is updating the transform 1 event loop after the animation resulting in the glitch. One way to work around this is to leave the scale at 1.0 at the end of the animation itself by using a keyframe animation (which I found at this answer) and adding the code to repeat 3 times:
UIView.animateKeyframes(withDuration: 1, delay: 0, options: [], animations: {
UIView.modifyAnimations(withRepeatCount: 3, autoreverses: false) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
self.titleButton.transform = CGAffineTransformMakeScale(1.4, 1.4)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
self.titleButton.transform = .identity
}
}
}) { (isFinished) in
}

Using UIView.animateKeyframes to animate CAShapeLayer properties

I'm struggling to understand why the following key-framed animation doesn't perform as I expect. Here minuteClock is a subclassed UIView that is also a child view of the main view. minuteClock's horizontal position is set via an exposed constraint. minuteClock's own layer has a CAShapeLayer sublayer called borderLayer.
I'm trying to simultaneously animate the location of minuteClock and the fillcolor of that grandchild borderLayer. What I'm finding is that the keyframes work for the positioning but are ignored for the CAShapeLayer. That is, while the minuteClock moves to 100 half-way through the overall duration, I don't see borderLayer's fillcolor turn red. Instead it seems to just animates very quickly (like a quarter-second) to green.
UIView.animateKeyframes(withDuration: 2.0,
delay: 0,
options: .calculationModeLinear,
animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0,
relativeDuration: 0.5,
animations: {
self.minuteClockPositionConstraint.constant = 100
self.minuteClock.borderLayer.fillColor = UIColor.red.cgColor
self.view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.5,
relativeDuration: 0.5,
animations: {
self.minuteClockPositionConstraint.constant = 50
self.minuteClock.borderLayer.fillColor = UIColor.green.cgColor
self.view.layoutIfNeeded()
})
}, completion: nil)
I think I'm probably going about this wrong, mixing up keyframed animation at the view level with implicit animation triggered by changing a property on a CAShapeLayer.
Any suggestions as to how I should be doing this?

animateKeyframes with repeat and delay does not work as expected

I'm creating a simple left to right animation for a label using key frames but when the animation repeats, the delay is ignored.
The first time it executes, the delay of 3 seconds has an effect, but when the animation repeats, the delay is ignored. This causes the animation to re-start immediately after it ends.
UIView.animateKeyframes(withDuration: 10, delay: 3, options: [.calculationModePaced, .repeat], animations: {
let xDist = self.Label_ArtistAlbum2.frame.origin.x
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = self.Label_ArtistAlbum2.frame.origin.x - (xDist * 0.1)
})
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
}, completion: nil)
I've tried adding an extra keyframe at the end however this has no effect even with the altered times:
UIView.animateKeyframes(withDuration: 10, delay: 3, options: [.calculationModePaced, .repeat], animations: {
let xDist = self.Label_ArtistAlbum2.frame.origin.x
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = self.Label_ArtistAlbum2.frame.origin.x - (xDist * 0.1)
})
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.7, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
//attempted pause - does not appear to work perhaps since the position is unchanged?
UIView.addKeyframe(withRelativeStartTime: 0.8, relativeDuration: 0.2, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
}, completion: nil)
If the delay will not be repeated along with the rest of the animation, how can I create a pause before the entire animation repeats?
I had a similar problem for animating a loading view. I solved it this way:
I created an enum for the steps in the animation
private enum TriangleToAnimate {
case one
case two
case three
case pause
}
I have my variables
private var triangleViewToFireCount = TriangleToAnimate.one
var triangle1View : TriangleView
var triangle2View : TriangleView
var triangle3View : TriangleView
I start a timer to run each animation
override init(frame: CGRect) {
Timer.scheduledTimer(timeInterval: 0.33, target: self, selector: #selector(LoadingView.timerFire), userInfo: nil, repeats: true)
}
For the selector I have a fire method. In the method I have a switch for each of the enum cases
func timerFire(){
let anim = createAnimation()
switch triangleViewToFireCount {
case .one:
triangle1View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .two
case .two:
triangle2View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .three
case .three:
triangle3View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .pause
default:
triangleViewToFireCount = .one
}
}
This is the code how I created the animation as a keyframe
func createAnimation() -> CAKeyframeAnimation{
let tr = CATransform3DIdentity
let orignalScale = CATransform3DScale(tr, 1, 1, 1)
let doubleScale = CATransform3DScale(tr, 2, 2, 1)
let keyAn = CAKeyframeAnimation(keyPath: "transform")
keyAn.keyTimes = [0, 0.1, 0.6]
keyAn.duration = 1
keyAn.values = [orignalScale,doubleScale,orignalScale]
keyAn.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
return keyAn
}
I've done a lot of testing on this issue and have found what I believe is the problem.
I think that the animation option for the Curve is my issue.
In my case choosing
calculationModePaced
has the effect of recalculating my keyframe parameters and not guaranteeing to hit any of them except the beginning and end. All intermediate keyframes become 'suggestions'.
You can't create a pause at the end because the keyframe is 'consumed' when recalculating and does not stand on it's own.
I changed to calculationModeLinear and got the keyframes I expected, and the pause too.
However, it is as smooth as I would like so I'll have to keep tinkering...
Apple's docs are here - they're descriptive but could really use some graphics and/or examples:
https://developer.apple.com/reference/uikit/uiview?language=swift
A good graph of the curves can be found here: https://www.shinobicontrols.com/blog/ios7-day-by-day-day-11-uiview-key-frame-animations

Changing opacity during keyframe animation Swift

I am trying to animate a few circles and changing their opacities simultaneously.
UIView.animateKeyframesWithDuration(2, delay: 0, options: .Repeat, animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: {
self.innerRingView.alpha = 1
})
UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.5, animations: {
self.innerRingView.alpha = 0
self.middleRingView.alpha = 1
})
UIView.addKeyframeWithRelativeStartTime(1, relativeDuration: 0.5, animations: {
self.middleRingView.alpha = 0
self.outterRingView.alpha = 1
})
UIView.addKeyframeWithRelativeStartTime(1.5, relativeDuration: 0.5, animations: {
self.outterRingView.alpha = 0
})
}, completion: nil)
For some reason, it never gets to the second animation:
UIView.addKeyframeWithRelativeStartTime(1, relativeDuration: 0.5, animations: {
self.middleRingView.alpha = 0
self.outterRingView.alpha = 1
})
Because of this, my animation isn't work. innerRingView never goes back to 0 opacity and outterRingView never appears (all the views are set to 0 alpha by default).
What seems to be the problem here?
Issue appeared to be with your key frame relative start time
UIView.addKeyframeWithRelativeStartTime
which must be be in the range 0 to 1, where 0 represents the start of the overall animation and 1 represents the end of the overall animation. For example, for an animation that is two seconds in duration, specifying a start time of 0.5 causes the animations to begin executing one second after the start of the overall animation.

UIView scale animation overshoots when changed

When I animate a change to a view's transform, then reset that change in another animation before the first animation finishes, everything's great (shown here with a rotation). The animation smoothly switches to the new target:
But when I do this with a scale, the animation overshoots magnificently:
Here's the breaking code:
UIView.animateWithDuration(1) {
self.someView.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1)
}
UIView.animateWithDuration(1,
delay: 0.5,
options: nil,
animations: {
self.someView.layer.transform = CATransform3DIdentity
}, completion: nil
)
Has anyone else seen this? Am I doing something wrong?
EDIT: And is there a good workaround?
EDIT 2: I believe this is a duplicate of this question and am voting to close.
This blog post provides the answer: in iOS 8, UIView animations are additive, and this has an unfortunate result with scale animations.
Basically, the second animation happens together with the first animation. The only solution is to explicitly remove the original animation before starting a new one:
view.layer.transform = view.layer.presentationLayer().transform
view.layer.removeAllAnimations()
Hi I'm not quite sure what your looking for but if you want the view to go back to it's original scale you'd add the .Autoreverse flag.
UIView.animateWithDuration(1, delay: 0, options: .Autoreverse | .Repeat, animations: {
myView.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1)
}, completion: nil)
While if you wanted to string animations together I'd do it within UIView.animateKeyframesWithDuration()
UIView.animateKeyframesWithDuration(2, delay: 0.0, options: nil, animations: {
UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 0.5, animations: {
// Animation 1
})
UIView.addKeyframeWithRelativeStartTime(1, relativeDuration: 0.5, animations: {
// Animation 2
})
}, completion: nil)

Resources