iOS animation doesn't reset when replayed repeatedly - ios

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.

Related

UIProgressView do not redraw his frame when app launch back from background

i have view with `UIProgressView` and 3 dot-view. It's like a page control. Each page - the video. `progressView` displays progress of video playback
ok. I do not use constraints for left and right anchors, because my progressView should swap places with dot-view. For example, when current video is ended and next start play, we should swap positions of `progressView` with next dot-view. For swap i just change frames
and the problem is: when i move app to background and returns back, my `progressView` loses his old frame. It attaches to the left side, because `.frame.minX` is 0
and the last one: this problem occurs only after first returns from background
what i tried to do:
save progressView frames before app is going to background and restore it when app comes to foreground: progressView.frame = progressViewOldFrames and call setNeedsDisplay()
add constraint to leftAnchor with constant (frame.minX) before background and remove it after foreground
combine these 2 tries
so now it looks like
func appWillMoveInBackground() {
progressBarXConstraint = progressBar.leftAnchor.constraint(equalTo: self.leftAnchor, constant: progressBar.frame.minX)
progressBarXConstraint?.isActive = true
progressBarFrame = progressBar.frame
}
func updateProgressWidth() {
progressBarXConstraint?.isActive = false
// here i use constraints because my width and height also disables
// and only constraints helps me
progressBar.widthAnchor.constraint(equalToConstant: 32).isActive = true
progressBar.heightAnchor.constraint(equalToConstant: 6).isActive = true
progressBar.frame = progressBarFrame
progressBar.setNeedsDisplay()
}
UPDATE
ok, i should explain more. I guess i cant use constraints because we have some animation while we are scrolling. When we scroll to right - we should move our progressView to some points at right. And in this moment we should move right dot-view to the left. I mean, we do not scroll page by page, we can scroll to a half on the right, then we can return to the initial position.
this code of block did change frames of progressView and dot-view. Formulas are not important. Please, just understand the behavior of this view
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// calc some math variables
// and call method that make changes in frames
pageControl.moveBetweenPages(atPercentValue: distInPercents - 100)
}
// its part of body moveBetweenPages() func
progressBar.frame = CGRect(x: progStartX + progAddDist, y: 0,
width: SizeConstants.progressBarWidth.value,
height: SizeConstants.height.value)
let dotStartX: CGFloat = SizeConstants.progressBarWidth.value + SizeConstants.itemsSpacing.value + (CGFloat(currentPageNum) * dotSectionSize)
dots[currentPageNum].view.frame = CGRect(x: dotStartX - dotAddDist, y: 0,
width: SizeConstants.dotWidth.value,
height: SizeConstants.height.value)
images shows how it looks before background and after
matt from comments suggested me use constraints instead of frames and yes, it helps and it works
only thing i can say is dont forget call setNeedsLayout() after constraints update

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.

Show No Internet Connection Message Like Instagram

I was wondering how can i show a 'No Internet Connection' Just how like Instagram does it,
As an Example :
That see-through custom message animating to show under the navigationController . Would really love to get this to my project ,
thank you for you help
So here's a pic of the storyboard like this :-
"No internet connection" is a label, and the red view underneath is just to test the see through property of the label. If you are designing the UI in code, you can probably make a label similar to mine and place it to the top of the Navigation bar by using it's frame property.
The button here I'm using is just to show the label pop up on the scene (since it's just a demo answer). In your case, if the internet is not available, you will proceed to show the pop up.
So if you are making the UI in code, make sure to make the label in the viewDidLoad method. I have made an IBOutlet and the viewDidLoad now looks like this:-
override func viewDidLoad() {
super.viewDidLoad()
let transform = CGAffineTransform(translationX: 0, y: -label.frame.height)
label.alpha = 0
label.transform = transform
}
On the view loading, I'm moving the label behind the navigation bar, using CGAffineTransform. The distance, how much to move up is the label's height, since we don't want any part to be clipped on the scene.
Next step, is just a fix. I'm making alpha = 0, because navBar is translucent is nature and hence will change it's colour, since our label is behind it. So setting alpha to 0, takes care of it, and in third step apply the transform.
Now, if the internet connection is not available, we should pop out the label under the navBar. The code will look something like this:-
fun checkInternet() {
// called by some of your observer, which checks for changes in internet connection
if !isInternetAvailable {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0, options: .curveLinear, animations: {
self.label.alpha = 0.5
self.label.transform = .identity
}, completion: nil)
}
}
So here, I'll show the pop up with an animation using UIView.animate with some spring damping, so it has a nice bouncy effect to it. I'm setting the alpha to 0.5, since you mentioned you want a see through label, and I'm setting the label to a transform which will bring it back to it's original position when it was created, that's why I'm using .identity.
You can play around usingSpringWithDamping values and change options to have different effects.

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.

UIScrollViewDelegate - animateWithDuration in scrollViewDidEndDragging is not working as expected

I have a UIScrollView with 2 sub views/pages side by side (horizontal content size = 2 * Screen Width + gutter space between pages). I would like to increase the completion speed of the animation, ie after the user has completed the dragging and lifted the finger. Based on the suggestions found in SO, I implemented a UIScrollViewDelegate as below.
class MyScrollViewDelegate: NSObject, UIScrollViewDelegate
{
var targetX: CGFloat = 0.0
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
{
// I have not implemented the paging logic yet as I wanted to test the replacement animation first.
// Below code simply uses the suggested tagetOffset, but limiting the same between the valid min and max values
targetX = fmax(0, fmin(scrollView.contentSize.width - scrollView.frame.size.width, targetContentOffset.memory.x))
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool)
{
UIView.animateWithDuration(0.25, animations: { scrollView.contentOffset = CGPointMake(self.targetX, 0) })
}
}
My understanding is that once the dragging is over, my own animation code in the scrollViewDidEndDragging will take over and complete the translation. The problem I am facing is that there seems to be an additional animation/jump when I pull the view inward when it is at the leftmost or rightmost edge.
For example, when the contentOffset.x = 0 (left edge), if I pull the view rightward by say 20 points and release (even after pausing for a second), the contentOffset.x & targetContentOffset.memory.x would be -20, and the calculated self.targetX would be 0. Hence in my animation I expect the page to move back towards left by 20 points with given speed as soon as I lift the finger. But what I observe is that, the page goes further to the right by almost the same amount (dragged distance) and then animates back from there to 0. The movement is so fast that I can't make out whether it is an animation or direct jump. Afterwards it follows my animation parameters to fall back.
As mentioned, the rightward jump seems to be proportional to the dragging distance, suggesting that my assumption about the "current position" before the start of animation is probably wrong. But I am not able to figure out the exact reason. Your help is much appreciated. The setup is ios 9.0, swift 2.0, xcode 7.0.1.
Edit 1:
I commented the animateWithDuration (but kept the scrollViewDidEndDragging). Animations stopped except in the edge region, where there is a default animation pulling the content back. Further reading pointed to the bounce property. I have a doubt that this default animation is colliding with the one I supplied.
Edit 2:
The bounce animation seems to be the culprit. While searching in this direction I came across another question in SO (Cancel UIScrollView bounce after dragging) describing the issue and possible solutions.
The issue seems to be because of queuing up of bounce animation and the custom animation. For details please read - Cancel UIScrollView bounce after dragging. I am not sure how the chosen answer solves the problem of custom duration. There is another solution involving sub-classing. That also didn't work in my case. Not sure whether it is due to different iOS versions.
Following is what worked for me. It is only the basic snippet. You can enhance the same to have better animation curves and velocity handling.
func scrollViewWillBeginDecelerating(scrollView: UIScrollView)
{
// below line seems to prevent the insertion of bounce animation
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
// provide your animation with custom options
UIView.animateWithDuration(0.25, animations: { scrollView.contentOffset = CGPointMake(targetX, targetY) })
}

Resources