I have an array of UIViews which I animate with the following code:
for (int i = 0; i<(int)viewArray.count; i++) {
UIView *view = [viewArray objectAtIndex:i];
CALayer *layer = view.layer;
//animate the layer
}
My question is, is there any way to have a delay between every animation, so that, for example, it animates one UIView in the array and the next animation starts after 0.2 seconds or so? Any help would be greatly appreciated!
You may want to try the following
float timeDelay = 0.2
float duration = YOUR_DURATION
for (int i = 0; i<(int)viewArray.count; i++) {
UIView *view = [viewArray objectAtIndex:i];
//animate the layer
[UIView animateWithDuration:duration delay:(i+1)*timeDelay
options: {Check UIView Class Reference on developer.apple.com}
animations:^{
[view.layer fantastic animation here];
} completion^(BOOL finised){
if(finished){
//Leave blank if nothing, good practice to implement debuging here or post animation processes here (like removing a subview from a super)
}
}];
}
Keep in mind that if u send your program the the background it may break your animations so make sure u recall this method in ur app Delegates:
- (void)applicationWillEnterForeground:(UIApplication *)application;
Hope it helps
Use this guy:
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion
use the second parameter. Inside your loop, call that function, passing an every-increasing number to that parameter.
Related
I'm trying to record a UIView animation for a WatchKit app. First, I implemented the function with out the block which return zero frames being recorded. This was due to the [recorder stop] being called before the animation completed (I think). So, I added a completion block. Now, it never has self.completion is YES. I want the completion block to notify me when the animation is complete. What am I missing here?
ViewController.m
-(void)runAnimation{
ALBatteryView *batteryView = [[ALBatteryView alloc] initWithFrame:CGRectMake(0, 0, 64, 64)];
[self.batteryIcon addSubview:batteryView];
recorder.view = _batteryIcon;
[recorder start];
[batteryView setBatteryLevelWithAnimation:YES forValue:[UIDevice currentDevice].batteryLevelInPercentage inPercent:YES];
CGFloat batteryPer = [UBattery batteryLevel];
batteryPercentage.text = [NSString stringWithFormat:#"%.0f%%", batteryPer];
battery = [NSString stringWithFormat:#"%.0f%%", batteryPer];
[batteryView batteryAnaminationWithCompletion:^(BOOL finished){
if (finished){
[recorder stop];
}
}];
}
AlBatteryView.h
#interface ALBatteryView : UIView {
UIView *batteryFill;
}
#property (nonatomic, strong) void(^completion)(BOOL finished);
- (void)setBatteryLevelWithAnimation:(BOOL)isAnimated forValue:(CGFloat)batteryLevel inPercent:(BOOL)inPercent;
- (void)reload;
- (void)batteryAnaminationWithCompletion:(void (^)(BOOL finished))completion;
#end
ALBatteryView.m
- (void)setBatteryLevelWithAnimation:(BOOL)isAnimated forValue:(CGFloat)batteryLevel inPercent:(BOOL)inPercent {
// Declare the newWidth and save the correct battery level
// based on inPercent value
CGFloat newWidth;
CGFloat newBatteryLevel = (inPercent) ? batteryLevel : batteryLevel * 100;
// Set the new width
newWidth = kOnePercent * newBatteryLevel;
// If animated proceed with the animation
// else assign the value without animates
if (isAnimated)
[UIView animateWithDuration:2.0 animations:^{
/* This direct assignment is possible
* using the UIView+ALQuickFrame category
* http://github.com/andrealufino/ALQuickFrame */
batteryFill.width = newWidth;
// Set the color based on battery level
batteryFill.backgroundColor = [self setColorBasedOnBatteryLevel:newBatteryLevel];
if (self.completion) {
self.completion(YES);
}
}];
else
batteryFill.width = newWidth;
}
-(void)batteryAnaminationWithCompletion:(void (^)(BOOL finished))completion{
self.completion = completion;
}
You need to set your block property before you call for the animation (setBatteryLevelWithAnimation). Otherwise it will be nil when you try to access it before it's set.
Also you should set your block property directly, it will be more clear, because that's what your -batteryAnaminationWithCompletion method does (btw it should be spelled "Animation")
From:
[batteryView batteryAnaminationWithCompletion:^(BOOL finished){
if (finished){
[recorder stop];
}
}];
To:
[batteryView setCompletion:^(BOOL finished){
if (finished){
[recorder stop];
}
}];
You should set your completion block before starting the animation (change code order).
Then, your block should be declared as copy.
#property (nonatomic, copy) void(^completion)(BOOL finished);
Then, delete the (wrongly named) batteryAnaminationWithCompletion method altogether, and just use the property:
batteryView.completion = ...
If I am reading it right,
When you call -(void)runAnimation
it calls the animation block right here and animation block will referencing the completion which is nil at the moment
[batteryView setBatteryLevelWithAnimation:YES forValue:[UIDevice currentDevice].batteryLevelInPercentage inPercent:YES];
If you are thinking animationWithDuration call the block 2 second later than.. it's wrong.. self.completion is nil at the moment and animation's duration is 2 second but self.completion is not animatable and it calls right up front.. you need some kind of lock if you want to wait this animation to be finished.
[UIView animateWithDuration:2.0 animations:^{
/* This direct assignment is possible
* using the UIView+ALQuickFrame category
* http://github.com/andrealufino/ALQuickFrame */
batteryFill.width = newWidth;
// Set the color based on battery level
batteryFill.backgroundColor = [self setColorBasedOnBatteryLevel:newBatteryLevel];
if (self.completion) {
self.completion(YES);
}
}];
Just print out NSLog before animateWithDuration and NSLog from inside of block to see when if (self.completion) is referenced..
The interface is unduly complex if the only objective is animation. There's no need for a block property at all. Consider the following...
// ALBatteryView.h
#interface ALBatteryView : UIView
// up to you, but I'd clean up the animating interface by making a property of this class determine
// how it handles levels in all cases as either absolute or %
#property(assign, nonatomic) BOOL inPercent;
// use naming style like the sdk...
- (void)setBatteryLevel:(CGFloat)batteryLevel animated:(BOOL)animated completion:(void (^)(BOOL))completion;
#end
In the implementation...
// ALBatteryView.m
#interface ALBatteryView ()
// hide the internal view property here. make it weak, and assign it after [self subView:...
#property(weak,nonatomic) UIView *batteryFill;
#end
In just that one animation method, using a few tricks in UIView animation, you can accomplish everything that it looks like you need...
- (void)setBatteryLevel:(CGFloat)batteryLevel animated:(BOOL)animated completion:(void (^)(BOOL))completion {
// one trick is that UIView animation with zero duration just
// changes the animatable properties and calls its completion block
NSTimeInterval duration = (animated)? 2.0 : 0.0;
CGFloat newBatteryLevel = (self.inPercent)? batteryLevel : batteryLevel * 100;
CGFloat newWidth = kOnePercent * newBatteryLevel;
// don't name that color-returning method "set" color. The "set" part happens here...
UIColor *newColor = [self colorBasedOnBatteryLevel:newBatteryLevel];
// now, the animation in just one shot, passing the block parameter
// which has the same signature as the UIView animation block param
[UIView animateWithDuration:duration animations:^{
self.batteryFill.width = newWidth;
self.batteryFill.backgroundColor = newColor;
} completion:completion];
}
Can someone please tell me why this doesn't work?:
//Changing the Images
- (void) squareOneColour {
NSUInteger r = arc4random_uniform(5);
[self.squareOne setBackgroundImage:[self.colorArray objectAtIndex:r] forState:UIControlStateNormal];
}
//Moving buttons
- (void) squareOneMover {
NSUInteger r = arc4random_uniform(3);
[self.squareOne setHidden:NO];
CGPoint originalPosition = self.squareOne.center;
originalPosition.y = -55;
[self.squareOne setCenter:originalPosition];
CGPoint position = self.squareOne.center;
position.y += 760;
[UIView animateWithDuration:r + 3
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
[self.squareOne setCenter:position];
}
completion:^(BOOL complete) {
if (complete) {
[self squareOneColour];
[self squareOneMover];
}
}
];
}
I am trying to get a UIButton to animate down the screen and when the animation is finished, for it to repeat but with a different background. The code above seems it should work but the second animation is never completed. (The UIButton is animated once, then everything stops). I have tried putting the code shown below (into the completion block), with an outside NSUInteger method creating a random number for r but it never works. But it does work when I replace r with a different number, like 1 or 2.
I found the answer. After playing around with it, I found that the function I wanted the UIButton to do was only fit for a UIImageView. I guess I'll have to make an invisible button over the UIImageView
You're stepping on your own feet. Step out to the next cycle of the runloop and all will be well:
[self squareOneColour];
dispatch_async(dispatch_get_main_queue(), ^{
[self squareOneMover];
});
i'd have to create some animated shapes in a uiview in my iphone app (with xcode), like these ones (circle , rectangle)
The first one should be a circle increasing and decreasing its radius, from 0 to x and viceversa; the second one is a rectangle increasing its length from 0 to x, and viceversa. The shapes should be animated infinitely... until the user taps some button to stop the animations at some point. How can i achieve those particular draws and animations? What's the best way to achieve that? And how to stop them?
Many thanks for any advice.
In my case I had to animate the movement of subviews of my UIView object and I accomplished it like this: break up the continuous animation into logical parts, animate a part and configure this animation to start another animation after it is done animating. Below you see my code as is (what it does is move subviews that are placed on a circle along that circle over a given angle).
- (void) animatePart: (NSString*) animationId finished: (NSNumber*) finished context: (void *) context
{
// the context contains an NSNumber which is the angle remaining to turn the pieces
CGFloat displacement = [trackSet currentDisplacementOnTrack:drag.trackIndex];
NSMutableDictionary* contextDict = (NSMutableDictionary*) context;
Move * move = (Move *) [contextDict objectForKey:ANIMCTX_MOVE];
//NSNumber *direction = (NSNumber *) [contextDict objectForKey:ANIMCTX_DIRECTION];
printf("> EVC animatePart: displ: %3.3f ", displacement);
if (FABS(displacement) < 0.1) {
printf(", ending animation\n");
// this is the last piece of animation
[trackSet completeMoveOnTrack:move.trackRef];
[self updatePiecesAnimatedFromModel];
self.animationContext = nil;
}
else {
CGFloat dir = displacement == 0 ? 0 : displacement / FABS(displacement);
CGFloat delta = -ANIM_DELTASTATION * dir;
printf(" EVC animatePart: delta: %3.3f\n", delta);
CGFloat newDelta = FABS(delta) < FABS(displacement) ? delta : -displacement;
[trackSet registerMoveOnTrack:drag.trackIndex by:newDelta];
[UIView beginAnimations:animationId context:context];
[UIView setAnimationDuration:0.1];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animatePart:finished:context:)];
[self updatePiecesFromTrack:drag.trackIndex];
[UIView commitAnimations];
}
printf("< EVC animatePart\n");
}
As you can see, the UIView methods setDelegate: and animationDidStopSelector: are used to configure this animation to call a certain method after it's done animating. The selector passed here is that of this same method. This sounds like a recursive method (it even has a stop condition, just like recursive methods normally do), but it's not. So no worries about stack overflows ;-)
For completeness, to kickstart this animation I use the method below (copied verbatim from my code).
- (void) animateMove: (Move *) move direction: (int) direction
{
printf("> EVC animateMove: %d\n", direction * move.stations);
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithObject:move forKey:ANIMCTX_MOVE];
[dict setObject:[NSNumber numberWithInt: direction] forKey:ANIMCTX_DIRECTION];
self.animationContext = dict;
//[trackSet openMoveOnTrack:move.trackRef];
// the move has already been carried out in the model, but is not yet reflected in the UI
// we don't need to call trackSet's openMoveOnTrack and closeMoveOnTrack
// for the animation we initially set the track's current displacement to that of the move
// then in a series of animated steps we decrement that displacement gradually to zero,
// while showing the intermediate results in the UI.
CGFloat moveDisplacement = (CGFloat) (move.stations);
[trackSet registerMoveOnTrack:move.trackRef by:moveDisplacement];
[self animatePart:#"displacementAnimation" finished:[NSNumber numberWithInt:0] context:dict];
printf("< EVC animateMove: \n");
}
if I code these:
UIView *topView = [UIView new];
for (int i = 0; i < 100; i++) {
UIView *childView = [UIView new];
[topView addSubview:childView]
}
Does topView redraw every time when I called the "addSubview" function?
No, topView does not redraw every time. It is simply flagged as "needs display" each time, which is a cheap operation. When you return to the run loop, the run loop will tell topView to actually redraw itself and clear the "needs display" flag.
I have a subview which I want to show up to make a dissolve fade effect. For that purpose, I'm trying to:
Show it
Fade it
For that purpose, I set a "showFade" method on my superview (Fader is the subview defined previously. someImage is already defined).
- (void)showFade {
[Fader setHidden:NO];
[Fader setImage: someImage];
[[Fader animator] setHidden:YES];
}
The problem is: it works for the first time, and the subview fades, but it never shows up again. What am I doing wrong?
EDIT: As requested, here is a more complete sample of the code
- (void)awakeFromNib {
Fader = [[NSImageView alloc] initWithFrame: [self bounds]];
Images[0] = #"Tower";
Images[1] = #"Desert";
Images[2] = #"Fall";
image = 0;
[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:#selector(showFader:) userInfo:nil repeats:YES];
[self addSubview: Fader];
[self nextImage];
[super awakeFromNib];
}
- (void)showFader: (NSTimer*)timer {
[Fader setImage: [self image]];
[Fader setHidden:NO];
[[Fader animator] setHidden:YES];
[self nextImage];
}
- (void)nextImage {
if (image == 2) {
image = 0;
}
else {
image++;
}
[self setImage: [NSImage imageNamed: Images[image]]];
}
So, basically, I have a repeating timer making the parent NSImageView to loop between an array of images and making the "Fader" to show up with the previous image and fade out. The problem is that it only shows up once.
I had the same problem a long time ago (yet I never solved it), but I think this could have something to do with the fact that you ask your view to be showed and dissolved in the same event loop.
I guess you should let the event loop make an iteration (so that your view is actually displayed) before asking to dissolve the view.
I guess you could do it by setting a (short) timer that would fire a couple of millisecs after the view is displayed (by setHidden:NO).
I also guess there are easier ways to solve the problem (and I would be glad to hear them).
What is more, you could also try animator setAlphaValue:0.0 to have a smooth transition.
I am not definitely not sure I have a correct answer, that's only a suggestion...
hidden is a BOOL property, you can't animate BOOLs cause there are only two discrete values: NO and YES. Use the opacity instead, you can animate opacity from 1.0 (fully visible) to 0.0 (invisible) to fade out. And hidden has to be set to NO for this to work, otherwise it will be hidden all the time.
This method works perfectly for me to make an image start out blurry and smoothly come into focus.
let parentView = UIView()
let imageView = UIImageView(image: UIImage(named: "ImageToFocus"))
parentView.addSubview(imageView)
let blurEffect = UIBlurEffect(style: .Light)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.frame = imageView.frame
blurView.alpha = 1.0
self.blurTimer = NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: Selector("animateBlur:"), userInfo: blurView, repeats: true)
func animateBlur(timer:NSTimer)
{
let blurView = timer.userInfo as UIVisualEffectView
if blurView.alpha > 0.0 {
blurView.alpha -= 0.05
} else {
blurTimer = nil
}
}