Pre-Emptive Swift Animation Object Move - Confusing - swift

Good day,
I have a very simple animation function, that drops a button by 200.
However, I discover that before the animation begins, the object is moved up (-) by 200! Therefore, after the animation, the button is back where it started.
I tried to set self.button1.center = self.view.center in viewDidAppear before calling the function.
func dropStep(){
UIView.animate(withDuration: 6, animations: {
self.button1.center.y += 200
}, completion: nil)
}
I expected the animation to start from where it is intended (at the center of the view) and not pre-emptively shifted up by 200 points.

Try,
func dropStep(){
button1.center = self.view.center
UIView.animate(withDuration: 6, animations: {
self.button1.center.y += 200
}, completion: nil)
}

Okay, so I could not genuinely find the reason behind this.
However, this was an app that was used and reused to learn animations, so maybe somehow the object referencing outlets or some sort of hidden reference was messed up, such that the app behaved funny.
Therefore, I created a new app with virtually the same code, and it behaved as expected. Thanks anyway for your help.

Related

Stopping and Resetting a CGAffineTransform

I'm trying to write code where when a trigger happens an instance of UIImageView creates a slow "growing" effect with a CGAffineTransform running for 20 seconds.
The issue happens when the trigger happens again before the first transform has completed. Instead of the image resetting back to it's original size, it shrinks exponentially, depending on when the trigger happened during the first transformation.
Here is my current code:
func changeCategoryImage() {
self.categoryPanoramaImageView.transform = CGAffineTransform.identity
UIView.transition(with: self.categoryPanoramaImageView, duration: 0.4, options: .transitionCrossDissolve, animations: {
self.categoryPanoramaImageView.image = self.newPanoramaImage
}) { (done) in
UIView.animate(withDuration: 10, animations: {
self.categoryPanoramaImageView.transform = CGAffineTransform(scaleX: 5.0, y: 5.0)
})
}
}
I was under the impression that CGAffineTransform.identity would reset the image back to it's original size. This is seems to not be the case.
How can I halt and reset the current running transformation in order to start a new one?
I was under the impression that CGAffineTransform.identity would reset the image back to it's original size. This is seems to not be the case.
Correct. It isn't the case. That's not how animations work. You are making a direct change to the underlying view (i.e. the model layer), but the animation is still present (i.e. the presentation layer).
Thus, you are setting the view's transform to the identity, but you are not removing the existing animation. And animations are additive by default. Hence the result you are observing.
Try inserting
self.categoryPanoramaImageView.layer.removeAllAnimations()
as the first line of your function. That way, you will remove the animation and start over from the identity.

Swift animation of constraint not working

The below function is just moving the view to a new place. It isn't showing the animation:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.9, animations: {
//self.leadingConst.constant -= 200
self.view.setNeedsLayout()
})
}
self.leadingConst.constant -= 200 should be outside UIView.animate, like this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.leadingConst.constant -= 200
UIView.animate(withDuration: 0.9, animations: {
self.view.setNeedsLayout()
})
}
reference: trying to animate a constraint in swift
A few weeks ago I ran into the same problem. The problems came down to a few things. I was mixing adding views programmatically and adding them to the storyboard, I wasn't calling layoutSubviews(), and I was adding my constraints as IBOutlets as weak vars. So I would suggest to only use storyboard (otherwise animations start to get complicated quickly) and make your leadingConst constraint a var. You should also move the object that you want to animate in the animation block and then use a completion block to reset the constraint.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
view.layoutSubviews()
UIView.animate(withDuration: 0.9, animations: {
//something like this
self.animatableObject.origin.x -= 200
}, completion: {(finished: Bool) in
self.leadingConst.constant -= 200
})
}
I'm not sure what your trying to animate so the origin.x line is just an example.
2023 answer
The correct solution was given in a comment, you need to call layoutIfNeeded()
A typical example
UIView.animate(withDuration: t) { [weak self] in
guard let self = self else { return }
self.bottom.constant = -H
self.superview!.layoutIfNeeded()
}
IMPORTANT: "IN OR OUT" IS THE SAME
somewhat bizarrely, it makes NO difference if you do:
this
UIView.animate(withDuration: t) {
self.bottom.constant = -66
self.superview!.layoutIfNeeded()
}
or this
self.bottom.constant = -66
UIView.animate(withDuration: t) {
self.superview!.layoutIfNeeded()
}
There are 1000 totally incorrect comments on SO that you have to do it one way or the other to "mak eit work". It has nothing to do with the issue.
WHY THE SOLUTION IS layoutIfNeeded
It was perfectly explained in the #GetSwifty comment.
layoutIfNeeded is when the actual animation takes place. setNeedsLayout just tells the view it needs to layout, but doesn't actually do it.
WHY THE ISSUE IS CONFUSING: IT "SOMETIMES" WORKS ANYWAY
Often you actually do not need to explicitly have layoutIfNeeded. This causes lots of confusion. ie, this will work "sometimes" ...
UIView.animate(withDuration: t) {
self.bottom.constant = -66
}
There are particular things to consider:
If you are animating "the view itself" (ie likely in a custom UIView subclass), the cycle can be different from when you are animating some view below you "from above"
If you have other animations going on, that can affect the view cycle.
Note that if it "magically works" without layoutIfNeeded, it may NOT work other times in your app, depending on what's going on in the app.
In short you must always add layoutIfNeeded, that's all there is to it.
setNeedsLayout wont work, you need to layoutSubviews().
UIView.animate(withDuration: 0.2,
delay: 0.0,
animations: {
self.myConstraint.constant = value
self.view.layoutSubviews()
}

UIView Block-Animation Completion Always True

I have had an animation that I have recently caught to be not working
//time is a variable used in my code
UIView.animate(withDuration: time, delay: 0, options: [.curveLinear, .allowUserInteraction], animations: {
//this class is ofType UIScrollView
self.setContentOffset(CGPoint(x: self.contentSize.width-self.frame.width, y: 0), animated: false)
//Completion Handler
}, completion: { finished in
//It's always true, not sure a way to fix this
if(finished ) {
But, later in my code, I have a method to remove certain animations, specifically from this scrollView.
self.layer.removeAllAnimations()
It gets called, and I would assume it is suppose to make the above
if(finished ) { //Here
return false, therefore it should not go inside the finished. But, finished is ALWAYS true. Whether I cancel this animation, continue the animation, doesn't matter what I do to the animation, the completion handler is always true. Any tips on this matter?
Your call to setContentOffset specifies animated:false, if that is the only property you are changing, then there are no actual animations going on, so the call to .animate(...) will always complete with a value of true. If you set this property (or another one) with animated:true and your duration is too short for the animation to finish, then it could complete with a false value for the parameter.

Implementing basic animations with tvOS

I'm hoping someone may be able to help me understand why my tvOS animations are not running.
In my code I have a function like this:
func testAnimation() {
clockLabel.alpha = 0.0
UIView.animateWithDuration(1.0, animations: {
self.clockLabel.alpha = 1.0
})
}
I am calling testAnimation() from within my viewDidLoad(), but no animation ever seems to happen.
I've tested with a few different types of animations, from things like position to opacity, but it seems that no animation ever actually runs in the Simulator.
At this time, my app does not have a focus. All I'm trying to do is load a blank UIView with a label in the middle that fades in.
You're trying to animate your UILabel before it has been displayed. Move your animation from viewDidLoad() to viewDidAppear().
I can confirm this behaviour as this happens for me as well. What I did was setting a delay of 0.1 (afterDelay:), and it is "good enough"
On the other hand, what DID actually work was setting a transform for my views. E.g. (objc though):
CGAffineTransform scaleTransform = CGAffineTransformMakeScale(0.5,0.5);
[UIView animateWithDuration: 0.5 animations:^{
_myView.transform = scaleTransform;
} completion: nil]
Maybe it's a bug in tvOS
Try using UIView Animation, like this (Swift):
clockLabel.alpha = 0
UIView.animateWithDuration(0.5, delay: 0.0, options: .Linear, animations: {
self.clockLabel.alpha = 1.0
}, completion: { finished in
// you may add some code here to make another action right after the animation end, or just delete this comment :)
})
This works on our side.

UIScreenEdgeRecognizerGesture smooth like safari

My container view controller has a screen edge pan gesture to change the views. The code for panning the views looks as follows:
func changeView(recognizer: UIScreenEdgePanGestureRecognizer) {
println("INITIAL: \(recognizer.translationInView(view))")
if recognizer.state == .Began {
// Create and configure the view
println("BEGAN: \(recognizer.translationInView(view))")
}
if recognizer.state == .Changed {
println("CHANGED: \(recognizer.translationInView(view))")
let translation = recognizer.translationInView(view)
currentView.view.center.x += translation.x
pendingView.view.center.x += translation.x
recognizer.setTranslation(CGPointZero, inView: view)
}
if recognizer.state == .Ended {
if recognizer.view!.center.x > view.bounds.size.width {
// Animate the view to position
} else {
// Animate the view back to original
}
}
}
While this works, I'm still having an issue with the start of the panning. When a user swipes quickly, translation will have a value big enough to make the start of the pan looking "unsmoothly".
For example, with a quick swipe translation will start with a value of 100. The value is then added to the center.x of the views causing the undesired effect.
I noticed Safari has a screen edge gesture as well to change views and this effect doesn't occur no matter how quick the swipe is. Nor does this happen with a normal UIPanGestureRecognizer.
I've tried wrapping the "animation" in UIView.animateWithDuration(). It does look more smooth, but then it feels it's just lagging behind the actual gesture, unlike how it's done in Safari.
Can someone please tell me a better way to pan the views so it will look as smooth as in Safari?
EDIT:
I've added several lines to check the value of the translation and the problem is it jumps from 0 to some value causing the unwanted behavior. It doesn't matter where I put recognizer.setTranslation(CGPointZero, inView: view).
The output is:
INITIAL: (21.5, 0.0)
BEGAN: (21.5, 0.0)
INITIAL: (188.0, -3.0)
CHANGED: (188.0, -3.0)
After some more testing:
func changeView(recognizer: UIScreenEdgePanGestureRecognizer) {
println("INITIAL: \(recognizer.translationInView(view))")
recognizer.setTranslation(CGPointZero, inView: view)
}
INITIAL: (0.0, 0.0)
INITIAL: (130.5, -35.5)
FINAL:
Seems like creating and preparing the new view is causing some kind of minor lag in Began. The small amount of lag is enough to create a difference in translation of 100-200.
Probably have to preload the views somewhere else I guess.
This won't solve all your problems, since, as you have rightly said, a screen edge pan gesture recognizer is a little crusty in its behavior; but do note that you are omitting one valuable piece of data - the question of what recognizer.translationInView is in the .Began state. At that time, obviously, the finger has already moved considerably; for, if it had not, we would not have recognized this as a screen edge pan gesture! You will thus be much happier, I think, if you construct your tests like this:
switch recognizer.state {
case .Began:
// ... do initial setup
fallthrough // <-- TAKE NOTE
case .Changed:
// respond to changes
default:break
}
In that way, you will capture the missing datum and respond to it, and the jump will not be quite so bad.
I tried logging in both began and changed and my numbers (showing translationInView with no setTranslation back to zero) are this sort of thing:
began
changed
(-16.5, 0.0)
changed
(-41.5, 0.0)
changed
(-41.5, 0.0)
changed
(-58.5, 0.0)
(The first one, preceded by began, is the fallthrough execution of changed.) So yes, we do go from nothing to -41 very fast, but at least there is an intermediate value of -16.5 so it isn't quite so abrupt.
Also I should add that if there is a serious delay and jump it may well be that you have multiple conflicting gesture recognizers. If so, you can detect this fact by using delegate methods such as gestureRecognizer:shouldRequireFailureOfGestureRecognizer: - which will also let you prioritize between them and perhaps make the other g.r. give way sooner.