This is both a question and a partial solution.
*Sample project here:
https://github.com/JosephLin/TransitionTest
When using transitionFromViewController:...
, layouts done by the toViewController
's viewWillAppear:
doesn't show up when the transition animation begins. In other words, the pre-layout view shows during the animation, and it's contents snap to the post-layout positions after the animation.
If I customize the background of my navbar's UIBarButtonItem
, the bar button shows up with the wrong size/position before the animation, and snaps to the correct size/position when the animation ends, similar to Problem 1.
To demonstrate the problem, I made a bare-bone custom container controller that does some custom view transitions. It's pretty much a UINavigationController copy that does cross-dissolve instead of push animation between views.
The 'Push' method looks like this:
- (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];
}];
}
Now, DetailViewController
(the view controller I'm pushing to) needs to layout its content in viewWillAppear:
. It can't do it in viewDidLoad
because it wouldn't have the correct frame at that time.
For demonstration purpose, DetailViewController
sets its label to different locations and colors in viewDidLoad
, viewWillAppear
, and 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];
}
Now, when pushing the DetailViewController
, I'm expecting to see the label at y =200
at the begining of the animation (left image), and then jumps to y = 350
after the animation is finished (right image).
Expected view before and after animation.
However, the label was at y=50
, as if the layout made in viewWillAppear
didn't make it before the animation took place (left image). But notice that the label's background was set to yellow (the color specified by viewWillAppear
)!
Wrong layout at the beginning of the animation. Notice that the bar buttons also start with the wrong position/size.
Console Log
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:]
Notice that viewWillAppear:
was called AFTER transitionFromViewController:
Alright, here comes the partial solution part. By explicitly calling beginAppearanceTransition:
and endAppearanceTransition
to toViewController
, the view will have the correct layout before the transition animation takes place:
- (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];
}];
}
Notice that viewWillAppear:
is now called BEFORE transitionFromViewController:
TransitionTest[18398:c07] -[DetailViewController viewDidLoad]
TransitionTest[18398:c07] -[DetailViewController viewWillAppear:]
TransitionTest[18398:c07] Before transitionFromViewController:
TransitionTest[18398:c07] -[DetailViewController viewWillLayoutSubviews]
TransitionTest[18398:c07] -[DetailViewController viewDidLayoutSubviews]
TransitionTest[18398:c07] -[DetailViewController viewDidAppear:]
For whatever reason, the navbar buttons still begin with the wrong position/size at the beginning of the transition animation. I spent so many time trying to find THE right solution but without luck. I'm starting to feel it's a bug in transitionFromViewController:
or UIAppearance
or whatever. Please, any insight you can offer to this question is greatly appreciated. Thanks!
Other solutions I've tried
[self.view addSubview:toViewController.view];
before transitionFromViewController:
It actually gives exactly the right result to the user, fixes both Problem 1&2. The problem is, viewWillAppear
and viewDidAppear
will both be called twice! It's problematic if I want to do some expansive animation or calculation in viewDidAppear
.
[toViewController viewWillAppear:YES];
before transitionFromViewController:
I think it's pretty much the same as calling beginAppearanceTransition:
. It fixes Problem 1 but not Problem 2. Plus, the doc says not to call viewWillAppear
directly!
[UIView animateWithDuration:]
instead of transitionFromViewController:
Like this: [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];
}];
It fixes Problem 2, but the view started with the layout in viewDidAppear
(label is green, y=350). Also, the cross-dissolve is not as good as using UIViewAnimationOptionTransitionCrossDissolve
Ok, adding layoutIfNeeded to the toViewController.view seems to do the trick - this gets the view laid out properly before it shows up on screen (without the add/remove), and no more weird double 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) {
}];
}