Blocks on Swift (Animatewithduration:Animations:Completion:)

Blocks on Swift (animateWithDuration:animations:completion:)

The completion parameter in animateWithDuration takes a block which takes one boolean parameter. In Swift, like in Obj-C blocks, you must specify the parameters that a closure takes:

UIView.animateWithDuration(0.2, animations: {
self.blurBg.alpha = 1
}, completion: {
(value: Bool) in
self.blurBg.hidden = true
})

The important part here is the (value: Bool) in. That tells the compiler that this closure takes a Bool labeled 'value' and returns Void.

For reference, if you wanted to write a closure that returned a Bool, the syntax would be

{(value: Bool) -> bool in
//your stuff
}

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

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:

Sample Image


By the way, the performSegue probably should be inside a completion closure of the inner animate call.

UIView Animate and completion block

I was able to reproduce this issue and found a workaround.

When using a UIScrollView, which UITableView inherits from, you can't change its contentOffset is not an animatable property. Instead, you need to use the method setContentOffset:animated:.

So, what you need to do is as follows:

  1. Set your view controller as the UITableView delegate. I did in on viewDidAppear.
  2. Set a flag, so you know in the delegate that the scroll happened because you triggered.
  3. In the delegate method - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView, bounce back the animation (you could here add a delay by using performSelector:afterDelay:.

Here's the code:

@interface MyViewController ()

@property (assign, nonatomic) BOOL shouldReturn;

@end

@implementation MyViewController

-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (TRUE) {
NSLog(@"animating table view");
self.shouldReturn = YES;
self.tableView.delegate = self;
[self.tableView setContentOffset:
CGPointMake(self.tableView.contentOffset.x,
self.tableView.contentOffset.y - 60)
animated:YES];
}
}

- (void)viewDidLoad
{
[super viewDidLoad];
if (TRUE) {
UIRefreshControl *refresh = [[UIRefreshControl alloc] init];
refresh.attributedTitle = [[NSAttributedString alloc] initWithString:@"Pull to Refresh"];
[refresh addTarget:self action:@selector(refreshTableView:) forControlEvents:UIControlEventValueChanged];

[self setRefreshControl:refresh];
}
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
if (self.shouldReturn) {
self.shouldReturn = NO;
[self.tableView setContentOffset:CGPointZero animated:YES];
}
}

@end

How to add a completion to an animation?

Apple documentation says:

completion

A block object to be executed when the animation sequence ends. This
block has no return value and takes a single Boolean argument that
indicates whether or not the animations actually finished before the
completion handler was called.

So you need to give that boolean argument to completion handler:

UIView.animate(withDuration: 3.0, animations: {card.frame = newPosition}, completion: { finish in
if card.frame == newPosition {
card.removeFromSuperview()
}
})

animateWithDuration:animations:completion: in Swift

This is a good one, tricky!

The issue is in your completion block...

A. I would begin by rewriting it like this: (not the final answer, but on our way there!)

{ _ in self.storedCells.removeAtIndex(1) }

(the _ in place of the "finished" Bool, to indicate to the reader that its value isn't being used in the block - you may also consider adding a capture list as necessary to prevent a strong reference cycle)

B. The closure you have written has a return type when it shouldn't! All thanks to Swift's handy feature "implicit returns from single expression closures" - you are returning the result of that expression, which is the element at the given index

(the type of the closure argument for completion should be ((Bool) -> Void))

This can be resolved as so:

{ _ in self.storedCells.removeAtIndex(1); return () }

UIView animate completion block called twice with TRUE

The completion block should only be called once each time the animation completes. Make sure the animation isn't being called more than once.

Add a breakpoint to the beginning of the code you listed, edit it to log some message like "animation starting" and set it to continue after evaluation.

If you see the message more than once you can set the breakpoint to stop each time and look in the call stack to see where the calls are coming from.

If it isn't getting called twice it might be time to file a radar…

Can't put transform change in animation block of animateWithDuration in Swift

You could omit the Void -> in altogether in your case; the real culprit that is causing the problem is this line:

self.zigZag!.layer.mask.transform = CGAffineTransformMakeTranslation(100, 0)

because there is a type mismatch.

It should be:

Swift 2

UIView.animateWithDuration(0.5, delay: 0.05, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.CurveEaseIn, animations: {
self.view!.layer.mask.transform = CATransform3DMakeTranslation(100.0, 0.0, 0.0)
}, completion: nil)

Swift 3, 4, 5

UIView.animate(withDuration: 0.5, delay: 0.05, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: UIView.AnimationOptions.curveEaseIn, animations: {
self.view!.layer.mask.transform = CATransform3DMakeTranslation(100.0, 0.0, 0.0)
}, completion: nil)

UIView.animateWithDuration:animations:completion: gets canceled in XCTest

For the animation to complete with the correct flag, the view needs to in a UIWindow that is visible.

let window = UIWindow()
window.addSubview(view)
window.isHidden = false

With this, your test succeeds. But UIKit won't clean up the window without giving the run loop an extra kick at the very end of the test. So add

func tearDown() {
super.tearDown()
RunLoop.current.run(until: Date())
}

Then the window (and anything in it) will be deallocated.

Now that it's working, you can save time by reducing your duration. I got the test down to 23ms by using duration: 0.001



Related Topics



Leave a reply



Submit