自定义容器控制器:transitionFromViewController:动画之前视图未正确布局

24

这既是一个问题,也是一个部分解决方案。


*示例项目在此处:

https://github.com/JosephLin/TransitionTest


问题1:

使用transitionFromViewController:...时,由toViewControllerviewWillAppear:完成的布局不会在过渡动画开始时显示。换句话说,在动画期间显示预先布局的视图,并且其内容在动画之后捕捉到后布局的位置。

问题2:

如果我自定义了我的导航栏的UIBarButtonItem的背景,则在动画之前,工具栏按钮以错误的大小/位置显示,并在动画结束时捕捉到正确的大小/位置,类似于问题1。


为了演示问题,我制作了一个基本的自定义容器控制器,用于执行一些自定义视图转换。它与UINavigationController非常相似,只是在视图之间执行交叉溶解而不是推送动画。

'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 中执行此操作,因为此时它不会具有正确的框架。

为了演示目的,DetailViewControllerviewDidLoadviewWillAppearviewDidAppear 中将其标签设置为不同的位置和颜色:

- (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 here enter image description here 预期的视图在动画前后。

然而,标签在y = 50处,好像在viewWillAppear中设置的布局没有在动画发生之前完成(左边的图片)。但请注意,标签的背景色设置为黄色(由viewWillAppear指定的颜色)!

enter image description here enter image description here 动画开始时的错误布局。请注意,栏按钮也以错误的位置/大小开始。


控制台日志
TransitionTest[49795:c07] -[DetailViewController viewDidLoad]
TransitionTest[49795:c07] Before transitionFromViewController:
TransitionTest[49795:c07] -[DetailViewController viewWillAppear:]
TransitionTest[49795:c07] -[DetailViewController viewWillLayoutSubviews]
TransitionTest[49795:c07] -[DetailViewController viewDidLayoutSubviews]
TransitionTest[49795:c07] -[DetailViewController viewDidAppear:]
请注意,viewWillAppear:transitionFromViewController:之后被调用


问题1的解决方案

好了,这里是部分解决方案。通过显式调用beginAppearanceTransition:endAppearanceTransitiontoViewController,视图将在过渡动画发生前具有正确的布局:

- (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] 在 transitionFromViewController: 之前
TransitionTest [18398:c07] - [DetailViewController viewWillLayoutSubviews]
TransitionTest [18398:c07] - [DetailViewController viewDidLayoutSubviews]
TransitionTest [18398:c07] - [DetailViewController viewDidAppear:]


但这并不能解决问题2!

出于某种原因,导航栏按钮在过渡动画开始时仍然具有错误的位置/大小。我花了很多时间尝试找到正确的解决方法,但没有运气。我开始觉得这是一个transitionFromViewController:UIAppearance等中的错误。请提供任何您可以提供的对此问题的见解,非常感谢。谢谢!


我尝试过的其他解决方案

  • transitionFromViewController:之前调用[self.view addSubview:toViewController.view];

它实际上会给用户带来完全正确的结果,解决问题1和2。问题是,viewWillAppearviewDidAppear都将被调用两次!如果我想在viewDidAppear中进行一些昂贵的动画或计算,这是有问题的。

  • transitionFromViewController:之前调用[toViewController viewWillAppear:YES];

我认为这基本上与调用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效果比交叉溶解要好。


使用 [self.view addSubview:toViewController.view] 实际上似乎是一个合理的方法(至少对于我的用例来说)。只需在这些方法中检查它们是否已经在最后一秒钟内触发即可。 - sobri
2个回答

7

好的,将layoutIfNeeded添加到toViewController.view中似乎可以解决问题 - 这样在显示屏幕上之前就可以正确布局视图(不需要添加/删除),并且不再出现奇怪的双重viewDidAppear:调用。

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

}

嗨,escrafford,感谢您的回答。它解决了问题的一部分,但仍不是完美的解决方案。工具栏按钮仍然表现奇怪(问题2),而且viewWillAppear:被调用了两次,在某些情况下可能会有问题。此外,我收到了“未平衡的开始/结束出现转换”警告。 - Joseph Lin
我的修改解决了问题吗? - escrafford
嗯...你可能走对了路。右侧栏按钮不再失真,但是左侧栏按钮和底部工具栏上的按钮仍然会失真。 - Joseph Lin
Joseph Lin.. 有任何线索吗?奇怪的是,我只在leftBarButtonItems出现了这个问题,而不是标题。 - OutOnAWeekend

2

我也遇到过相同的问题,解决方法是使用转发外观事务和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();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接