I have an array of images loaded into a UIImageView that I am animating through one cycle. After the images have been displayed, I would like a #selector to be called in order to dismiss the current view controller. The images are animated fine with this code:
NSArray * imageArray = [[NSArray alloc] initWithObjects:
[UIImage imageNamed:#"HowTo1.png"],
[UIImage imageNamed:#"HowTo2.png"],
nil];
UIImageView * instructions = [[UIImageView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
instructions.animationImages = imageArray;
[imageArray release];
instructions.animationDuration = 16.0;
instructions.animationRepeatCount = 1;
instructions.contentMode = UIViewContentModeBottomLeft;
[instructions startAnimating];
[self.view addSubview:instructions];
[instructions release];
After the 16 seconds, I would like a method to be called. I looked into the UIView class method setAnimationDidStopSelector: but I cannot get it to work with my current animation implementation. Any suggestions?
Thanks.
Use performSelector:withObject:afterDelay::
[self performSelector:#selector(animationDidFinish:) withObject:nil
afterDelay:instructions.animationDuration];
or use dispatch_after:
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, instructions.animationDuration * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self animationDidFinish];
});
You can simple use this category UIImageView-AnimationCompletionBlock
And for Swift version: UIImageView-AnimationImagesCompletionBlock
Take a look on ReadMe file in this class..
Add UIImageView+AnimationCompletion.h and UIImageView+AnimationCompletion.m files in project and them import UIImageView+AnimationCompletion.h file where you want to use this..
For eg.
NSMutableArray *imagesArray = [[NSMutableArray alloc] init];
for(int i = 10001 ; i<= 10010 ; i++)
[imagesArray addObject:[UIImage imageNamed:[NSString stringWithFormat:#"%d.png",i]]];
self.animatedImageView.animationImages = imagesArray;
self.animatedImageView.animationDuration = 2.0f;
self.animatedImageView.animationRepeatCount = 3;
[self.animatedImageView startAnimatingWithCompletionBlock:^(BOOL success){
NSLog(#"Animation Completed",);}];
There is no way to do this precisely with UIImageView. Using a delay will work most of the time, but there can be some lag after startAnimating is called before the animation happens on screen so its not precise. Rather than using UIImageView for this animation try using a CAKeyframeAnimation which will call a delegate method when the animation is finished. Something along these lines:
NSArray * imageArray = [[NSArray alloc] initWithObjects:
(id)[UIImage imageNamed:#"HowTo1.png"].CGImage,
(id)[UIImage imageNamed:#"HowTo2.png"].CGImage,
nil];
UIImageView * instructions = [[UIImageView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
//create animation
CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
[anim setKeyPath:#"contents"];
[anim setValues:imageArray];
[anim setRepeatCount:1];
[anim setDuration:1];
anim.delegate = self; // delegate animationDidStop method will be called
CALayer *myLayer = instructions.layer;
[myLayer addAnimation:anim forKey:nil];
Then just implement the animationDidStop delegate method
With this method you avoid timers to fire while the imageView is still animating due to slow image loading.
You could use both performSelector: withObject: afterDelay: and GCD in this way:
[self performSelector:#selector(didFinishAnimatingImageView:)
withObject:imageView
afterDelay:imageView.animationDuration];
/!\ self.imageView.animationDuration has to bet configured before startAnimating, otherwise it will be 0
Than if -(void)didFinishAnimatingImageView:create a background queue, perform a check on the isAnimating property, than execute the rest on the main queue
- (void)didFinishAnimatingImageView:(UIImageView*)imageView
{
dispatch_queue_t backgroundQueue = dispatch_queue_create("com.yourcompany.yourapp.checkDidFinishAnimatingImageView", 0);
dispatch_async(backgroundQueue, ^{
while (self.imageView.isAnimating)
NSLog(#"Is animating... Waiting...");
dispatch_async(dispatch_get_main_queue(), ^{
/* All the rest... */
});
});
}
Created Swift Version from GurPreet_Singh Answer (UIImageView+AnimationCompletion)
I wasn't able to create a extension for UIImageView but I believe this is simple enough to use.
class AnimatedImageView: UIImageView, CAAnimationDelegate {
var completion: ((_ completed: Bool) -> Void)?
func startAnimate(completion: ((_ completed: Bool) -> Void)?) {
self.completion = completion
if let animationImages = animationImages {
let cgImages = animationImages.map({ $0.cgImage as AnyObject })
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.values = cgImages
animation.repeatCount = Float(self.animationRepeatCount)
animation.duration = self.animationDuration
animation.delegate = self
self.layer.add(animation, forKey: nil)
} else {
self.completion?(false)
}
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
completion?(flag)
}
}
let imageView = AnimatedImageView(frame: CGRect(x: 50, y: 50, width: 200, height: 135))
imageView.image = images.first
imageView.animationImages = images
imageView.animationDuration = 2
imageView.animationRepeatCount = 1
view.addSubview(imageView)
imageView.startAnimate { (completed) in
print("animation is done \(completed)")
imageView.image = images.last
}
You can create a timer to fire after the duration of your animation. The callback could process your logic you want to execute after the animation finishes. In your callback be sure check the status of the animation [UIImageView isAnimating] just in case you want more time to let the animation finish.
in ImageView.m
- (void) startAnimatingWithCompleted:(void (^)(void))completedHandler {
if (!self.isAnimating) {
[self startAnimating];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, self.animationDuration * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(),completedHandler);
}}
Related
As the title said, it's very strange, did you agree?
So , if someone found your codes
imageView.backgroundColor = [UIColor greenColor];
or
imageView.image = [UIImage imageName:#"angryBird"];"
are not working. Just mind that if your imageView is animating , or whether the animation has been removed or not.
----------The Answer Below---------------
Just stop animating before you set image and change background of the animating UIImageView.
UIImageView* imageView = [UIImageView alloc] init];
//......do sth. like setFrame ...
imageView.images = [NSArray arrayWithObjects....]; // set the animation images array
imageView.animationDuration = 1.0;
[imageView startAnimating];
//......do sth.
[imageView stopAnimating]; // **** do not forget this!!!!! ****
imageView.backgroundColor = [UIColor greenColor];
imageView.image = [UIImage imageName:#"angryBird"];
When you performSelector: withObject: afterDey:1.0s , in the selector method , also, you need to stopAnimating too, and then setImage or change background color.
Try to change background color and changing the image once animation is complete. it will simply work for you.
Try this:
[UIView animateWithDuration:1.0 animations:^(){} completion:^(BOOL finished){}];
Try this,Animation using block
[UIView animateWithDuration:1.0 animations:^{
}
completion:^(BOOL finished)
{
[imageView setImage:[UIImage imageNamed:#"YOUR_IMAGE_NAME"]];
[imageView setBackgroundColor:[UIColor greenColor]];
}];
Note: It is imageNamed: not imageName: ,I am not sure how this get compiled
Try this code and performSelector:withObject:afterDelay:: in this way
[self performSelector:#selector(animationDidFinish:) withObject:nil
afterDelay:instructions.animationDuration];
or use dispatch_after:
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, instructions.animationDuration * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self animationDidFinish];
});
i hope it helps you
I have an application where I need to initialize the contentOffset of a UIScrollView to a certain value. I notice when I set the contentOffset, it rounds the points up to the nearest integers. I've tried using both setContentOffset and setContentOffset:animated and still get the same result. When I try to manually add the contentOffset onto the frame origin, I don't get the result I want. Anyone know anyway around this?
UIScrollView *myScrollViewTemp = [[UIScrollView alloc] initWithFrame: CGRectMake(CropThePhotoViewControllerScrollViewBorderWidth, CropThePhotoViewControllerScrollViewBorderWidth, self.myScrollViewBorderView.frame.size.width - (CropThePhotoViewControllerScrollViewBorderWidth * 2), self.myScrollViewBorderView.frame.size.height - (CropThePhotoViewControllerScrollViewBorderWidth * 2))];
[self setMyScrollView: myScrollViewTemp];
[[self myScrollView] setAutoresizingMask: UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
[[self myScrollView] setShowsVerticalScrollIndicator: NO];
[[self myScrollView] setShowsHorizontalScrollIndicator: NO];
[[self myScrollView] setBouncesZoom: YES];
[[self myScrollView] setDecelerationRate: UIScrollViewDecelerationRateFast];
[[self myScrollView] setDelegate:self];
[[self myScrollViewBorderView] addSubview: [self myScrollView]];
[myScrollViewTemp release];
UIImageView *myImageViewTemp = [[UIImageView alloc] initWithImage: self.myImage];
[self setMyImageView: myImageViewTemp];
[[self myScrollView] addSubview:[self myImageView]];
[[self myScrollView] setContentSize: [self.myImage size]];
[[self myScrollView] setMinimumZoomScale: self.previousZoomScale];
[[self myScrollView] setMaximumZoomScale: 10];
[[self myScrollView] setZoomScale: self.previousZoomScale animated:NO];
[[self myScrollView] setContentOffset: self.contentOffset animated:NO];
[myImageViewTemp release];
UPDATE: Updated code.
It appears this isn't a bug on my part, but is the intended behavior by Apple whether on purpose or not. In order to work around this, you need to set the contentOffset in the viewWillLayoutSubviews method of the UIViewController. Apparently, you need to set the contentOffset after the whole view frame has been set.
I too struggled with this issue quite a bit, but it turns out that you could just set the bounds on the scroll view, which does the same thing and actually deals ok with CGFloat values.
CGPoint newOffset = CGPointMake(3.8, 0);
We got the new offset here.
[scrollView setContentOffset:newOffset];
After this line the current offset is (4,0).
scrollView.bounds = CGRectMake(newOffset.x, newOffset.y, scrollView.bounds.size.width, scrollView.bounds.size.height);
And this gives you (3.8, 0)
Swift 4:
I've maded an extension to have more precision:
extension UIScrollView {
func setPreciseContentOffset( x:CGFloat? = nil, y:CGFloat? = nil, animated:Bool,completion:#escaping ((Bool) -> Void)) {
var myX:CGFloat = self.contentOffset.x
var myY:CGFloat = self.contentOffset.y
if let x = x {
myX = x
}
if let y = y {
myY = y
}
let point = CGPoint(x:myX,y:myY)
if animated {
// calculate duration
var duration = 0.25 // min duration
let destCoord = myX != self.contentOffset.x ? myX : myY
let diff = fabs ( self.contentOffset.x - destCoord)
duration += TimeInterval(diff) / 1000
UIView.animate(withDuration:duration, delay: 0.0, options: [.curveEaseInOut], animations: {
self.bounds = CGRect(x:point.x,y:point.y,width: self.bounds.size.width,height: self.bounds.size.height)
}, completion: { (finished: Bool) in
completion(finished)
})
} else {
self.bounds = CGRect(x:point.x,y:point.y,width: self.bounds.size.width,height: self.bounds.size.height)
completion(true)
}
}
}
Usage:
// supposing you have to scroll manually in horizontal
self.myScrollview.setPreciseContentOffset(x:contentX, animated: true, completion: {[weak self] _ in
guard let `self` = self else { return }
// HERE the scroll is ended, pay attention the end scrolling delegates aren't be called because you set manually self.bounds so HERE implement your scrolling end code
})
I think this is because the contents of any subviews of the UIScrollView would be rendered blurred if the contentOffset is not rounded up, especially text.
I am messing around with a basic CAAnimation example where Im just trying to get a CALayer to rotate in a subclass of UIView. I start by making a new CALayer with a red background and inserting it into self.layer. I then make a CAAnimation that is supposed to be triggered on opacity changes and apply it to the CALayer, after which it spins as expected.
Weird thing is that if I try to apply the exact same animation using performSelector:withObject:afterDelay:, the CALayer no longer animates, although its opacity still changes. Even weirder is that if I use performSelector:withObject:afterDelay: to animate self.layer using pretty much the same code, it works.
Any ideas as to what's going on? Here is my code:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
point = [[CAGradientLayer alloc] init];
point.backgroundColor = [UIColor redColor].CGColor;
point.bounds = CGRectMake(0.0f, 0.0f, 30.0f, 20.0f);
point.position = CGPointMake(100.0f, 100.0f);
[self.layer addSublayer:point];
[point release];
[self performSelector:#selector(test) withObject:nil afterDelay:2];
}
return self;
}
-(void)test {
[self spinLayer:point];
// [self spinLayer:self.layer]; works here
}
-(CAAnimation*)animationForSpinning {
CATransform3D transform;
transform = CATransform3DMakeRotation(M_PI/2.0, 0, 0, 1.0);
CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
basicAnimation.toValue = [NSValue valueWithCATransform3D:transform];
basicAnimation.duration = 5;
basicAnimation.cumulative = NO;
basicAnimation.repeatCount = 10000;
return basicAnimation;
}
-(void)spinLayer:(CALayer*)layer {
CAAnimation *anim = [self animationForSpinning];
[layer addAnimation:anim forKey:#"opacity"];
layer.opacity = 0.6;
}
and this is my interface
#interface MyView : UIView {
CALayer *point;
}
-(void)test;
-(void)spinLayer:(CALayer*)layer;
-(CAAnimation*)animationForSpinning;
#end
Note: I am instantiating this view with a frame equal to [UIScreen mainScreen].applicationFrame from within the app delegate.
Change this:
[layer addAnimation:anim forKey:#"opacity"];
into this:
[layer addAnimation:anim
forKey:#"notOpacity!"];
What happens is that implicit animation layer.opacity = 0.6 overwrites you explicit animation. This happens because Core Animation uses property names when adding implicit animations.
(If you move layer.opacity = 0.6 above [layer addAnimation:anim forKey:#"opacity"]; it will also work, but layer.opacity will not get animated.)
And why it works without delay: because implicit animations are "enabled" only if layer is in the tree and been displayed.
I'm trying to move two buttons around using the accelerometer in xcode. I have the first button working, when I call startDrifting, but when I call startDrifting2 for the second button, nothing is happening with the second button. I tried calling startAccelerometerUpdatesToQueue for the startDrifting2, but then the first one stopped working. any ideas?
-(void) startDrifting:(UIButton *) button
{
[self.motionManager startAccelerometerUpdatesToQueue:[[[NSOperationQueue alloc] init] autorelease] withHandler:^(CMAccelerometerData *data, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
CGRect labelFrame = button.frame;
labelFrame.origin.x += data.acceleration.x * MOTION_SCALE;
if (!CGRectContainsRect(self.view.bounds, labelFrame))
labelFrame.origin.x = button.frame.origin.x;
labelFrame.origin.y -= data.acceleration.y * MOTION_SCALE;
if (!CGRectContainsRect(self.view.bounds, labelFrame))
labelFrame.origin.y = button.frame.origin.y;
button.frame = labelFrame;
} );
}];
}
-(void) startDrifting2:(UIButton *) button
{
CMAccelerometerData *data = [self.motionManager accelerometerData];
dispatch_async(dispatch_get_main_queue(), ^{
CGRect labelFrame = button.frame;
labelFrame.origin.x += data.acceleration.x * MOTION_SCALE * 1;
if (!CGRectContainsRect(self.view.bounds, labelFrame))
labelFrame.origin.x = button.frame.origin.x;
labelFrame.origin.y += data.acceleration.y * MOTION_SCALE * 1;
if (!CGRectContainsRect(self.view.bounds, labelFrame))
labelFrame.origin.y = button.frame.origin.y;
button.frame = labelFrame;
});
}
Try setting the delegate to itself and then give it back to the main delegate of the app. When you hit the next button try and reassign the delegate back in the other method.
Is it possible to implement animating sequence of images using CAAnimation? And i dont want to use UIImageView...
I need Something similar to UIImageView where we can set imageView.animationImages and calling startAnimation... but using CAAnimation
CAKeyframeAnimation *animationSequence = [CAKeyframeAnimation animationWithKeyPath: #"contents"];
animationSequence.calculationMode = kCAAnimationLinear;
animationSequence.autoreverses = YES;
animationSequence.duration = kDefaultAnimationDuration;
animationSequence.repeatCount = HUGE_VALF;
NSMutableArray *animationSequenceArray = [[NSMutableArray alloc] init];
for (UIImage *image in self.animationImages)
{
[animationSequenceArray addObject:(id)image.CGImage];
}
animationSequence.values = animationSequenceArray;
[animationSequenceArray release];
[self.layer addAnimation:animationSequence forKey:#"contents"];