Как "отменить" видимость переходов для пользовательских переходов контроллера контейнера

Я создал пользовательский контроллер контейнера, который работает аналогично UIPageViewController, чтобы я мог реализовать некоторые пользовательские переходы и логику источника данных. Я попытался подражать тому, как новые API-интерфейсы API-интерфейса для контроля доступа работают в iOS 7, и он хорошо работает, за исключением некоторых раздражающих причуд с обратными вызовами внешнего вида, когда переход отменен...

А именно, при выполнении перехода, когда нужно называть beginAppearanceTransition:animated: и endAppearanceTransition?


В моем пользовательском классе контейнера есть такой код:

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

Итак, если я вызываю endAppearanceTransition независимо от того, был ли переход отменен, тогда мои обратные вызовы вида выглядят так, когда переход отменяется:

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

Если я вызываю endAppearanceTransition только когда переход завершен успешно, сначала все выглядит лучше:

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)

Но затем, в следующий раз, когда я начну переход, я получаю отсутствие обратных вызовов внешнего вида. Следующий набор обратных вызовов вида появляется только после beginAppearanceTransition:animated:, за которым следует вызов endApperanceTransition. Стоит отметить, что я не получаю сообщение "Неуравновешенные вызовы для начала/окончания видимых переходов для консоли управления ViewController".

В WWDC 2013 Session 218 (пользовательские переходы с использованием контроллеров просмотров) Брюс Нило делает довольно уместную шутку о своих коллегах, говорящих ему, что viewWillAppear: и viewWillDisappear: действительно следует называть viewMightAppear: и viewMightDisappear: (см. раздел этого сеанса, начинающийся в 42:00). Учитывая, что мы сейчас находимся в сфере отмены интерактивных жестов, нам кажется, что нам нужен метод cancelAppearanceTransition (или endAppearanceTransition:(BOOL)finished) для пользовательских контейнеров.

Кто-нибудь знает, что я делаю неправильно? Или могут быть отменены, пользовательские переходы в пользовательских контейнерах просто не поддерживаются должным образом?

Ответ 1

Так часто бывает с SO, вы бесцельно работаете над проблемой, а затем сразу после того, как задаете вопрос, вы найдете ответ...

Я просмотрел обратные вызовы внешнего вида UINavigationController встроенный interactivePopGestureRecognizer, чтобы узнать, что происходит при отмене перехода, и кажется, что мое предположение о том, какие методы внешнего вида следует вызывать, было неправильным.

Для контроллера с просмотром, который успешно перешел, получают следующие обратные вызовы (как и следовало ожидать):

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

Но для перехода контроллера контроллера, который не увенчался успехом (т.е. переход был отменен), контроллер просмотра получает следующие обратные вызовы:

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

Таким образом, мы получаем немного больше этой изворотливой семантики внешнего вида; Я не уверен, как взгляд может исчезнуть, если он еще не появился! О, хорошо... Полагаю, это немного лучше, чем мнение, показывающее, что оно будет, а затем ничего не происходит.

Итак, возвращаясь к моему коду CustomTransitionContext transitionCompletionHandler, решение выглядит следующим образом:

...

// Register a completion handler to run when the context 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];
    }
}

....

Итак, теперь обратные вызовы внешнего вида выглядят следующим образом:

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

Кроме того, последующие переходы теперь вызывают обратные вызовы, как ожидалось (поскольку мы закрываем все переходы с вызовами endAppearanceTransition).