UIView scale animation overshoots when changed - ios

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)

Related

iOS Swift Animation does not work some times

Cant solve problem of showing animation in custom popup.
In my app i use SwiftEntryKit to show top alert with custom view. This custom view contains animation
UIView.animate(
withDuration: 0.8,
delay: 0.0,
options: [.autoreverse, .repeat],
animations:
{
let scale = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.img.transform = scale
}, completion: nil)
Everything works as expected, i can show a popup with scale animation.
But when i call exactly same popup after network call with RxSwift animation does not work. No errors no info, view just gets it scaled up 1.3 size and does not show animation. Here is part of my code with networking
HelperAuth.makeRegister(name: name!, email: email!, password: password!)
.observeOn(MainScheduler.instance)
.subscribe(onNext:
{ reponse in
print(reponse)
}, onError:
{ error in
MessagesManager.showRedAlerter(text: "ERRRRAR")//here is my method to show popup with animation
}).disposed(by: coordinatorAuth.disposeBag)
Cant find any reason of this strange behavior.
As Dhaval Raval mentioned in comments wraping animation in completing block of other animation solves animation problem.
I changed my code of animation to
UIView.animate(
withDuration: 0.0,
animations: {
}, completion:
{ _ in
UIView.animate(
withDuration: 0.8,
delay: 0.0,
options: [.autoreverse, .repeat],
animations:
{
let scale = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.img.transform = scale
}, completion: nil)
})
and now it works as it should and from normal method call and after RxSwift Networking call

Change UIView's super view with animation in Swift

I have a VideoView (it is a child view of UIView).
By default, it is added to a UIView which is small view in the corner of the screen (I called it ParrentView1).
I have a button to zoom out VideoView. This button performs an action that removes VideoView from ParentView1 and adds it to a bigger view (called ParrentView2).
When I perform the code below, it works but the animation is weird. All I need is a zoom out animation from ParrentView1 to ParrentView2.
Here is my code:
VideoView.removeFromSuperview()
ParrentView2.addSubview(VideoView)
UIView.animate(withDuration: 0.8, delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1,
options: .curveEaseOut,
animations: {
VideoView.frame = ParrentView2.bounds
}, completion: nil)
thanks for helping
The likely cause is that when adding it to the other view, it gets assigned a different frame. The solution is to make sure the animation starts at the original location. Something like:
CGRect originalRect = ParrentView2.convert(VideoView.frame, from:VideoView.superView);
VideoView.removeFromSuperview()
ParrentView2.addSubview(VideoView)
VideoView.frame = originalRect;
UIView.animate(withDuration: 0.8, delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1,
options: .curveEaseOut,
animations: {
VideoView.frame = ParrentView2.bounds
}, completion: nil)
An improvement point: note that it is customary in Swift to start variable names with a lower case letter. It gets confusing when they don't.

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?

Perfect Swift3 Boing

To animate a bar opening...
#IBOutlet var barHeight: NSLayoutConstraint!
barHeight.constant = barShut?30:100
self.view.layoutIfNeeded()
t = !barShut?30:100
UIView.animate(withDuration: 0.15,
delay: 0,
options: UIViewAnimationOptions.curveEaseOut,
animations: { () -> Void in
self.barHeight.constant = t
self.view.layoutIfNeeded()
},
completion: {_ in
Screen.barShut = !Screen.barShut
}
)
That's great ...
But how would you make it boing like this?
(The only way I'd know to do this is, use CADisplayLink, with a few lines of code for a spring decaying.) Is this available in UIKit?
You can use the spring animation method that is built in to UIView:
func toggleBar() -> Void {
self.view.layoutIfNeeded()
let newHeight:CGFloat = !barShut ? 30:100
barShut = !barShut
barHeightConstraint.constant = newHeight
UIView.animate(withDuration: 1.5, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 3, options: [], animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
You will want a longer animation duration than 0.15 of a second in order for the bounce to seem realistic; I think the values I have look pretty good, but you can play with them to get the exact effect you are after.
Since the animation duration is longer, I found that I could tap the button the triggered the open/shut while the previous animation was still running. Setting barShut in the completion block meant that the bar didn't react to all taps. I moved the toggle outside of the animation to address this.

MKMapView doesn't adhere to animation delay

I'm fiddling with my MKMapView in Playground, and I'm having trouble animating it's frame.size.height. Here's my Playground code:
let vc = UIViewController()
vc.view.backgroundColor = UIColor.whiteColor()
XCPlaygroundPage.currentPage.liveView = vc
let map = MKMapView(frame: vc.view.frame)
map.backgroundColor = UIColor.redColor()
map.autoresizesSubviews = false
vc.view.addSubview(map)
UIView.animateWithDuration(2.0, delay: 1.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: .CurveEaseInOut, animations: {
map.frame.size.height = 200
}, completion: nil)
You can tell when the actual animation is occurring by observing the map's red background color. The visible map's height decreases before the MKMapView's height. Setting autoresizesSubviews = false doesn't seem to do anything. So why does it seem as if there are two distinct animations?
UPDATE In my Playground, I removed all my MKMapView subviews, and the _MKMapContentView, one of the map's two subviews (the other being the "Legal" MKAttributionLabel), was removed, rendering the MKMapView with just it's red background. So the _MKMapContentView is being resized as a subview, but calling map.autoresizesSubviews = false isn't doing the trick. What gives?
This is a bit of a hack, if you will. I tried animating the map's height using animateWithDuration:delay:usingSpringWithDamping and a nonzero delay. My problem was that the _MKMapContentView wouldn't adhere to the delay, which is what explained the distinct animations.
So, with a little help from #Matt's famous delay solution, I scrapped my function delay and put the entire method in a delay block, so that any (sub)views would animate concurrently.
delay(3.5) {
UIView.animateWithDuration(1.0, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 1.0, options: .CurveEaseInOut, animations: {
map.frame.size.height -= 200
}, completion: nil)
}
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
Bit of a workaround, but it works. Still eager to learn why map's subviews didn't adhere to the function delay, if anyone has an idea.
I think you maybe to user this to avoid:
map.backgroundColor = UIColor.whiteColor()
vc.view.addSubview(map)
UIView.animateWithDuration(2.0, delay: 1.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: .CurveEaseInOut, animations: {
map.frame.size.height = 200
}, completion:
map.backgroundColor = UIColor.redColor()
)

Resources