Пользовательский Контейнерный Контроллер: transitionFromViewController: Представление не правильно расположено перед анимацией

Это и вопрос, и частичное решение.


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

https://github.com/JosephLin/TransitionTest


Проблема 1:

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

Проблема 2:

Если я настрою фон моего navbar's 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 после окончания анимации (правое изображение).

введите описание изображения здесьвведите описание изображения здесь Ожидаемый вид до и после анимации.


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

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


Консольный журнал
TransitionTest [49795: c07] - [DetailViewController viewDidLoad]
TransitionTest [49795: c07] Перед transitionFromViewController:
TransitionTest [49795: c07] - [Представление DetailViewControllerWillAppear:]
TransitionTest [49795: c07] - [DetailViewController viewWillLayoutSubviews]
TransitionTest [49795: c07] - [DetailViewController viewDidLayoutSubviews]
TransitionTest [49795: c07] - [DetailViewController viewDidAppear:]
Заметить, что viewWillAppear: был назван ПОСЛЕ 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] - [Представление DetailViewControllerWillAppear:]
TransitionTest [18398: c07] Перед transitionFromViewController:
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 (метка зеленая, у =350). Кроме того, перекрестное растворение не так хорошо, как при использовании UIViewAnimationOptionTransitionCrossDissolve

2 ответа

Решение

Хорошо, добавление 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) {
                            }];

}

Возникла та же проблема, все, что вам нужно, это пересылка транзакций внешнего вида и UIView.Animate. Такой подход решает все проблемы, не создает новых. Вот код C# (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();
}
Другие вопросы по тегам