iOS: UIView animation ignores delay - ios

I have a problem with very simple UIView animation:
- (void)showView:(CGFloat)delay
{
[UIView animateWithDuration:1.0 delay:delay options:0 animations:^{
// self.alpha = 0.5; // Works OK with delay
self.frame = CGRectMake(100, 100, 100, 30); // delay is ignored
} completion:nil];
}
delay could be set to 1000 and still view is animated immediately. But somehow it works fine with alpha (without setting frame).

The frame is probably being set for you, perhaps by layout being performed by the superview, or this view itself. You haven't said how you're adding this view or what it does so its difficult to give specific advice, but in general terms:
Don't let a view be in control of its own frame - this is the superview's or view controllers responsibility. A view can have a preferred size, but the rest is outside.
it's usually better to animate bounds and / or centre instead of frame, particularly if you're ignoring the first point.
For presentation type animations, it may be better to animate the transform instead - either translation or scale, depending what your animation is.

[self performSelector:#selector(animateMe) withObject:nil afterDelay:1000];
If self is viewController then you should use self.view.frame instead of self.frame. If self is view then you should use self.frame.
- (void)animateMe {
[UIView animateWithDuration:1.0f animations:^{
// self.alpha = 0.5; // Works OK with delay
self.frame = CGRectMake(100, 100, 100, 30); // delay is ignored
} completion:^(BOOL finished) {
NSLog(#"Done");
}];
}

This answer uses Swift, specifically version 4.2, and is tested in Xcode 10 playgrounds. This is actually my first try animating, and I'm very green with iOS in general, but this is how I solved the problem of delay not being observed.
import UIKit
import PlaygroundSupport
// Set up the main view
let liveViewFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let liveView = UIView(frame: liveViewFrame)
liveView.backgroundColor = .white
// Show the live view using the main view
PlaygroundPage.current.liveView = liveView
// Set up a smaller view for animation
let smallFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
let square = UIView(frame: smallFrame)
square.backgroundColor = .purple
// Add the square to the live view ...
liveView.addSubview(square)
// ... and create an alias for it to allow delays to work.
// Calling square directly won't allow delays,
// giving it or the liveView subview an alias directly fails,
// and unwrapping (? or !) appears to fail as well.
// So, just call like `alias?.backgroundColor` ...
let squareView = square.superview?.subviews[0]
UIView.animate(withDuration: 1.5, delay: 1.0, options: [.repeat], animations: {
//square.backgroundColor = .orange // this doesn't respect the delay arg
squareView?.backgroundColor = .orange
squareView?.frame = CGRect(x: 150, y: 150, width: 200, height: 200)
squareView?.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
}, completion: nil)
This may not answer the question directly, but I believe similar logic could be applied for a workaround in related cases.

Instead of
UIView.animate(
withDuration: 0.2,
delay: 1,
options: .curveLinear,
animations: {
self.someButton.isHidden = false
})
}
Do just:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
UIView.animate(withDuration: 0.2,
delay: 0,
options: .curveLinear,
animations: {
self.someButton.isHidden = false
})
}

Related

How to use completion block for UIView.animate()?

I'm working on a project to learn animations and am having trouble using the completion block for the UIView.animate(withDuration:) func. MY animation is a shoebox that falls from the top of the screen, lands on a pedestal, then opens. Once the box opens I want a UIImageView to come out of the box, grow to full size of the screen and then segue to the next page, but the completion handler that the code for segueing is called before my animation completes and the UIImageView doesn't appear at all.
Here's my code:
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 1.5, delay: 0.0, options: .curveEaseInOut,
animations: {
//Opening the box
self.shoeBoxImage.shoeBox.animationImages = self.boxOpeningAnimation
self.shoeBoxImage.shoeBox.animationDuration = 1.5
self.shoeBoxImage.shoeBox.animationRepeatCount = 1
self.shoeBoxImage.shoeBox.contentMode = .scaleAspectFill
self.shoeBoxImage.shoeBox.startAnimating()
//set to the final image
self.shoeBoxImage.shoeBox.image = UIImage(named: "frame13")
},completion: {_ in
let nextPage = UIImageView()
nextPage.frame = CGRect(origin: self.shoeBoxImage.center, size: CGSize(width: 0.0, height: 0.0))
nextPage.image = UIImage(named: "FirstLoadBackgroundImg.jpeg")
nextPage.autoresizesSubviews = true
self.view.addSubview(nextPage)
self.view.bringSubviewToFront(nextPage)
UIView.animate(withDuration: 5.0, animations: {
nextPage.transform = CGAffineTransform(scaleX: 428, y: 926)
})
self.performSegue(withIdentifier: "FinishedLoading", sender: self)
})
}
This is my first time working with animations and programatically creating views so if someone could explain how to make the completion block wait for the animation to complete. In order to make the UIImageView appear and animate then once it's full screen, segue to the next page it would be very much appreciated.
The size is 0, 0. Transforming zero by any scale is still zero. I would advise you to not use transform at all, but rather just set the final frame to be what you want.
E.g.,
let startFrame = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
let endFrame = view.bounds
let imageView = UIImageView(image: ...)
imageView.contentMode = .scaleAspectFill
view.addSubview(imageView)
imageView.frame = startFrame
UIView.animate(withDuration: 3, delay: 0, options: .curveEaseInOut) {
imageView.frame = endFrame
} completion: { _ in
// do something here
}
That yields:
By the way, the performSegue probably should be inside a completion closure of the inner animate call.

Swift imageView animation doesn't work

I have code to create animation:
var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageView = UIImageView(frame:CGRect(x: 10, y: 10, width: 10, height: 10))
imageView.image = UIImage(named:"2.png")
UIView.animate(withDuration: 5) {
self.imageView = UIImageView(frame:CGRect(x: 10, y: 10, width: 500, height: 500))
self.view.addSubview(self.imageView)
self.view.layoutIfNeeded()
}
}
But animation doesn't work. And imageView not showing.
You should set the constraint's constant before the animation block and inside just call layoutIfNeeded
self.imageViewWidth.constant = 500
self.imageViewHeight.constant = 500
UIView.animate(withDuration: 10) {
self.layoutIfNeeded()
}
Try this..
UIView.animate(withDuration: 10) {
self.imageViewWidth.constant = 500
self.imageViewHeight.constant = 500
self.view.updateConstraintsIfNeeded()
self.view.layoutIfNeeded()
}
To decrease and increase size you have to use the on completion handler
UIView.animate(withDuration: 2, animations: {
self.imageViewWidth.constant = 10
self.imageViewHeight.constant = 10
self.view.layoutIfNeeded()
}) { (true) in
UIView.animate(withDuration: 10, animations: {
self.imageViewWidth.constant = 500
self.imageViewHeight.constant = 500
self.view.layoutIfNeeded()
})
}
if you want the decrease to start immediately change the first withDuration to 0
There are things that makes sense within an animation block (here: UIView.animate). Creating a newer image view is not one of them. Neither is addSubView.
The problem is that you are completely reassigning to your imageview variable - creating a new one within animation block.
You should create your image view and add it to your main view completely before you execute UIView.animate.
Animation is changing the visible properties of your objects, not recreating them. If you want frame change, simply do the following within animation block:
self.imageView.frame = CGRect(x: 10, y: 10, width: 500, height: 500)
You can also call layoutSubviews() or layoutIfNeeded() depending upon your constraint requirements, within UIView.animate.
Where is your problem
Problem 1
imageView = UIImageView(frame:CGRect(x: 10, y: 10, width: 10, height: 10))
imageView.image = UIImage(named:"2.png")
Here, you created new instance of an image view which never has been added into viewcontroller's view stack. You should add this view into stack. These line of code doesn't have any effect on animation performance.
Problem 2
UIView.animate(withDuration: 5) {
self.imageView = UIImageView(frame:CGRect(x: 10, y: 10, width: 500, height: 500))
self.view.addSubview(self.imageView)
self.view.layoutIfNeeded()
}
}
You initialized another imageview and added as a subview. This image view doesn't have any image or background color and as a result you do not see anything happending in the device screen. You should not do these stuff here. View.animate block does not need these stuff. Few things you should learn about animation. How animation works
Solution:
Try these lines of code instead.
// initial frame
var frame:CGRect = CGRect(x: 10, y: 10, width: 10, height: 10)
// create imageview and add this into view stack
self.imageView = UIImageView(frame:frame)
self.imageView.image = UIImage(named:"2.png")
self.view.addSubview(self.imageView)
// set new height & width
frame.size.width = 500;
frame.size.height = 500;
// animate view
UIView.animate(withDuration: 5) {
self.imageView.frame = frame;
self.view.layoutIfNeeded()
}
Hope, it will work!
if you want to animate constraints than you can use this
self.imageViewWidth.constant = 500
self.imageViewHeight.constant = 500;
UIView.animate(withDuration: 10) {
// if you are doing this in View controller then
self.view.layoutSubviews()
// if you are doing this in View then
self.layoutSubviews()
}

The completion block of UIView animation gets called before the animation is finished

I have a custom animator for view controller interactive transition. There is also a blur effect that is set to nil depending on the transition progress. The effect's animation code is the following:
#objc func blurEffectDissmisal() {
UIView.animate(withDuration: dismissAnimator.durationOfAnimation + 1, animations: {
self.blurEffectView?.effect = nil
}) { (done) in
if (done) {
self.blurEffectView?.removeFromSuperview()
}
}
}
I call it by a notification, which is called on the second view controller when the transition from it to the first one starts.
However, I have a problem here. The completion block is called before the animation ends. When I run the transition for the first time (without canceling it) it works fine, but during the subsequents runs it doesn't.
I had also tried to add the animation to my animator but it didn't work out, either.
Moreover, the completion block gets called before the actual animation ends when I cancel the transition (in this case, I understand why but can't figure out how to make it move backwards. Maybe I should create a reverse animation in a completion block?)
I have tried the suggestion from this answer as you can see, but doesn't help.
If you know how this problem could be solved, I would really appreciate your help.
Use a delay before calling animate function .
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
UIView.animate(withDuration: 2.0,delay: 0, animations: {
self.frame.origin.x = -2000
}) { (done) in
if(done){
self.removeFromSuperview()
}
}
}
I've created a playground where you can take a look at this change, just create new playground, click on the assistant editor (top left corner, two joined circles) and take a look at the animation. This should do.
import PlaygroundSupport
import UIKit
let rect = CGRect(x: 0, y: 0, width: 40, height: 40)
let view = UIView(frame: rect )
view.backgroundColor = UIColor.yellow
let label = UILabel(frame: rect)
label.text = "A"
label.textAlignment = .center
view.addSubview(label)
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = view.bounds
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(blurEffectView)
PlaygroundPage.current.liveView = view
UIView.animate(withDuration: 4, animations: {
blurEffectView.alpha = 0
}) { (done) in
if (done) {
blurEffectView.removeFromSuperview()
}
}
The problem you are facing is not that the UIView.animate doesn't do it's job, it's just because you are setting the view to nil is not animable. Like imagine deleting someething could be animated...
UIView.animate(withDuration: 1, animations: {
self.blurEffectView?.frame = CGRect(x: self.blurEffectView?.frame.origin.x, y: self.view.frame.size.height, width: self.blurEffectView?.frame.size.width, height: self.blurEffectView?.frame.size.height)
}, completion: {
(value: Bool) in
self.blurEffectView?.removeFromSuperview()
})
Using the same animation (on UIView) I am able to achieve this :

UIView.animate problems with view frame

I'm struggling trying to make an animation. This is what's happening: video
The back card was supposed to do the same animation as in the beginning of the video, but it's doing a completely different thing. I'm checking the UIView.frame at the beginning of the animation and it's the same as the first time the card enters, but obviously something is wrong... Here is the code:
func cardIn() {
let xPosition = (self.darkCardView?.frame.origin.x)! - 300
let yPosition = (self.darkCardView?.frame.origin.y)!
self.initialPos = self.darkCardView.frame
let height = self.darkCardView?.frame.size.height
let width = self.darkCardView?.frame.size.width
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(Double.pi/4)))!
UIView.animate(withDuration: 1.1, animations: {
self.darkCardView?.frame = CGRect(x: xPosition, y: yPosition, width: width!, height: height!)
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(0)))!
})
}
func cardOut() {
let xPosition = (self.darkCardView?.frame.origin.x)! - 600
let yPosition = (self.darkCardView?.frame.origin.y)!
let height = self.darkCardView?.frame.size.height
let width = self.darkCardView?.frame.size.width
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(0)))!
UIView.animate(withDuration: 1.0, delay: 0, options: .allowAnimatedContent, animations: {
self.darkCardView?.frame = CGRect(x: xPosition, y: yPosition, width: width!, height: height!)
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(-Double.pi/4)))!
}) { (true) in
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(0)))!
self.darkCardView?.frame = self.initialPos
self.cardIn()
}
}
Does somebody know how can I repeat the same animation that's in the beginning of the video after cardOut function is called?
Given that we do not know where/when you are calling the above two functions I can only take a guess at what is happening.
So you have an animate in and an animate out, but what about a reset function?
Timeline:
Animate the card in.
...
Eventually animate the card out
...
Reset the cards location to off screen to the right (location before animating in for the first time)
...
animate the card in
...
Repeat, ect....
I managed to solve the animation problem by calling viewDidLoad() instead of calling self.cardIn() in the end o cardOut completion block. But I don't want to depend on calling viewDidLoad every time a new card has to enter the screen...
Also, I didn't set any properties of the back card in viewDidLoad. I only have it in the .xib file. Any ideas?
Note that if you set a view's transform to anything other than the identity transform then the view's frame rect becomes "undefined". You can neither set it to a new value nor read it's value reliably.
You should change your code to use the view's center property if you're changing the transform.
As Jerland points out in their post, you also probably need to reset your back card to it's starting position before beginning the next animation cycle. (Set it to hidden, put it's center back to the starting position and set it's transform back to the identity transform.
EDIT:
This code:
UIView.animate(withDuration: 1.0, delay: 0, options: .allowAnimatedContent, animations: {
self.darkCardView?.frame = CGRect(x: xPosition, y: yPosition, width: width!, height: height!)
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(-Double.pi/4)))!
}) { (true) in
self.darkCardView?.transform = (self.darkCardImageView?.transform.rotated(by: CGFloat(0)))!
self.darkCardView?.frame = self.initialPos
self.cardIn()
}
Does not set the transform to identity. Try changing that line to
self.darkCardView?.transform = .identity

UIView animation that mimics presentViewController easing

I have a view which animates from the bottom of my ViewController, and I'm trying to get the animation to be the same as the default
present(viewController, animated:true, completion:nil)
I want to animate a view with the same easing and timing as the above code. Right now I'm using this:
UIView.animate(withDuration: 0.35, delay: 0, options: .curveEaseInOut, animations: {
self.myView.frame = newFrame
}, completion: nil)
The animations aren't similar though, and I don't know what values to use in my UIView animation to make it look like a viewController present. Anyone know what values could get me close?
here is sample code with somewhat animation like presenting viewcontroller
var subViewNew = UIView()
make initial dream setup of you subview which you want to make to add on main view as below
override func viewDidLoad() {
super.viewDidLoad()
subViewNew.frame = CGRect(x: 100,y: 100,width:self.view.frame.width - 200,height:200)
self.subViewNew.frame.origin.y = self.view.frame.origin.y + self.view.frame.size.height
subViewNew.backgroundColor = UIColor.gray
}
for appearance of the view subview you can go like this
UIView.animate(withDuration: 0.7, animations: {
self.subViewNew.frame.origin.y = 300 + 20
self.view.addSubview(self.subViewNew)
})
for dismiss or hiding it you can go like this
UIView.animate(withDuration: 0.7, animations: {
self.subViewNew.frame.origin.y = self.view.frame.origin.y + self.view.frame.size.height
self.subViewNew.removeFromSuperview() //If you want then only
})
Hope this help you

Resources