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

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.

Related

Adding a subview inside of a UIStoryboardSegue

I'm writing an app where I need to use a fairly "complex" UIStoryboardSegue. The designer gave me the following:
Now the basic idea behind the segue is simple, sliding up the source.view, then sliding up one more view before eventually sliding the destination.view. However, my question is the following:
How do I insert a second view in-between the source.view and the destination.view from a subclass of UIStoryboardSegue ? Obviously I can't just do addSubview, since there is no view to add to. Is there another place to add a view in a UIStoryboardSegue, so I can still create this segue?
Thanks.
If this is what you want
You can achieve it very easily,
Step 1:
Create a subclass of UIStoryboardSegue and override perform
import UIKit
class SpecialEffectSegue: UIStoryboardSegue {
override func perform() {
let firstVCView = self.source.view as UIView!
let secondVCView = self.destination.view as UIView!
let intermediateView = UIView()
intermediateView.backgroundColor = UIColor.red
// Get the screen width and height.
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
// Specify the initial position of the destination view.
secondVCView?.frame = CGRect(x: 0.0, y: screenHeight, width: screenWidth, height: screenHeight)
intermediateView.frame = CGRect(x: 0.0, y: screenHeight, width: screenWidth, height: screenHeight)
// Access the app's key window and insert the destination view above the current (source) one.
let window = UIApplication.shared.keyWindow
window?.insertSubview(intermediateView, aboveSubview: firstVCView!)
window?.insertSubview(secondVCView!, aboveSubview: secondVCView!)
UIView.animate(withDuration: 0.4, animations: { () -> Void in
firstVCView?.frame = ((firstVCView?.frame)?.offsetBy(dx: 0.0, dy: -screenHeight))!
intermediateView.frame = (intermediateView.frame.offsetBy(dx: 0.0, dy: -screenHeight))
}) { (Finished) -> Void in
UIView.animate(withDuration: 0.4, animations: { () -> Void in
secondVCView?.frame = (secondVCView?.frame.offsetBy(dx: 0.0, dy: -screenHeight))!
}) { (Finished) -> Void in
self.source.present(self.destination, animated: false, completion: {
intermediateView.removeFromSuperview()
})
}
}
}
}
Though code looks huge and complicated what is happening in it is pretty simple.
Get the source and destination viewController's view using
let firstVCView = self.source.view as UIView!
let secondVCView = self.destination.view as UIView!
Because you need a intermediate view which is red in color here you create one more view
let intermediateView = UIView()
intermediateView.backgroundColor = UIColor.red
Now get the UIScreen width and heght so that you can configure these views frame so that it looks good as per your need
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
// Specify the initial position of the destination view.
secondVCView?.frame = CGRect(x: 0.0, y: screenHeight, width: screenWidth, height: screenHeight)
intermediateView.frame = CGRect(x: 0.0, y: screenHeight, width: screenWidth, height: screenHeight)
Note that the I set the frame of SecondVC and intermediateView such that they are below screen bounds and Ill animate them to come up in UIView.animate block
Now obviously because animations are happening on key window of your app access the key window
let window = UIApplication.shared.keyWindow
Now insert the subviews to window as per your need.
window?.insertSubview(intermediateView, aboveSubview: firstVCView!)
window?.insertSubview(secondVCView!, aboveSubview: secondVCView!)
So after this I have views stacked up as FirstVCView -> IntermediateView -> SecondVCView
Now we have pretty much what we need isnt it. Now animate it using UIView.animate
UIView.animate(withDuration: 0.4, animations: { () -> Void in
firstVCView?.frame = ((firstVCView?.frame)?.offsetBy(dx: 0.0, dy: -screenHeight))!
intermediateView.frame = (intermediateView.frame.offsetBy(dx: 0.0, dy: -screenHeight))
}) { (Finished) -> Void in
UIView.animate(withDuration: 0.4, animations: { () -> Void in
secondVCView?.frame = (secondVCView?.frame.offsetBy(dx: 0.0, dy: -screenHeight))!
}) { (Finished) -> Void in
self.source.present(self.destination, animated: false, completion: {
intermediateView.removeFromSuperview()
})
}
}
Important thing to notice is intermediateView.removeFromSuperview() in completion block.
Now I have decided to present destination using self.source.present( if you need to push destination VC with your funky animation say
self.source.navigationController?.pushViewController(destination, animated: false)
Thats all :)
Now open your storyboard, drag a segue from one your FirstVC to SecondVC and select the segue class as SpecialEffectSegue thats it now enjoy
Hope it helps :)

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

Custom Segue Class UIView Animation Issue

I am having trouble creating a custom segue using swift 3 and iOS 8. I am trying to transition between view controllers by fading from one VC to a black screen and then fading from black to my second VC. I tried to implement the segue by creating a custom segue using the code below, but it is not working as I would like it to. My goal is to perform an animation when the black square goes from 0.5 alpha to 1.0 alpha, then present my second view controller, then set the black square from 1.0 alpha back to 0.5 alpha and delete it. Right now, it does the first part correctly but after the animation finishes you can see the first VC for a brief instant before the second VC pops up. How should I change my code to make the transition smoother and get the desired result?
override func perform() {
let sourceVC = self.source
let destinationVC = self.destination
let square = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
square.alpha = 0.5
square.backgroundColor = UIColor.black
sourceVC.view.addSubview(square)
UIView.animate(withDuration: 0.2, animations: {
square.alpha = 1.0
})
UIView.animate(withDuration: 0.2, delay: 0.2, animations: {
square.alpha = 0.5
}) { (finished) in
sourceVC.present(destinationVC, animated: false, completion: nil)
}
}
Update your code like below and try:
UIView.animate(withDuration: 0.2, animations: {
square.alpha = 1.0
UIView.animate(withDuration: 0.2, delay: 0.2, animations: {
square.alpha = 0.5
}) { (finished) in
DispatchQueue.main.async {
sourceVC.present(destinationVC, animated: false, completion: nil)
}
})
}

iOS: UIView animation ignores delay

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

Resources