Animation of subviews in iOS one at a time - ios

I have 9 custom subviews that I create and lay out programmatically. I want to animate their creation and change of position each one at a time but right now the whole set of 9 subviews is being animated at the same time.
This is my code:
- (void)viewDidLoad{
//create 9 setCardViews and add them to the array setCards to be able to identify them later
for (int i=0; i<9; i++) {
SetCardView *card = [[SetCardView alloc]initWithFrame:CGRectMake(self.gameView.center.x, self.gameView.center.y, 0, 0)];
[self.gameView addSubview:card];
[self.setCards addObject:card];
}
//layout those 9 views
[self updateLayout];
}
Then, the updateLayout method is called. It sets a frame size inside of which the subviews are laid out:
-(void)updateLayout
{
CGRect canvas = [self resizeCanvasWithNumberOfCards:[self.setCards count]];
for (SetCardView *card in self.setCards)
{
[self layOutCard:card withFrame:canvas atIndex:[self.setCards indexOfObject:card]];
}
}
Finally, inside the layOutCard method I calculate the position of the view (I omit that part of the code here) and I animate the change in its frame:
-(void)layOutCard:(SetCardView *)card withFrame:(CGRect)canvas atIndex:(NSUInteger)index
{
//calculate the new frame of the view
CGRect rect = //calculations ;
[SetCardView animateWithDuration:0.5 delay:self.delay options: UIViewAnimationOptionCurveEasyInOut animations:^{card.frame=rect;} completion:nil];
}
So all these animations happen at the same time, maybe because it's inside a loop in the updateLayout method so the controller waits for it to finish? Anyway I can't think of a way to make the animation of the views without using a loop so that they animate one at a time.
How could I do that?
Thanks

One of the way you can make it works is increase delay when you enumerate cards in updateLayout and after that pass it to layOutCard:withFrame:atIndex (of course you have to add delay parameter to that method. Or ft index parameter start from 0 and increase by 1 every time you can use it to calculate delay:
-(void)layOutCard:(SetCardView *)card withFrame:(CGRect)canvas atIndex:(NSUInteger)index
{
//calculate the new frame of the view
CGRect rect = //calculations ;
float myDelay = 0.5 * index; //0.5 is your duration.
[SetCardView animateWithDuration:0.5 delay: myDelay options: UIViewAnimationOptionCurveEasyInOut animations:^{card.frame=rect;} completion:nil];
}

You have it done (almost). One small adjustment on your code. At a run time your for loop is executed very fast leaving you with the impression that all animations are starting at the same time. You are using start with delay on your animation block already so simply increase the delay every time you go into to loop like this.
-(void)updateLayout
{
CGRect canvas = [self resizeCanvasWithNumberOfCards:[self.setCards count]];
for (SetCardView *card in self.setCards)
{
[self layOutCard:card withFrame:canvas atIndex:[self.setCards indexOfObject:card]];
// increase the delay so next animation will start after this one is finished
// check if this card is the last from your array
if([card isEqual:[self.setCards lastObject]])
{
// if card is last object set self.delay back to 0
self.delay = 0;
{
else
{
// if card is not the last object increase the delay time for next animation
self.delay += 0.5; // your current animation duration
}
}
}

Related

Changing a frame in viewDidLayoutSubviews

When a view controller's view is first shown I want to run an animation in which all elements in the view controller slide from outside the bottom of the screen to their natural positions. To achieve this, I do subview.frame.origin.y += self.view.frame.size.height in viewDidLayoutSubviews. I also tried viewWillAppear, but it doesn't work at all. Then I animate them up to their natural positions with subview.frame.origin.y -= self.view.frame.size.height in viewDidAppear.
The problem is that viewDidLayoutSubviews is called several times throughout the view controller's lifespan. As such, when things like showing the keyboard happen all my content gets replaced outside the view again.
Is there a better method for doing this? Do I need to add some sort of flag to check whether the animation has already run?
EDIT: here's the code. Here I'm calling prepareAppearance in viewDidLayoutSubviews, which works, but viewDidLayoutSubviews is called multiple times throughout the controller's life span.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self prepareAppearance];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self animateAppearance];
}
- (NSArray *)animatableViews
{
return #[self.createAccountButton, self.facebookButton, self.linkedInButton, self.loginButton];
}
- (void)prepareAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
UIView * view = [views objectAtIndex:it];
CGRect frame = view.frame;
// Move the views outside the screen, to the bottom
frame.origin.y += self.view.frame.size.height;
[view setFrame:frame];
}
}
- (void)animateAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
__weak UIView * weakView = [views objectAtIndex:it];
CGRect referenceFrame = self.view.frame;
[UIView animateWithDuration:0.4f
delay:0.05f * it
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect frame = weakView.frame;
frame.origin.y -= referenceFrame.size.height;
[weakView setFrame:frame];
}
completion:^(BOOL finished) {
}];
}
}
If you need to animate something when view will appear and then not touch subviews later, I would suggest the following:
Don't change/touch viewDidLayoutSubviews
Add logic to move elements outside the screen (to their initial position before animation) in viewWillAppear
Add logic to animate elements into their proper position in viewDidAppear
UPDATE:
If you're using auto-layout (which is very good thing), you can't animate views by changing their frames directly (because auto-layout would ignore that and change them again). What you need to do is to expose outlets to constraints responsible for Y-position (in your case) and change that constraints rather then setting frames.
Also don't forget to include call to [weakView layoutIfNeeded] after you update constraints in the animation method.
I did both things in viewDidAppear:. It seems that when viewDidAppear: is called, the view is not actually visible, but about to. So the UI elements never show in their initial position if they are replaced there.

Custom UIDynamicBehavior: how do I know the total number of animation steps/ticks?

I'd like to implement a custom UIDynamicBehavior that makes a view "burst". To do that I need to fade it out and scale it to 2x its size.
I do this by setting the view's alpha and bounds in the action block. However, how do I know how often the action block is called? The docs say "on each tick", but how many?
I added a counter. With no other animations, the block is called 30 times. With a few gravity and dynamic behaviors, it is called 500 times.
I also don't see how the UIDynamicAnimator knows when its behaviors are "done" moving stuff around. Can anyone shed some light on this?
The code below works sometimes, but other times the behavior stops before the view is animated completely (i.e. it is still visible).
self.action = ^{
static NSInteger count = 0;
NSLog(#"animation tick: %d", count);
count++;
UIView *view = (UIView*)[weakSelf.items lastObject];
view.alpha = view.alpha - 0.1;
CGRect bounds = view.bounds;
bounds.size.width += 1;
bounds.size.height += 1;
view.bounds = bounds;
};
For detecting when the animation has finished you could try this:
__weak Entity *weakSelf = self;
self.behavior.action = ^{
if (weakSelf.center.x == weakSelf.lastPosition.x && weakSelf.center.y == weakSelf.lastPosition.y) {
NSLog(#"end of dynamic movement");
}
weakSelf.lastPosition = weakSelf.center;
};
Or check another value that you are changing.

Animate view repeatedly and continuously

I'm simulating some effects, like leafs falling, snow and rain.
My first call was to use CAEmitterLayer, that works very well, but i need to render the layer in context to save as an image, and that seens impossible or, at least, very complicated.
So i am working with some view animations. I need to continuously animate my imageViews so it can fall through screen and reappear on top of it, each one with diffenrent speed.
So i relocate each imageView on screen with animateWithDuration and when animation is done i call the method that do that recursively from completion block so the imageview can make it's way to the end of screen. Once an imageView reachs the end of screen i relocate it on top.
The problem is when animation is over my imageView stops a little bit until the completion block calls the method recursively, i want it to be continuously.
My code generates random position for each imageView, and my animation duration is very short so i can always update imageView's location in case the user tap the button to save the view as an image.
Anyway, here is my code:
- (void)effectOnObject:(UIImageView *)object{
NSInteger initialX;
NSInteger initialy;
CGFloat duration;
//the object have reached the end of screen
if (object.frame.origin.y >= self.frame.size.height) {
//generate random position to relocate the object on x axis
initialX = arc4random() % 321;
//generate random position to relocate the object on y axis (little bit above top of screen)
initialy = ((NSInteger)-object.frame.size.height) - arc4random() % 11;
//duration 0 so the object can be replaced without animation
duration = 0.0;
}
else{
initialX = object.frame.origin.x;
//this change the speed of object
initialy = object.frame.origin.y + 10
//setted duration to short time
duration = 0.01;
}
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionAllowAnimatedContent
animations:^{
[object setFrame:CGRectMake(initialX, initialy, object.frame.size.width, object.frame.size.height)];
}
completion:^(BOOL completed){
//recursively call
[self effectOnObject:object];
}];
}
- (void)startEffect{
//initiate effect on each imageView
for (UIImageView *object in self.subviews) {
[self effectOnObject:object];
}
}
How can i make this animation repeatedly and continuously, without the gap when the method is called recursively from completion block and the animation restarts?
You don't need to do the animation with 0 duration when the object reaches the end of the screen. Just test for this in your completion block and relocate the view before calling recursively. Also, a duration of 0.01 seems a bit extreme (100 frames per second). Perhaps a more reasonable value like 0.1 would help.
Something like this:
- (void)effectOnObject:(UIImageView *)object{
//setted duration to short time
CGFloat duration = 0.1;
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionAllowAnimatedContent
animations:^{
NSInteger initialX = object.frame.origin.x;
//this change the speed of object
NSInteger initialy = object.frame.origin.y + 10;
[object setFrame:CGRectMake(initialX, initialy, object.frame.size.width, object.frame.size.height)];
}
completion:^(BOOL completed){
NSInteger initialX = arc4random() % 321;
//generate random position to relocate the object on y axis (little bit above top of screen)
NSInteger initialy = ((NSInteger)-object.frame.size.height) - arc4random() % 11;
[object setFrame:CGRectMake(initialX, initialy, object.frame.size.width, object.frame.size.height)];
//recursively call
[self effectOnObject:object];
}];
}
From what I read briefly you should check out CAReplicatorLayer. It is perfect for repeating stuff! Only issue I can think of is that the transform and values have to be incremental...

ios 7 - UIView animate method with delay is not delayed

I'm currently testing new iOS 7 views controller transition.
What i want is a custom modal presenting transition that present your next view cut into several strip from top off screen. Each strip should appear after an incremental delay to give the desired effect.
So my code looks like this :
- (void)presentModalWithContext:(id<UIViewControllerContextTransitioning>)context
{
UIView *inView = [context containerView];
UIView *fromView = [context viewControllerForKey:UITransitionContextFromViewControllerKey].view;
UIView *toView = [context viewControllerForKey:UITransitionContextToViewControllerKey].view;
NSTimeInterval stripTime = 1.0;
NSTimeInterval stripDelay = 1.0;
NSInteger stripCount = 10;
CGFloat stripHeight = toView.frame.size.height / stripCount;
for (NSInteger i = 0; i < stripCount; i++)
{
CGFloat offsetY = i*stripHeight;
CGRect snapRect = CGRectMake(0, offsetY, toView.frame.size.width, stripHeight);
UIView *view = [toView resizableSnapshotViewFromRect:snapRect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];
CGRect stripRect = CGRectMake(0, -(stripCount-i)*stripHeight, snapRect.size.width, snapRect.size.height);
view.frame = stripRect;
[inView insertSubview:view aboveSubview:fromView];
NSTimeInterval interval = stripDelay*(stripCount-i);
[UIView animateWithDuration:stripTime delay:interval options:0 animations:^{
CGPoint center = view.center;
center.y += stripCount*stripHeight;
view.center = center;
} completion:^(BOOL finished) {
NSLog(#"complete");
if (i == stripCount-1)
[context completeTransition:YES];
}];
}
}
I've already checked initial and final position of each strip and already is OK. My interval variable is also properly set at each loop.
But it seems that this is not delayed at all. All strips are moving together, giving the impression that the complete view is moving.
A quick look to basic log shows that all animations are performed at the same time :
2013-09-20 01:11:32.908 test_transition[7451:a0b] complete
2013-09-20 01:11:32.909 test_transition[7451:a0b] complete
2013-09-20 01:11:32.910 test_transition[7451:a0b] complete
2013-09-20 01:11:32.910 test_transition[7451:a0b] complete
2013-09-20 01:11:32.911 test_transition[7451:a0b] complete
2013-09-20 01:11:32.911 test_transition[7451:a0b] complete
2013-09-20 01:11:32.912 test_transition[7451:a0b] complete
2013-09-20 01:11:32.912 test_transition[7451:a0b] complete
2013-09-20 01:11:32.913 test_transition[7451:a0b] complete
2013-09-20 01:11:32.913 test_transition[7451:a0b] complete
Do someone is able to spot what's wrong here ?
EDIT :
It seems this is the following line that cancel the delay of any animations, even if those are not concerning the view being snapshotted :
UIView *view = [toView resizableSnapshotViewFromRect:snapRect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];
If i set the parameter afterScreenUpdates to NO, the view snapshot is null and i get the following error log :
Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has been rendered at least once before snapshotting or snapshot after screen updates.
How do i render the view before snapshotting ? I tried [toView setNeedsDisplay] but with no success ...
Here's a solution.
Although this question is 2 years old, it's still a pertinent one as it still exists on iOS9. I realize it miiight not be as much help to the asker seeing it's been 2 years, but I only just came across this. So here's a solution.
When you want to transition between view controllers, you're probably gonna be using an Animator Object to run your custom UIView block animation code. This might be sophisticated with multiple animation blocks and some using a delay value. But if during your transition you want to capture or screenshot a portion of something (whether it's by UIView.drawViewHierarchyInRect, UIView.snapshotViewAfterScreenUpdates, or UIView.resizableSnapshotViewFromRect), using any of these methods will disengage the delays in your animation. For the last 2 methods, if you pass false for afterScreenUpdates, then it won't disengage the delays, but it also won't capture anything; it has to be true to capture something, but setting it to true will disengage your delay.
Using any of these methods will disengage the delay in your animation block, and generally mess things up. If you tell UIKit the transition is gonna be 3 secs and you have an animation block (UIView.animateWithDuration...) that has a 2 sec delay and 1 sec animation, if your delay gets disengaged then the animation runs instantly and the transition lasts just 1 sec, which throws UIKit out of sync and stuff gets generally messed up cuz it was expecting it to be 3 secs.
Here's a solution:
Say you're transition from view controller A to view controller B. In your Animator Object's code file (an object that inherits from NSObject and conforms to UIViewControllerAnimatedTransitioning protocol), you put your animation code in the animateTransition(transitionContext: UIViewControllerContextTransitioning) { ...} delegate method. If you're transitioning from VC A to VC B, say you want to screenshot something from VC B, and show it and do something with it during the transition. One way that works perfectly, is to use the drawViewHierarchyInRect method in View Controller B's viewDidLoad method, to capture and store the image (or create a UIImageView from that image and store the UIImageView) in an instance property of View Controller B. You need to use the drawViewHierarchyInRect method and not any of the other two because those require the content to be visible on screen (i.e. already rendered); the drawViewHiearchyInRect method captures offScreen content, even if it's not added to the view.
As soon as you commence a transition, the 'to view controller' gets initialized and it's viewDidLoad gets called. So in your transition animation code, you can grab the image you screenshotted (or the imageView) by referring to the view controller's property and do whatever you want with it in your transition. Your delays will not be disengaged.
Main Point: Don't screenshot stuff during the transition. Instead, put the screenshot code in the viewDidLoad of the view controller, and store its output in an instance variable and refer to that in your transition.
Hope this helps, I only just came across this problem today and just came across a solution.
After working on your code for a bit, and comparing it to mine, where the delay parameter was honored correctly, I still can't figure out why yours doesn't work. In any case, I found another way that does work. I break the animation into two parts. In the first part, I create the slices of the view using your code, add them to the inView, and also to a mutable array. In the second part, I call the animation block recursively, with no delay, until the last strip is displayed. One limitation to this approach, is that each strip animation has to complete before the next one begins (since the next one is called from the completion block), so you don't have independent control over the duration and delay. Anyway, here is what I did. In the presenting view controller, I just do this:
-(IBAction)presntBlue:(id)sender {
BlueViewController *blue = [self.storyboard instantiateViewControllerWithIdentifier:#"Blue"];
blue.modalTransitionStyle = UIModalPresentationCustom;
blue.transitioningDelegate = self;
[self presentViewController:blue animated:YES completion:nil];
}
-(id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
RDPresentationAnimator *animator = [RDPresentationAnimator new];
animator.isPresenting = YES;
return animator;
}
And in the RDPresentationAnimator class, I have this:
#interface RDPresentationAnimator () {
NSInteger stripCount;
CGFloat stripHeight;
NSMutableArray *stripArray;
}
#end
#implementation RDPresentationAnimator
#define ANIMATION_TIME .3
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
return ANIMATION_TIME;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)context {
UIView *inView = [context containerView];
UIView *toView = [context viewControllerForKey:UITransitionContextToViewControllerKey].view;
stripCount = 10;
stripHeight = toView.frame.size.height / stripCount;
stripArray = [NSMutableArray new];
for (NSInteger i = 0; i < stripCount; i++)
{
CGFloat offsetY = i*stripHeight;
CGRect snapRect = CGRectMake(0, offsetY, toView.frame.size.width, stripHeight);
UIView *view = [toView resizableSnapshotViewFromRect:snapRect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];
CGRect stripRect = CGRectMake(0, -(stripCount-i)*stripHeight, snapRect.size.width, snapRect.size.height);
view.frame = stripRect;
[inView addSubview:view];
[stripArray addObject:view];
}
[self animateStrip:stripCount - 1 withContext:context];
}
-(void)animateStrip:(NSInteger) index withContext:(id <UIViewControllerContextTransitioning>) context{
[UIView animateWithDuration:ANIMATION_TIME animations:^{
UIView *view = stripArray[index];
CGPoint center = view.center;
center.y += stripCount*stripHeight;
view.center = center;
} completion:^(BOOL finished) {
if (index >0) {
[self animateStrip:index - 1 withContext:context];
}else{
[context completeTransition:YES];
};
}];
}
I thought I'd add another answer that does give you the independent control over the stripTime and stripDelay. I never did find a way to make it work using the new UIViewControllerContextTransitioning methods. This way uses normal UIView animations, followed by a no animation presentViewController. This method should work correctly in either portrait or landscape orientation (notice that I use self.view.bounds to calculate stripHeight and snapRect, so that those values will be correct for either orientation).
#interface ViewController () {
NSInteger stripCount;
CGFloat stripHeight;
NSMutableArray *stripArray;
}
#end
#implementation ViewController
-(IBAction)presntBlue:(id)sender {
BlueViewController *blue = [self.storyboard instantiateViewControllerWithIdentifier:#"Blue"];
[self animateView:blue];
}
-(void)animateView:(UIViewController *) toVC; {
UIView *toView = toVC.view;
toView.frame = self.view.bounds;
[self.view addSubview:toView];
NSTimeInterval stripDelay = 0.2;
NSTimeInterval stripTime = 1.0;
stripCount = 10;
stripHeight = self.view.bounds.size.height / stripCount;
stripArray = [NSMutableArray new];
for (NSInteger i = 0; i < stripCount; i++) {
CGFloat offsetY = i*stripHeight;
CGRect snapRect = CGRectMake(0, offsetY, self.view.bounds.size.width, stripHeight);
UIView *view = [toView resizableSnapshotViewFromRect:snapRect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];
CGRect stripRect = CGRectMake(0, -(stripCount-i)*stripHeight, snapRect.size.width, snapRect.size.height);
view.frame = stripRect;
[self.view addSubview:view];
[stripArray addObject:view];
}
[toView removeFromSuperview];
for (NSInteger i = 0; i < stripCount; i++) {
NSTimeInterval interval = stripDelay*(stripCount-i);
UIView *view = stripArray[i];
[UIView animateWithDuration:stripTime delay:interval options:0 animations:^{
CGPoint center = view.center;
center.y += stripCount*stripHeight;
view.center = center;
} completion:^(BOOL finished) {
if (i == 0){
[self presentViewController:toVC animated:NO completion:nil];
}
}];
}
}
Added note:
In the animateView: method, I add the toView to self.view,, and then remove it after making the strips. I do this to make sure it works correctly in portrait and landscape -- if I omit those two statements, there's a slight glitch in the landscape animation when the animation finishes. If I have those two lines in, I occasionally get a glitch at the beginning where you can see the whole toView for a brief flash. I don't know why this only happens occasionally, and I haven't updated my phone yet, so I don't know if this happens on the device as well.

Animate multiple views in a subviews with delay less than 1 second

just started to use Obj C for iOS, I was trying to animate all the views (UIImageView) inside my subviews, but when I set the delay time to less than 1 second, the animation would animate all views at once. If i set the delay to 1 second or more, it will be animate correctly but the second animation will only start after the first one completes. I want to animate all those views back to back for example view number 1 would start animating, before it completes, the second should start to animate.
Here are my code.
-(void)animate
{
int delaytime = 0.3; //first view will start after 0.3 secs
for(UIView *view in self.subviews)
{
view.alpha = 0; //set it to transparent
delaytime += 1 ; //couldn't get the desired effect when it is less than 1
[UIView animateWithDuration:0.3
delay:delaytime
options: UIViewAnimationOptionTransitionNone
animations:^{
view.alpha = 1; // fade in }
completion:^(BOOL finished){
NSLog(#"Complete");
}];
}
}
Your delay variable is an int and is therefore always rounded down to the integer value (0.3 becomes 0 and even 0.9999 becomes 0). This is the reason why all "delays" less than one second starts immediately for you, an int variable can't hold decimal values.
You should instead us the correct type NSTimeInterval
NSTimeInterval delaytime = 0.3;

Resources