Adding and removing animation on a UILabel - ios

I am creating a game for english typing. The words fall from the top to the edge of keyboard. For animating the fall of a label I have used:
UIView.animateWithDuration(25, delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: {
label.frame = CGRectMake(self.view.frame.width - (label.frame.origin.x+label.frame.width), self.view.frame.height-self.keyboardYPosition!, label.frame.width, width)
}){(success) in
label.removeFromSuperview()
for label in self.currentLabelsArray {
label.layer.removeAllAnimations()
label.removeFromSuperview()
}
self.gameOver()
}
Everything goes fine. The problem I am facing is : I am calling this animation block on creation of every single label. Actually this animation block is in a function called createLabels(). And the createLabel function is called every 5 seconds using NSTimer. Once the game is over I show a view above with restart button. Now here comes the problem:
Before the game ends we might have 3-4 labels already created that are pushed into the animation block. But the very first label might end the game. I get a gameover view above it with a restart button. Once I tap restart, my game again ends because the labels created earlier are still calling the completion block of UIAnimation. I cannot restart the game unless all my labels have completed the animation.
Is there any way to remove animation once the game if completed so that the already created labels no more come into the completion block?
I have used the following code to remove the label from view and remove its animation:
for label in self.currentLabelsArray {
label.layer.removeAllAnimations()
label.removeFromSuperview()
}

A quick guess is that, perhaps you missed to invalidate the timer object.
for label in self.currentLabelsArray {
label.layer.removeAllAnimations()
label.removeFromSuperview()
}
myTimer.invalidate();
So the timer will not call createLabels after the 5 sec. Once the game restarts schedule the time again.
Hope that helps!
EDIT:
Another pointer could be to clear the labels array.
self.currentLabelsArray.removeAllObjects();
and call self.gameOver() iff, there is no request of game over in queue.
EDIT:
UIView.animateWithDuration(25, delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: {
label.frame = CGRectMake(self.view.frame.width - (label.frame.origin.x+label.frame.width), self.view.frame.height-self.keyboardYPosition!, label.frame.width, width)
}){(success) in
//label.removeFromSuperview()
for label in self.currentLabelsArray {
label.layer.removeAllAnimations()
label.removeFromSuperview()
}
if(self.currentLabelsArray.count > 0) {
self.currentLabelsArray.removeAllObjects()
self.gameOver()
}
}

Related

Why is this code to add a subview executing in this sequence?

I am making a small view show up after a long press (iconsContainerView) and am not understanding why the code in handleLongPress(gesture:) is executing in the manner that it is. It's my understanding that it should go top to bottom and each line should run immediately. Meaning as soon as view.addSubview(iconsContainerView) runs, the view should show up in the top left of the screen, as its opacity has not yet been set to 0.
So, the code as written (once the gesture has begun) seems like the view would be shown on the screen in the top left, then move when it's transformed, then disappear (when the opacity is set to 0), then re-appear in the animation when the opacity is set to 1. But what happens is the view doesn't even show up until the code hits the animate block.
So, everything works how I want it to – I do want the subview to fade in after the long press. But I'm just trying to understand what's behind this and why each line of code isn't being immediately executed (or at least showing up on the screen that way). It is running on the main thread, and I've dropped break points in and verified the lines are running in sequence.
class ViewController: UIViewController {
let iconsContainerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = .red
containerView.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
return containerView
}()
override func viewDidLoad() {
super.viewDidLoad()
setUpLongPressGesture()
}
fileprivate func setUpLongPressGesture() {
view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)))
}
#objc func handleLongPress(gesture: UILongPressGestureRecognizer) {
print("Long gesture", Date())
if gesture.state == .began {
view.addSubview(iconsContainerView)
let pressedLocation = gesture.location(in: view)
let centeredX = (view.frame.width - iconsContainerView.frame.width) / 2
iconsContainerView.transform = CGAffineTransform(translationX: centeredX, y: pressedLocation.y - iconsContainerView.frame.height)
iconsContainerView.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.iconsContainerView.alpha = 1
})
} else if gesture.state == .ended {
iconsContainerView.removeFromSuperview()
}
}
}
I think you're expecting that your code functions like this
you add a subview
system draws the view on the screen
you update the views transform
system redraws the view on the screen
you updates the views alpha
system redraws the view on the screen
Since your code is running on the main thread and the systems drawing code also runs on the main thread, theres no way they both could be running at the same time or flip flopping between the two.
What actually happens is that behind the scenes your app has a loop (a RunLoop) that is always running. The simplest way to think about it is that it
handles input
draws views to the screen
repeat
Your code would fall into the handle input part. So you whole method has to finish running before the loop can move onto the next step which is drawing the views to the screen. This is also why it is important not to do a lot of work on the main thread, if your method takes a second to run that would mean that the app could not draw to the screen or handle additional input for 1 whole second which would make the app seem frozen.
Side Notes
In reality the main run loop can have a lot more stuff going on in it. It also has a lot of optimizations to make sure its only running when it needs to be to avoid constantly running the cpu or redrawing when nothing has changed which would murder your battery life. This should be enough of an understanding for the majority of iOS development, unless you starting directly interacting with the main run loop or create other run loops but that is rarely needed.

UIview animation misbehaves when image of an unrelated button is changed

On my viewcontroller, I have a button to enable/disable audio using the following action:
func audioControl() {
playMusic = playMusic ? false : true // toggle
if (playMusic) {
startMusic()
audioBtn.setImage(UIImage(named: "musicOn"),for: .normal)
}
else {
stopMusic()
audioBtn.setImage(UIImage(named: "musicOff"),for: .normal)
}
}
The button toggles its image, as expected.
There is also an unrelated UIImageView object on the same VC which is being translated as follows:
UIView.animate(withDuration: 10.0, delay: 0.0, options: [ .curveLinear, .repeat ], animations: {
self.movingImg.center = CGPoint(x: screenSize.minX, y: screenSize.maxY)
})
This simple animation works fine by itself. However, as soon as the button is tapped, the movingImg object goes outside the boundary of screen and it is no longer visible.
I am not sure why a change of image on a button causes another object animation to misbehave. Any thoughts? I am using Swift 3.
Thank you.
After reading through discussions in a number of SO threads, I found that with the image change (along with any other layout changes), the viewcontroller's viewDidLayoutSubviews() gets called. As such, the followings are two things I did to resolve this issue:
Within the audioControl() method, I removed the animation of movingImg (i.e. movingImg.layer.removeAllAnimations()) before updating the button image.
I added viewDidLayoutSubviews() to my viewcontroller and started the animation of movingImg there.
Hopefully, this approach helps others too.

iOS animation doesn't reset when replayed repeatedly

I have a very simple animation block which performs the following animation:
(tap the image if the animation doesn't play)
func didTapButton() {
// reset the animation to 0
centerYConstraint.constant = 0
superview!.layoutIfNeeded()
UIView.animate(
withDuration: 1,
animations: {
// animate the view downwards 30 points
self.centerYConstraint.constant = 30
self.superview!.layoutIfNeeded()
})
}
Everything is great when I play the animation by itself. It resets to position 0, then animates 30 points.
The problem is when the user taps the button multiple times quickly (i.e. during the middle of an ongoing animation). Each time the user taps the button, I would expect it to reset to position 0, then animate downwards 30 points. Instead, I get this behavior:
(tap the image if the animation doesn't play)
It's clearly traveling well over 30 points. (Closer to 120 points.)
Why is this happening, and how can I "reset" the animation properly so that it only at most travels 30 points?
Things that I have tried that didn't work:
Using options: UIViewAnimationOptions.beginFromCurrentState. It does the same exact behavior.
Instead of executing the animation directly, do it after a few milliseconds using dispatch_after. Same behavior.
"Canceling" the previous animation by using a 0 second animation that changes the constant to 0, and calls superview!.layoutIfNeeded.
Other notes:
If instead of changing the position, I change the height via a constraint, I get similar odd behavior. E.g. if I set it to go from 30 points to 15 points, when repeatedly pressing the button, the view will clearly grow up to around 120 points.
Even if I don't use constraints, and instead I animate the transform from CGAffineTransform.identity to CGAffineTransform(scaleX: 0.5, y: 0.5), I get the same behavior where it'll grow to 120 points.
If I try a completely different animatable property say backgroundColor = UIColor(white: 0, alpha: 0.5) to backgroundColor = UIColor(white: 0, alpha: 0), then I get correct behavior (i.e. each time I tap the button, the color resets to 0.5 gray at most. It never gets to black.
I can reproduce the behavior you describe. But if I call removeAllAnimations() on the layer of the view that is moving, the problem goes away.
#IBAction func didTapButton(_ sender: Any) {
animatedView.layer.removeAllAnimations()
centerYConstraint.constant = 0
view.layoutIfNeeded()
centerYConstraint.constant = 30
UIView.animate(withDuration: 2) {
self.view.layoutIfNeeded()
}
}
Note, I'm not removing the animation from the superview (because that still manifests the behavior you describe), but rather of the view that is moving.
So far, the only solution I found is to recreate the view rather than trying to reset the existing one.
This gives me the exact behavior I was looking for:
(tap the image if the animation doesn't play)
It's unfortunate that you need to remove the old view and create a new one, but that's the only workaround I have found.

How to read current displayed frame of animation?

There is a image and a button on Watchkit interface.
Image animation starts and i press the button, animation pause.
Now i need to read which frame is currently displayed.
Is it possible?
As Aleksander said it is not possible to get the right frame name from your animation. However you could divide your animation in parts and call the next part when the previous part is done animating. You could try my framework I just released to ease your work with animations and also have a completion handling.
TimelineKit - Framework
func startAnimationWithRange(range: Range)
{
Timeline.with(identifier: "MyAnimation", delay: 0.0, duration: 2.0, execution: {
// put your animation code in here
// assumed animation time == duration (2.0 seconds)
}, completion: {
// check if your button was clicked and return or call the next animation part
self.startAnimationWithRange(range: nextAnimationRange)
}).start
}
Check the TimelineKit.swift source file for documentation. Feedback is also welcome. :)

Swift: removeFromSubview happens before animation

I have a little setup where I have an MPMoviePlayerController setup as a video background. I'm trying to get it to have a smooth transition when it updates/changes. Here's my current code. BTW, newBackground.view.alpha is initially 0.
let oldBackground = currentBackground
self.view.insertSubview(newBackground.view, aboveSubview: oldBackground.view)
UIView.animateWithDuration(1.5 as NSTimeInterval, animations: {
newBackground.view.alpha = 1
}, completion: {
finished in
oldBackground.view.removeFromSuperview()
println("The subview should be removed now")
})
When this executes, the oldBackground.view is immediately removed before the newBackground starts to fade in. The println, however, happens after the animation is completed. I don't understand why the removeFromSuperview happens immediately, but the println happens when I expect it to. If I remove oldBackground.view.removeFromSuperview(), the animation fades in and looks fine (but the view obviously hasn't been removed, it's just sitting behind newBackground).
EDIT: Bizarrely enough, it seems to work as expected in the simulator. Running on my iPhone 6 Plus gives me the issue every time. I've uninstalled and re-run it from Xcode and the problem persists.
If anyone has any advice for me, I would be very happy. Thank you.
I guess when the newBackground.view.alpha = 1 is executed during animation it brings the background to visibility and overlays the oldBackground.
When you put the new subview above the old subview, old subview goes to the back. This happens before the animation code starts. Make sure your new subview's alpha is 0 before inserting it. And if you want a fade out animation you should set your oldBackground alpha to 0.
Like this :
let oldBackground = currentBackground
newBackground.view.alpha = 0
self.view.insertSubview(newBackground.view, aboveSubview: oldBackground.view)
UIView.animateWithDuration(1.5 as NSTimeInterval, animations: {
newBackground.view.alpha = 1
oldBackground.view.alpha = 0
}, completion: {
finished in
oldBackground.view.removeFromSuperview()
println("The subview should be removed now")
})

Resources