Yaman
Yaman

Reputation: 3991

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 ...

Upvotes: 0

Views: 3933

Answers (3)

Peter Parker
Peter Parker

Reputation: 2201

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.

Upvotes: 2

rdelmar
rdelmar

Reputation: 104082

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.

Upvotes: 1

rdelmar
rdelmar

Reputation: 104082

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];
        };
    }];
}

Upvotes: 1

Related Questions