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

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.

Related

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.

Animate an imageView with changing constraints (progress bar) Swift

So, I am creating a sort of progress bar, mostly just an animation to go from 0 to 100 percent. I have two images, one that is red on bottom, one on top that is black. I am trying to take away from the top image to create the idea that it is loading from 0 to 100%, slowly revealing the image below. I have all of that working, my problem is that I do not see the in between. I see 0% and 100%, I can debug it and view the controller and see my view controller (self) updating and slowly taking away the top image so I know that it is working but I do not see it go through each animate. I have edited one moved things around a ton but have not been able to see the changes. Thanks for everyones help in advance!
override func viewDidAppear(_ animated: Bool) {
if intCounter <= 100{
UIView.animate(withDuration: 0.05, animations: catScanE.layoutIfNeeded)
repeat {
dblImageH = dblImageH - 3.80
self.catScanE.frame = CGRect(x: 27 , y: 197, width: 320, height: dblImageH)
intCounter = intCounter + 1
} while (intCounter <= 100)
} else {
performSegue(withIdentifier: "lastSegue", sender: nil)
}
}
I have gotten the animate to basically work, it scrolls down the screen but shows a type of animation. I want it to slowly restrict the constraints but this is not the issue now. Since the animation "works", I am not able to get it to segue after the animation, this is what I have. Adding the if intCounter part makes it skip the animation and segue, without it the screen performs the animation and sits there...
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 5.0, animations: {
self.dblImageH = self.dblImageH - 3.80
self.catScanE.frame = CGRect(x: 27 , y: 197, width: 320, height: self.dblImageH)
self.intCounter = self.intCounter + 1
if self.intCounter > 100{
self.performSegue(withIdentifier: "lastSegue", sender: nil)}
})
}
I figured it out with some help from Ravron and more StackOverflow searching. I set the content mode to top in the attributes and used this code to make it act like a progress bar (this is a black image with a red one underneath to look like it is going from 0 to 100). I ran into an issue with the image basically moving up or downwards, this was fixed by changing the y coordinate to match the change in size so it would basically stay in the same position. I just need to figure out how to make a counter to go from 0 to 100 while it is animating. Thanks! Hopefully this can help someone else making a custom progress bar.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 5.0, animations: {
self.catScanE.frame = CGRect(x: 27 , y: 200, width: 320, height: 0)
}, completion: {finished in self.performSegue(withIdentifier: "lastSegue", sender: nil)})
}
Your animate block runs only once, and the final settings of the view's frame is all that counts. The view is actually being animated from 0% to 100% as you wish, but it happens in 50 milliseconds, or about three frames.
Just set the frame to the desired final position in the animate block and increase the duration to something reasonable, like 100 * 0.05s = 5s.
To expand slightly: the animate method says, informally, "run the block below, which changes one or more traits of various views. Then, implicitly animate those traits from their current values to the values set when the block was run." So it's only called once, and in it you should set traits to their desired final values.
Consider reading through the animations section of the View Programming Guide for iOS which gives a comprehensive overview of animation basics on the platform, and also covers UIView block-based animations, the type you're trying to use here.

How to compare if the coordinates are equal to the original coordinates of an object

Can someone please help me??
Lets start with the main information: I created an app that has a 2 view and one is called InfoView. This InfoView is located off screen with the following code:
(Code Hide View)
UIView.animateWithDuration(1.0, delay: 0.3, options: [ ], animations: {self.InfoView.center.x -= self.view.bounds.width + 150}, completion: nil)
Its automatically moved when the app starts. I placed the Code Hide View on ViewDidAppear.
When the button Info is pressed the InfoView ease in from left to right to its original coordinates, (As I placed it on the storyboard with x=12 and y=20 and the Views size depends on the screen size). I use the following code to bring it back.
(Code Show View)
UIView.animateWithDuration(1.0, delay: 0.0, options: [ ], animations: {self.InfoView.center.x += self.view.bounds.width + 150}, completion: nil)
The InfoView has a button that when pressed hides the view (Code Hide View) once again so that its off screen.
My problem is that when I tryout the app if the Iphone receives a call (phone call or sms or etc), after the call had ended it would show the InfoView on screen and without pressing the Show View Code to run.
Ive then hidden the view with:
InfoView.Hidden = True
And when I run the Show View code it changes to false and when it hides it turns to true and so on. However now when I receive a call and after the call has ended One can't see the InfoView (Thats good) although If I press the show Info Button the InfoView will appear suddenly on screen and ease out the right side of the screen, making it unreachable (off screen).
My question is there a way to compare the InfoView coordinates so that if its on Screen (And I haven't called it, run the show View) it could be sent back off screen. Like with an if statement. for example like:
if InfoView == CGPoint(12,20).... //(Then Run Hide View)
(I know the if statement is wrong) And I do understand that I got to run that func in the app delegate in the applicationWillResignActive.
Please can someone help me, am I on the right track or can someone give me another solution. (Tell Me If I need to explain it better)
You should compare it by below way. Do not comapre with hardcoded point.
if(InfoView.center.x > self.view.bounds.width)
{
//infoview is hidden
}
else
{
//infoview is visible
}

Adding and removing animation on a UILabel

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()
}
}

Resources