Подтвердить что ты не робот

Пользовательский контейнерный контроллер: переходFromViewController: просмотр не выполняется должным образом перед анимацией

Это вопрос и решение partial.


* Пример проекта здесь:

https://github.com/JosephLin/TransitionTest


Проблема 1:

При использовании transitionFromViewController:... макеты, сделанные toViewController viewWillAppear:, не отображаются, когда начинается анимация перехода. Другими словами, представление перед макетом отображается во время анимации, и это содержимое привязывается к позициям после макета после анимации.

Проблема 2:

Если я настраиваю фон моего навигатора UIBarButtonItem, кнопка панели отображается с неправильным размером/позицией перед анимацией и привязывается к правильному размеру/позиции, когда анимация заканчивается, аналогично задаче 1.


Чтобы продемонстрировать проблему, я создал пользовательский контейнерный контроллер, который выполняет некоторые пользовательские переходы. Это в значительной степени копия UINavigationController, которая перекрестно растворяется вместо push-анимации между представлениями.

Метод "Push" выглядит следующим образом:

- (void)pushController:(UIViewController *)toViewController
{
    UIViewController *fromViewController = [self.childViewControllers lastObject];

    [self addChildViewController:toViewController];
    toViewController.view.frame = self.view.bounds;

    NSLog(@"Before transitionFromViewController:");
    [self transitionFromViewController:fromViewController
                      toViewController:toViewController
                              duration:0.5
                               options:UIViewAnimationOptionTransitionCrossDissolve
                            animations:^{}
                            completion:^(BOOL finished) {
                                [toViewController didMoveToParentViewController:self];
                            }];
}

Теперь, DetailViewController (контроллер просмотра, на который я нажимаю), нужно разместить его содержимое в viewWillAppear:. Он не может сделать это в viewDidLoad, потому что в то время у него не было бы правильного кадра.

Для демонстрационной цели DetailViewController устанавливает свою метку в разные местоположения и цвета в viewDidLoad, viewWillAppear и viewDidAppear:

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"%s", __PRETTY_FUNCTION__);
    CGRect rect = self.descriptionLabel.frame;
    rect.origin.y = 50;
    self.descriptionLabel.frame = rect;
    self.descriptionLabel.text = @"viewDidLoad";
    self.descriptionLabel.backgroundColor = [UIColor redColor];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    NSLog(@"%s", __PRETTY_FUNCTION__);
    CGRect rect = self.descriptionLabel.frame;
    rect.origin.y = 200;
    self.descriptionLabel.frame = rect;
    self.descriptionLabel.text = @"viewWillAppear";
    self.descriptionLabel.backgroundColor = [UIColor yellowColor];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSLog(@"%s", __PRETTY_FUNCTION__);
    CGRect rect = self.descriptionLabel.frame;
    rect.origin.y = 350;
    self.descriptionLabel.frame = rect;
    self.descriptionLabel.text = @"viewDidAppear";
    self.descriptionLabel.backgroundColor = [UIColor greenColor];
}

Теперь, когда вы нажимаете DetailViewController, я ожидаю увидеть метку в y =200 в начале анимации (левое изображение), а затем перейдет к y = 350 после завершения анимации (правое изображение).

enter image description hereenter image description here Ожидаемый просмотр до и после анимации.


Однако метка находилась в y=50, как если бы макет, сделанный в viewWillAppear, не сделал его до того, как анимация состоялась (левое изображение). Но обратите внимание, что фон метки был установлен на желтый (цвет, указанный viewWillAppear)!

enter image description hereenter image description here Неверный макет в начале анимации. Обратите внимание, что кнопки панели также начинаются с неправильного положения/размера.


Журнал консоли
TransitionTest [49795: c07] - [DetailViewController viewDidLoad]
TransitionTest [49795: c07] До переходаFromViewController:
TransitionTest [49795: c07] - [DetailViewController viewWillAppear:]
TransitionTest [49795: c07] - просмотр [DetailViewControllerWillLayoutSubviews]
TransitionTest [49795: c07] - [DetailViewController viewDidLayoutSubviews]
TransitionTest [49795: c07] - [DetailViewController viewDidAppear:]
Обратите внимание, что viewWillAppear: был вызван AFTER transitionFromViewController:


Решение проблемы 1

Хорошо, вот часть частичного решения. Явным образом вызывая beginAppearanceTransition: и endAppearanceTransition до toViewController, представление будет иметь правильную компоновку до того, как произойдет анимация перехода:

- (void)pushController:(UIViewController *)toViewController
{
    UIViewController *fromViewController = [self.childViewControllers lastObject];

    [self addChildViewController:toViewController];
    toViewController.view.frame = self.view.bounds;

    [toViewController beginAppearanceTransition:YES animated:NO];

    NSLog(@"Before transitionFromViewController:");
    [self transitionFromViewController:fromViewController
                      toViewController:toViewController
                              duration:0.5
                               options:UIViewAnimationOptionTransitionCrossDissolve
                            animations:^{}
                            completion:^(BOOL finished) {
                                [toViewController didMoveToParentViewController:self];
                                [toViewController endAppearanceTransition];
                            }];
}

Обратите внимание, что viewWillAppear: теперь называется ПЕРЕД transitionFromViewController: TransitionTest [18398: c07] - [DetailViewController viewDidLoad]
TransitionTest [18398: c07] - [DetailViewController viewWillAppear:]
TransitionTest [18398: c07] До переходаFromViewController:
TransitionTest [18398: c07] - просмотр [DetailViewControllerWillLayoutSubviews]
TransitionTest [18398: c07] - [DetailViewController viewDidLayoutSubviews]
TransitionTest [18398: c07] - [DetailViewController viewDidAppear:]


Но это не устраняет проблему 2!

По какой-то причине кнопки навигации по-прежнему начинаются с неправильного положения/размера в начале анимации перехода. Я потратил так много времени, пытаясь найти правильное решение, но без везения. Я начинаю чувствовать, что это ошибка в transitionFromViewController: или UIAppearance или что-то еще. Пожалуйста, любое понимание, которое вы можете предложить по этому вопросу, очень ценится. Спасибо!


Другие решения, которые я пробовал

  • Вызов [self.view addSubview:toViewController.view]; до transitionFromViewController:

Фактически он дает точно правильный результат пользователю, исправляет обе проблемы 1 и 2. Проблема в том, что viewWillAppear и viewDidAppear будут вызываться дважды! Это проблематично, если я хочу сделать некоторую экспансивную анимацию или вычисление в viewDidAppear.

  • Вызов [toViewController viewWillAppear:YES]; до transitionFromViewController:

Я думаю, что это почти так же, как вызов beginAppearanceTransition:. Он исправляет проблему 1, но не проблема 2. Кроме того, в документе говорится, что он не вызывает viewWillAppear напрямую!

  • Используйте [UIView animateWithDuration:] вместо transitionFromViewController:

Вот так:   [self addChildViewController: toViewController];   [self.view addSubview: toViewController.view];   toViewController.view.alpha = 0.0;

[UIView animateWithDuration:0.5 animations:^{
    toViewController.view.alpha = 1.0;
} completion:^(BOOL finished) {
    [toViewController didMoveToParentViewController:self];
}];

Он исправляет проблему 2, но представление начиналось с макета в viewDidAppear (метка зеленая, y = 350). Кроме того, кросс-растворение не так хорошо, как использование UIViewAnimationOptionTransitionCrossDissolve

4b9b3361

Ответ 1

Хорошо, добавив layoutIfNeeded в toViewController.view, похоже, делает трюк - это правильно отображает представление, прежде чем оно появится на экране (без добавления/удаления) и не более странного двойного viewDidAppear: call.

- (void)pushController:(UIViewController *)toViewController
{
    UIViewController *fromViewController = [self.childViewControllers lastObject];

    [self addChildViewController:toViewController];

    toViewController.view.frame = self.view.bounds;
    [toViewController.view layoutIfNeeded];

    NSLog(@"Before transitionFromViewController:");
    [self transitionFromViewController:fromViewController
                      toViewController:toViewController
                              duration:0.5
                               options:UIViewAnimationOptionTransitionCrossDissolve
                            animations:^{}
                            completion:^(BOOL finished) {
                            }];

}

Ответ 2

Если бы та же проблема, все, что вам нужно, это пересылка транзакций внешнего вида и UIView.Animate. Этот подход устраняет все проблемы, не создает новых. Вот код С# (xamarin):

var fromView = fromViewController.View;
var toView = toViewController.View;

fromViewController.WillMoveToParentViewController(null);
AddChildViewController(toViewController);

fromViewController.BeginAppearanceTransition(false, true);
toViewController.BeginAppearanceTransition(true, true);

var frame = fromView.Frame;
frame.X = -viewWidth * direction;
toView.Frame = frame;
View.Add(toView);

UIView.Animate(0.3f,
    animation: () =>
    { 
        toView.Frame = fromView.Frame; 
        fromView.MoveTo(x: viewWidth * direction);
    },
    completion: () =>
    {
        fromView.RemoveFromSuperview();

        fromViewController.EndAppearanceTransition();
        toViewController.EndAppearanceTransition();

        fromViewController.RemoveFromParentViewController();
        toViewController.DidMoveToParentViewController(this);
    }
);

и, конечно же, вы должны отключить автоматическую пересылку методов внешнего вида и сделать это вручную:

public override bool ShouldAutomaticallyForwardAppearanceMethods
{
    get { return false; }
}

public override void ViewWillAppear(bool animated)
{
    base.ViewWillAppear(animated);
    CurrentViewController.BeginAppearanceTransition(true, animated);
}

public override void ViewDidAppear(bool animated)
{
    base.ViewDidAppear(animated);
    CurrentViewController.EndAppearanceTransition();
}

public override void ViewWillDisappear(bool animated)
{
    base.ViewWillDisappear(animated);
    CurrentViewController.BeginAppearanceTransition(false, animated);
}

public override void ViewDidDisappear(bool animated)
{
    base.ViewDidDisappear(animated);
    CurrentViewController.EndAppearanceTransition();
}