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.
Related
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
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.
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.
I am making an app with a search button in the nav bar. I would like to replicate the search animation in the music app.
I would like to press search and have the search bar come in from the top, and the table view to fade in.
Is there an easy way to do this?
I believe its pretty simple
It is actually a different view that is sliding down.
So essentially create a view with everything you want on it. Like the textField, the cancel button, etc.
This view when it is created needs to appear above your mainView.
So the y value on your frame will be 0 minus your new view height.
When you are ready for the view to appear use UIView.animateWithDuration to change your views frame y value to 0
UIView.animateWithDuration(2, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: {
myView.frame = CGRectMake(0, myView.frame.origin.y + myView.frame.height, 100, 100)
//could also just set the y value to 0 but I included the origin/height part to help you understand
}, completion: {
(value: Bool) in
})
I am trying to animate show/hide of search bar using below code (The search bar should come from left and expand to right within 1-2 seconds). However, it doesn't animate and searchBar is immediately shown no matter how much time I put. I noticed following:
Duration is not respected
Not even delay is respected
Animation is not happening. Component is immediately shown
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
//code to get selected value...
//Hide the collection view and show search bar
UIView.animateWithDuration(10.0,
delay: 0.0,
options: UIViewAnimationOptions.TransitionCrossDissolve,
animations: {
self.searchBar.hidden = false
self.searchBar.frame = CGRectMake(0, 0, 300, 44) //This doesn't work either
},
completion: { (finished: Bool) in
return true
})
}
I am using Xcode 7, iOS 8 and Swift 2.0. I have seen at other solution, but none of them works for me. Kindly help...
Update: It worked with below code. However, it used default animation option of UIViewAnimationOptionCurveEaseInOut and TransitionNone
UIView.animateWithDuration(0.7,
animations: {
self.searchBar.alpha = 1.0
self.searchBarRect.origin.x = self.searchBarRect.origin.x + 200
self.searchBar.frame = self.searchBarRect
},
completion: { (finished: Bool) in
return true
})
Before the animateWithDuration, set the XPosition to -200 and YPosition to -200.
Like Daniel Suggested, you need to also change the alpha from something other than 1.0 in order to see it "fade in".
Are you using autolayout? If you are, you may want to animate the constraints instead of the frame.
Finally, are you sure you've wired up the search bar properly?
Edit: PS, I would keep the duration to something like 3.0 or 5.0. 10.0 will take forever and may make you think that it isn't doing anything because of how long it takes. Make it 3.0 or 5.0 (max) just for testing and then decrease to where you want it.