Reputation: 5461
Currently I'm using a button to activate an UIView Animation and in it's completion block I'm using a CABasicAnimation to return it to it's starting location. The animation runs smoothly at first and returns to its original starting point but when I press the button again the animation automatically starts at the spot the original UIView Animation ended whereas it's suppose to start at the original starting point.
class ObjectCreateMoveViewController: UIViewController, CAAnimationDelegate {
let square = UIView()
override func viewDidLoad() {
super.viewDidLoad()
createSquare()
}
func createSquare() {
square.frame = CGRect(x: 10, y: 10, width: 100, height: 100)
square.backgroundColor = UIColor.red
square.layer.borderColor = UIColor.blue.cgColor
square.layer.borderWidth = 4
square.layer.cornerRadius = 2
view.addSubview(square)
}
func transformAnimation () {
let transAnimation = CABasicAnimation(keyPath: "transform.translation")
transAnimation.toValue = NSValue(cgPoint: CGPoint(x: 10, y: 10))
let rotAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotAnimation.toValue = -CGFloat.pi
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
scaleAnimation.toValue = NSNumber(value: 1)
let groupTransform = CAAnimationGroup()
groupTransform.duration = 3
groupTransform.delegate = self
groupTransform.beginTime = CACurrentMediaTime()
groupTransform.animations = [transAnimation, rotAnimation, scaleAnimation]
groupTransform.isRemovedOnCompletion = false
groupTransform.fillMode = kCAFillModeForwards
groupTransform.setValue("circle", forKey: "transform")
square.layer.add(groupTransform, forKey: "transform")
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if flag {
//should be (10,10)but when I printed out the ending positions it was (60,60)
square.layer.position.x = 60
square.layer.position.y = 60
print("This is the x point \(square.layer.position.x)")
print("This is the y point \(square.layer.position.y)")
}
}
@IBAction func transformButtonPressed(_ sender: Any) {
UIView.animate(withDuration: 3, delay: 0, options: .curveEaseInOut, animations: {
let translateTransform = CGAffineTransform(translationX: 150, y: 200)
self.square.transform = translateTransform.scaledBy(x: 1.5, y: 1.5).rotated(by: CGFloat.pi/2)
}, completion: { _ in
self.transformAnimation()
})
}
}
Upvotes: 1
Views: 1227
Reputation: 77691
David Rönnqvist gave you a really good description to work from, but if you want a really, really simple option for "animating it back the way it came" ...
Option 1 - use .autoReverse
:
@IBAction func transformButtonPressed(_ sender: Any) {
UIView.animate(withDuration: 3, delay: 0, options: [.curveEaseInOut, .autoreverse], animations: {
let translateTransform = CGAffineTransform(translationX: 150, y: 200)
self.square.transform = translateTransform.scaledBy(x: 1.5, y: 1.5).rotated(by: CGFloat.pi/2)
}, completion: { _ in
self.square.transform = .identity
})
}
Option 2 - if you want to "send it back" on another action, for example:
@IBAction func transformButtonPressed(_ sender: Any) {
UIView.animate(withDuration: 3, delay: 0, options: .curveEaseInOut, animations: {
let translateTransform = CGAffineTransform(translationX: 150, y: 200)
self.square.transform = translateTransform.scaledBy(x: 1.5, y: 1.5).rotated(by: CGFloat.pi/2)
}, completion: { _ in
// un-comment to animate it back right away,
// or leave commented, and call transformBack() from somewhere else
//self.transformBack()
})
}
func transformBack() -> Void {
UIView.animate(withDuration: 3, delay: 0, options: .curveEaseInOut, animations: {
// resets transform to "none"
self.square.transform = .identity
}, completion: { _ in
})
}
Upvotes: 1
Reputation: 56635
There's a few things going on here, so let me first give you some background and then go through what is happening to see where things go wrong.
Spoiler: It's the combination of isRemovedOnCompletion = false
and a forwards fillMode
.
On iOS, every view always have a layer that it owns and manages. Setting a property like the frame
or the transform
of the view automatically sets the frame
or transform
of the layer. For the layer, this value that is set is sometimes called the "model value" to distinguish it from the "presentation value" which is how the layer appears on screen during an ongoing animation. In practice this means two things:
If one tries to inspect the value of the layer's position while the view is animating using UIView animations, then the position
value is going to be the end value, that was assigned in the animation block. To get to the "current" value as it appears on screen, one would have to look at the layer's presentation layer's value.
If one adds an explicit CAAnimation to a layer, the layer is going to animate the change on screen, but if the animated property is inspected during the animation it's going to remain unchanged. Once the animation finishes and is removed, the layer is once again going to render it's model value, making it appear as if it jumped back to an old value unless the value was also changed.
The view (and its layer) stars out with a frame origin of (10, 10)
and a size of (100, 100)
making it's center
(the layer's position
) (60, 60)
. Since no transform is set, the "identity transform" (no transformation) is used.
When the button is pressed, the animation block is executed which changes the transform of the view (and thus also its layer) to one with a translation of (150, 200)
a scale of (1.5, 1.5)
and a rotation of π/2
. This changes the transform
"model" value of the layer.
When the first animation is completed, it triggers the second animation. At this point the model values are still what they were set to in the animation block.
Three transform animations is configures to animate to a translation of (10, 10)
, a rotation of -π
, and a scale of 1
. The group of these animations is configured to not be removed upon completion. The model values are not updated at this point.
The animation group finishes but is not removed. At this point the model values are still that transform that was set in the original animation block. Not removing the CAAnimation has persisted a difference between the model (what the property is) and the presentation (how it appears on screen).
Note that the position
is expected to be (60, 60)
at this point, because it was never changed. Changing the transform
property doesn't change the position
property, only where the layer appears on screen.
The next time the button is pressed the first animation is triggered again. It is going to animate from the "old" value to the "new" value. Since the transform
property (of the model) was never changed, these are the same.
I can't recall if it's documented or not, but UIView animations use the property that was changed as the key when adding animations to the layer (which happens behind the scenes).
Because the second animation was added to the layer for the key "transform"
and the layer can only have one animation per unique key. The group animation is removed. This makes it look like the view jumps back to the "end value" of the first animation.
As far as I can recall what UIKit is doing, the view would then perform a 3 second animation from one value to the same vale (i.e. not changing anything).
Once the UIView animation finishes, the completion block runs the second animation again which adds the group animation to the layer.
The real problem are these two lines:
groupTransform.isRemovedOnCompletion = false
groupTransform.fillMode = kCAFillModeForwards
Together they seem like they do the right thing because it appears correct on screen, but as you've seen above they persist the temporary difference between the model and the presentation layer long after the animation completes.
As a good rule of thumb: the "only" time to not remove an animation upon completion is if it's animating the disappearance of a view/layer and the view/layer is being removed upon completion.
One way to approach the fix would be to remove those two lines, and then deal with having to also update the view's (or layer's) transform to the new transform
value before adding the animation group to the layer.
However, in this case the same animation could be achieved using another UIView animation from within the completion block. Alternatively you could use a UIView key-frame animation with two 3 second steps.
Sticking with UIView animations where possible has the benefit that the model values are updated.
Upvotes: 2