Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to 'cancel' view appearance transitions for custom container controller transitions

I have created a custom container controller that works similarly to a UIPageViewController so that I could implement some custom transitions & data source logic. I tried to mimic the way the new customer view controller transition APIs work in iOS 7, and it works well except for some irritating quirks with the view appearance callbacks when a transition is cancelled...

Namely, when performing a transition, when exactly should beginAppearanceTransition:animated: and endAppearanceTransition be called?


My custom container class has some code like this:

- (BOOL)shouldAutomaticallyForwardAppearanceMethods
{
    return NO;  // Since the automatic callbacks are wrong for our custom transition.
}

- (void)startTransition:(CustomTransitionContext *)context
{
    // Get reference to the view controllers involved in the transition.
    UIViewController *oldVC = [context viewControllerForKey:UITransitionContextFromViewController];
    UIViewController *newVC = [context UITransitionContextToViewController];
    
    // Prepare parent/child relationship changes.
    [oldVC willMoveToParentViewController:nil];
    [self addChildViewController:newVC];

    // Begin view appearance transitions.
    [oldVC beginAppearanceTransition:NO animated:[context isAnimated]];
    [newVC beginAppearanceTransition:YES animated:[context isAnimated]];
    
    // Register a completion handler to run when the context's completeTransition: method is called.
    __weak CustomContainerController *weakSelf = self;
    context.transitionCompletionHandler = ^(BOOL complete) {
        // End appearance transitions here?
        [oldVC endAppearanceTransition];
        [newVC endAppearanceTransition];

        if (complete) {
            // Or only if the transition isn't cancelled; here?
            [oldVC endAppearanceTransition];
            [newVC endAppearanceTransition];

            [oldVC removeFromParentViewController];
            [newVC didMoveToParentViewController:weakSelf];
        } else {
            [newVC removeFromParentViewController];
            [oldVC didMoveToParentViewController];
        }
    }

    if ([context isInteractive] && [self.transitionController conformsToProtocol:@protocol(UIViewControllerInteractiveTransitioning)]) {
        // Start the interactive transition.
        [self.transitionController startInteractiveTransition:context];
    } else if ([context isAnimated] && [self.transitionController conformsToProtocol:@protocol(UIViewControllerAnimatedTransitioning)]) {
        // Start the animated transition.
        [self.transitionController animateTransition:context];
    } else {
        // Just complete the transition.
        [context completeTransition:YES];
    }
}

So if I call endAppearanceTransition regardless of whether the transition was cancelled, then my view callbacks look like this when a transition is cancelled:

oldVC viewWillDisappear:  // Fine
newVC viewWillAppear:     // Fine
// ... some time later transition is cancelled ...
oldVC viewDidDisappear:   // Wrong! This view controller's view is staying.
newVC viewDidAppear:      // Wrong! The appearance of this view controllers view was cancelled.

If I call endAppearanceTransition only when the transition finished successfully, things look better at first:

oldVC viewWillDisappear:  // Fine
newVC viewWillAppear:     // Fine
// ... some time later transition is cancelled ...
// ... silence. (which is correct - neither view actually appeared or disappeared,
//               and I can undo side effects in viewWill(Dis)Appear using the 
//               transitionCoordinator object)

But then, the next time I start a transition, I receive no view appearance callbacks. The next set of view appearance callbacks arrive only after a beginAppearanceTransition:animated: that is followed by an endApperanceTransition call. It is worth noting that I don't receive the oft-reported "Unbalanced calls to begin/end appearance transitions for ViewController" console warning.

In WWDC 2013 Session 218 (Custom Transitions Using View Controllers) Bruce Nilo makes a rather pertinent joke about his colleagues telling him that viewWillAppear: & viewWillDisappear: really ought to be called viewMightAppear: & viewMightDisappear: (see the section of that session beginning at 42:00). Given that we're now in the realm of cancellable interactive gestures, it seems that we need a cancelAppearanceTransition (or endAppearanceTransition:(BOOL)finished) method for custom containers.

Is anybody aware of what I'm doing wrong? Or are cancellable, custom transitions in custom containers just not supported properly yet?

like image 379
Stuart Avatar asked Jan 23 '14 11:01

Stuart


1 Answers

So as is often the way with SO, you spend hours fruitlessly working on a problem, and then right after you post the question, you find the answer...

I looked at the appearance callbacks of UINavigationController's built-in interactivePopGestureRecognizer to see what happens when the cancelling the transition, and it appears that my assumption about what appearance methods should be called was wrong.

For a view controller that is successfully transitioned to, the following callbacks are received (as you'd expect):

                 transition
                  finished
viewWillAppear:  ---------->  viewDidAppear:

But for a view controller transition that is unsuccessful (i.e. the transition was cancelled), the view controller receives the following callbacks:

                 transition                       immediately
                 cancelled                        followed by
viewWillAppear:  ---------->  viewWillDisappear:  ---------->  viewDidDisappear:

So we get a bit more of those dodgy view appearance semantics; I'm not sure how a view can disappear if it hasn't yet appeared! Oh well... I suppose it's slightly better than a view saying it will appear, and then nothing happening.

So, returning to my CustomTransitionContext's transitionCompletionHandler code, the solution looks like this:

...

// Register a completion handler to run when the context's completeTransition: method is called.
__weak CustomContainerController *weakSelf = self;
__weak CustomTransitionContext *weakContext = context;
context.transitionCompletionHandler = ^(BOOL complete) {
    if (complete) {
        // End the appearance transitions we began earlier.
        [oldVC endAppearanceTransition];
        [newVC endAppearanceTransition];

        [oldVC removeFromParentViewController];
        [newVC didMoveToParentViewController:weakSelf];
    } else {
        // Before ending each appearance transition, begin an
        // appearance transition in the opposite direction.
        [newVC beginAppearanceTransition:NO animated:[weakContext isAnimated]];
        [newVC endAppearanceTransition];
        [oldVC beginAppearanceTransition:YES animated:[weakContext isAnimated]];
        [oldVC endAppearanceTransition];

        [newVC removeFromParentViewController];
        [oldVC didMoveToParentViewController];
    }
}

....

So now the appearance callbacks look like this:

oldVC viewWillDisappear:
newVC viewWillAppear:
// ... some time later transition is cancelled ...
newVC viewWillDisappear:
newVC viewDidDisappear:
oldVC viewWillAppear:
oldVC viewDidAppear:

Also, subsequent transitions now trigger the callbacks as expected (since we're closing off all transitions with calls to endAppearanceTransition).

like image 195
Stuart Avatar answered Oct 12 '22 16:10

Stuart