How to use Auto Layout with container transitions? - ios

How can you use Auto Layout with the UIViewController container transition method:
-(void)transitionFromViewController:(UIViewController *)fromViewController
toViewController:(UIViewController *)toViewController
duration:(NSTimeInterval)duration
options:(UIViewAnimationOptions)options
animations:(void (^)(void))animations
completion:(void (^)(BOOL finished))completion;
Traditionally, using Springs/Struts, you set the initial frames (just before calling this method) and set up the final frames in the animation block you pass to the method.
That method does the work of adding the view to the view hierarchy and running the animations for you.
The problem is that you we can't add initial constraints in the same spot (before the method call) because the view has not yet been added to the view hierarchy.
Any ideas how I can use this method along with Auto Layout?
Below is an example (Thank you cocoanetics) of doing this using Springs/Struts (frames)
http://www.cocoanetics.com/2012/04/containing-viewcontrollers
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController
{
// XXX We can't add constraints here because the view is not yet in the view hierarchy
// animation setup
toViewController.view.frame = _containerView.bounds;
toViewController.view.autoresizingMask = _containerView.autoresizingMask;
// notify
[fromViewController willMoveToParentViewController:nil];
[self addChildViewController:toViewController];
// transition
[self transitionFromViewController:fromViewController
toViewController:toViewController
duration:1.0
options:UIViewAnimationOptionTransitionCurlDown
animations:^{
}
completion:^(BOOL finished) {
[toViewController didMoveToParentViewController:self];
[fromViewController removeFromParentViewController];
}];
}

Starting to think the utility method
transitionFromViewController:toViewController:duration:options:animations:completion can not be made to work cleanly with Auto Layout.
For now I've replaced my use of this method with calls to each of the "lower level" containment methods directly. It is a bit more code but seems to give greater control.
It looks like this:
- (void) performTransitionFromViewController:(UIViewController*)fromVc toViewController:(UIViewController*)toVc {
[fromVc willMoveToParentViewController:nil];
[self addChildViewController:toVc];
UIView *toView = toVc.view;
UIView *fromView = fromVc.view;
[self.containerView addSubview:toView];
// TODO: set initial layout constraints here
[self.containerView layoutIfNeeded];
[UIView animateWithDuration:.25
delay:0
options:0
animations:^{
// TODO: set final layout constraints here
[self.containerView layoutIfNeeded];
} completion:^(BOOL finished) {
[toVc didMoveToParentViewController:self];
[fromView removeFromSuperview];
[fromVc removeFromParentViewController];
}];
}

The real solution seems to be to set up your constraints in the animation block of transitionFromViewController:toViewController:duration:options:animations:.
[self transitionFromViewController:fromViewController
toViewController:toViewController
duration:1.0
options:UIViewAnimationOptionTransitionCurlDown
animations:^{
// SET UP CONSTRAINTS HERE
}
completion:^(BOOL finished) {
[toViewController didMoveToParentViewController:self];
[fromViewController removeFromParentViewController];
}];

There are two solutions depending on whether you simply need to position the view via auto layout (easy) vs. needing to animate auto layout constraint changes (harder).
TL;DR version
If you only need to position a view via auto layout, you can use the -[UIViewController transitionFromViewController:toViewController:duration:options:animations:completion:] method and install the constraints in the animation block.
If you need to animate auto layout constraint changes, you must use a generic +[UIView animateWithDuration:delay:options:animations:completion:] call and add the child controller regularly.
Solution 1: Position a view via Auto Layout
Let's tackle the first, easy case first. In this scenario, the view should be positioned via auto layout so that changes to the status bar height (e.g. via choosing Toggle In-Call Status Bar), among other things, will not push things off the screen.
For reference, here is Apple's official code regarding the transition from one view controller to another:
- (void) cycleFromViewController: (UIViewController*) oldC
toViewController: (UIViewController*) newC
{
[oldC willMoveToParentViewController:nil]; // 1
[self addChildViewController:newC];
newC.view.frame = [self newViewStartFrame]; // 2
CGRect endFrame = [self oldViewEndFrame];
[self transitionFromViewController: oldC toViewController: newC // 3
duration: 0.25 options:0
animations:^{
newC.view.frame = oldC.view.frame; // 4
oldC.view.frame = endFrame;
}
completion:^(BOOL finished) {
[oldC removeFromParentViewController]; // 5
[newC didMoveToParentViewController:self];
}];
}
Rather than using frames as in the example above, we must add constraints. The question is where to add them. We cannot add them at marker (2) above, since newC.view is not installed in the view hierarchy. It is only installed the moment we call transitionFromViewController... (3). That means we can either install the constraints right after the call to transitionFromViewController, or we can do it as the first line in the animation block. Both should work. If you want to do it at the earliest time, then putting it in the animation block is the way to go. More on the order of how these blocks are called will be discussed below.
In summary, for just positioning via auto layout, use a template such as:
- (void)cycleFromViewController:(UIViewController *)oldViewController
toViewController:(UIViewController *)newViewController
{
[oldViewController willMoveToParentViewController:nil];
[self addChildViewController:newViewController];
newViewController.view.alpha = 0;
[self transitionFromViewController:oldViewController
toViewController:newViewController
duration:0.25
options:0
animations:^{
newViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
// create constraints for newViewController.view here
newViewController.view.alpha = 1;
}
completion:^(BOOL finished) {
[oldViewController removeFromParentViewController];
[newViewController didMoveToParentViewController:self];
}];
// or create constraints right here
}
Solution 2: Animating constraint changes
Animating constraint changes is not as simple, because we are not given a callback between when the view is attached to the hierarchy and when the animation block is called via the transitionFromViewController... method.
For reference, here is the standard way of adding/removing a child view controller:
- (void) displayContentController: (UIViewController*) content;
{
[self addChildViewController:content]; // 1
content.view.frame = [self frameForContentController]; // 2
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self]; // 3
}
- (void) hideContentController: (UIViewController*) content
{
[content willMoveToParentViewController:nil]; // 1
[content.view removeFromSuperview]; // 2
[content removeFromParentViewController]; // 3
}
By comparing these two methods and the original cycleFromViewController: posted above, we see that transitionFromViewController takes care of two things for us:
[self.view addSubview:self.currentClientView];
[content.view removeFromSuperview];
By adding some logging (omitted from this post), we can get a good idea of when these methods are called.
After doing so, it appears that the method is implemented in a manner similar to the following:
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion
{
[self.view addSubview:toViewController.view]; // A
animations(); // B
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[fromViewController.view removeFromSuperview];
completion(YES);
});
}
Now it is clear to see why it's not possible to use transitionFromViewController to animate constraint changes. The first time you can initialize constraints is after the view is added (line A). The constraints should be animated in the animations() block (line B), but there is no way to run code between these two lines.
Therefore, we must use a manual animation block, along with the standard method of animating constraint changes:
- (void)cycleFromViewController:(UIViewController *)oldViewController
toViewController:(UIViewController *)newViewController
{
[oldViewController willMoveToParentViewController:nil];
[self addChildViewController:newViewController];
[self.view addSubview:newViewController.view];
newViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
// TODO: create initial constraints for newViewController.view here
[newViewController.view layoutIfNeeded];
// TODO: update constraint constants here
[UIView animateWithDuration:0.25
animations:^{
[newViewController.view layoutIfNeeded];
}
completion:^(BOOL finished) {
[oldViewController.view removeFromSuperview];
[oldViewController removeFromParentViewController];
[newViewController didMoveToParentViewController:self];
}];
}
Warnings
This is not equivalent to how the storyboard embeds a container view controller. For example, if you compare the translatesAutoresizingMaskIntoConstraints value of the embedded view via a storyboard vs. the method above, it will report YES for the storyboard, and NO (obviously, since we explicitly set it to NO) for the method I recommend above.
This can lead to inconsistencies in your app, since certain parts of the system seem to depend on UIViewController containment to be used with translatesAutoresizingMaskIntoConstraints set to NO. For example, on an iPad Air (8.4), you may get strange behavior when rotating from portrait to landscape.
The simple solution seems to be to keep translatesAutoresizingMaskIntoConstraints set to NO, then set newViewController.view.frame = newViewController.view.superview.bounds. However, unless you are very careful with when this method is called, it most likely will give you an incorrect visual layout. (Note: The way that the storyboard ensures the view sizes properly is by setting the embedded view's autoresize property to W+H. Printing out the frame right after adding the subview will also reveal a difference between the storyboard vs. programatic approach, which suggests that Apple is setting the frame directly on the contained view.)

I hope your question gains some traction because I think it's a good one. I don't have a definitive answer for you, but I can describe my own experiences with situations similar to yours.
Here's the conclusion I have drawn from my experiences: you can't use auto layout directly on the root view of a view controller. As soon as I set translatesAutoresizingMaskIntoConstraints to NO on a root view, I start getting bugs–or worse.
So I use a hybrid solution instead. I set frames and use autoresizing to position and size the root view in a layout that is otherwise configured by auto layout. For example, here's how I load a page view controller as child view controller in viewDidLoad in an app that uses auto layout:
self.pageViewController = ...
...
[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];
// could not get constraints to work here (using autoresizing mask)
self.pageViewController.view.frame = self.view.bounds;
[self.pageViewController didMoveToParentViewController:self];
This is the way Apple loads a child view controller in the Xcode "Page-Based Application" template–and this is performed in an auto layout enabled project.
So if I were you, I would try setting frames to animate the view controller transition and see what happens. Let me know how it works.

Related

Is it possible to add a table view controller to a part of a view controller?

Let's say that I have a UITableViewController which is mostly reusable, and should be used from many UIViewControllers, but it should cover only part of the total view (e.g. 90% of the total height). Normally I would do this with navigation, but if I want to keep the top 10% of the UIViewController visible, and show the UITableViewController for the remaining 90%, it is possible and if yes how to do it?
Yes. The big view controller is container view controller, and the small view controller (table view controller in this case) is child view controller. We can add or remove child view controller in the container view controller.
Add a child view controller to a container
- (void)displayContentController:(UIViewController *)content {
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];
}
Remove a child view controller from a container
- (void)hideContentController:(UIViewController *)content {
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
}
We can also remove an old child view controller and add a new child view controller at the same time. Here is the example code (with animation).
- (void)cycleFromViewController:(UIViewController *)oldVC
toViewController:(UIViewController *)newVC {
// Prepare the two view controllers for the change.
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
// Get the start frame of the new view controller and the end frame
// for the old view controller. Both rectangles are offscreen.
newVC.view.frame = [self newViewStartFrame];
CGRect endFrame = [self oldViewEndFrame];
// Queue up the transition animation.
[self transitionFromViewController:oldVC toViewController:newVC
duration:0.25 options:0
animations:^{
// Animate the views to their final positions.
newVC.view.frame = oldVC.view.frame;
oldVC.view.frame = endFrame;
}
completion:^(BOOL finished) {
// Remove the old view controller and send the final
// notification to the new view controller.
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
}];
}
Yes, you can. Just add UITableViewController as child controller to your parent UIViewController.
Also, you can read about it here Apple Documentation

For custom segue animation, how to tell the ViewController that it was animated?

Here is my custom segue animation's code:
[UIView animateWithDuration:.4 animations:^{
destinationViewController.view.frame = targetFrame;
buttonBarImageView.alpha = 0.0;
} completion:^(BOOL finished) {
[sourceViewController presentViewController:destinationViewController animated:NO completion:nil];
[buttonBarImageView removeFromSuperview];
}];
Note in the above code that I pass NO for animated in presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion, because I'm using my custom animation and don't want to use any standard animation.
My destination view controller's method viewWillAppear:(BOOL)animated now should behave differently depending on the animation flag. In the VC, the flag decides if an additional own inner animation should be played or not. Various destination VCs exist and they have different inner animations. They are additional to the custom animation I'm talking about above.
However, I can not pass YES to the VC because I have to pass NO to presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion, which causes NO to be passed to the VC.
How can I prevent presentViewController:... from running an animation and still pass YES to the viewWillAppear:(BOOL)animated method of the VC?
One possibility to check is passing NO to super when calling [super viewWillAppear:]
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:NO];
//-- your own animation here
}
so you can pass YES to presentViewController:, but you override it when passing it to super.
If this does not work, I bet the only chance you have is passing NO to presentViewController: and add your own flag to the view controller so you can control whether it is custom animated or not.

UIView removeFromSuperview inside animation PROPERLY

Now, this question have partially been asking alot, but none actually considering how (or when) the messages -viewWillDisappear & -viewDidDisappear are being sent. Almost every example use the following design:
[UIView animateWithDuration:0.5
delay:1.0
options: UIViewAnimationCurveEaseOut
animations:^{
yourView.alpha = 0;
}completion:^(BOOL finished){
[yourView removeFromSuperview]; // Called on complete
}];
The problem with this is that these messages will both be sent when de animation ends!
Now, -addSubview can be animated (if put inside the animations-block) which will send the corresponding messages (-viewWillAppear & -viewDidAppear) with correct timedifference. So naturally one would place -removeFromSuperview inside the animations-block. This WILL send the messages correctly, but the view is actually removed instantly making the animation... Well, it won't animate because nothing is left to animate!
Is this intentional from apple and if so, why? How do you do it correctly?
Thanks!
Edit.
Just to clearify what I'm doing:
I got a custom segue, vertically animating a Child-ViewController down from top which works as expected with the following code:
-(void)perform{
UIViewController *srcVC = (UIViewController *) self.sourceViewController;
UIViewController *destVC = (UIViewController *) self.destinationViewController;
destVC.view.transform = CGAffineTransformMakeTranslation(0.0f, -destVC.view.frame.size.height);
[srcVC addChildViewController:destVC];
[UIView animateWithDuration:0.5f
animations:^{
destVC.view.transform = CGAffineTransformMakeTranslation(0.0f, 0.0f);
[srcVC.view addSubview:destVC.view];
}
completion:^(BOOL finished){
[destVC didMoveToParentViewController:srcVC];
}];
}
Here it will happen in the following order (thanks to -addSubview being inside the animations-block):
Add childView (will automatically invoke -willMoveToParentViewController)
-addSubview will invoke -viewWillAppear
When the animation finishes, -addSubview will invoke -viewDidAppear
Manually invoke -didMoveToParentViewController inside the completion-block
Above is the exact expected behavior (just like the built-in transitions behave).
With the following code to do the above segue but backwards (with an unwindSegue):
-(void)perform{
UIViewController *srcVC = (UIViewController *) self.sourceViewController;
srcVC.view.transform = CGAffineTransformMakeTranslation(0.0f, 0.0f);
[srcVC willMoveToParentViewController:nil];
[UIView animateWithDuration:5.5f
animations:^{
srcVC.view.transform = CGAffineTransformMakeTranslation(0.0f, -srcVC.view.frame.size.height);
}
completion:^(BOOL finished){
[srcVC.view removeFromSuperview]; // This can be done inside the animations-block, but will actually remove the view at the same time ´-viewWillDisappear´ is invoked, making no sense!
[srcVC removeFromParentViewController];
}];
}
the flow will be like this:
Manually invoke -willMoveToParentView:nil to notify that it will be removed
When the animation finishes, both -viewWillDisappear & -viewDidDisappear will be invoked simultaneously (wrong!) and -removeFromParentViewController will automatically invoke -didMoveToParentViewController:nil.
And if I now move -removeFromSuperview in to the animations-block, the events will be sent correctly but the view is removed when the animation starts instead of when the animation finishes (this is the part that makes no sense, following how -addSubview behaves).
Your question is about removing view controller, because, viewWillDisappear and viewDidDisappear are method of view controller.
viewWillDisappear: will be called from completion block, not earlier, because this is the place where you said that you want to remove subview from main view.
If you want to remove some property before that point, then in child controller override willMoveToParentViewController: method. This method will be called before animation block.
Here's code example:
//Prepare view for removeing.
[self.childViewController willMoveToParentViewController:nil];
[UIView animateWithDuration:0.5
delay:1.0
options: UIViewAnimationOptionCurveEaseOut
animations:^{
self.childViewController.view.alpha = 0;
}completion:^(BOOL finished){
[self.childViewController.view removeFromSuperview];
[self.childViewController didMoveToParentViewController:self];
}];
So, the flow will be:
First willMoveToParentViewController: with nil parameter will be called
Animation block will start and view will set it's alpha property to 0
When animation finish, completion block will start to execute...
[self.childViewController.view removeFromSuperview]; will be called first
Then viewWillDissapear: in childViewController will be called
Then [self.childViewController didMoveToParentViewController:self];
And at the end viewDidDissapear: in childViewController will execute.
Pre request for this flow is that you embed childViewController with code like this:
[self addChildViewController:self.childViewController];
[self.view addSubview:self.childViewController.view];
[self.childViewController didMoveToParentViewController:self];

iOS container view controller adds child's view in portrait orientaton

I am developing an iPad app that should only support landscape orientation. I use these two methods to add a new child view controller to my container view controller:
- (void) assignFirstChildViewController:(UIViewController*)controller
{
self.currentChildViewController = controller;
[self addChildViewController:self.currentChildViewController];
[self.currentChildViewController didMoveToParentViewController:self];
[self.containerView addSubview:self.currentChildViewController.view];
}
- (void)assignNewChildController:(UIViewController *)childViewController
{
id currentChildViewController = self.currentChildViewController;
if(!currentChildViewController){
[self assignFirstChildViewController:childViewController];
}else{
[self.currentChildViewController willMoveToParentViewController:nil];
[self addChildViewController:childViewController];
__weak __block PTSBaseContainerViewController *weakSelf=self;
[self transitionFromViewController:self.currentChildViewController
toViewController:childViewController
duration:1.0
options:0
animations:^{
[UIView transitionFromView:self.currentChildViewController.view toView:childViewController.view duration:1.0 options:UIViewAnimationOptionTransitionCrossDissolve completion:NULL];
}
completion:^(BOOL finished) {
[weakSelf.currentChildViewController removeFromParentViewController];
weakSelf.currentChildViewController = childViewController;
[weakSelf.currentChildViewController didMoveToParentViewController:weakSelf];
}];
}
}
The problem is that the view of the child view controller is added in portrait orientation and it messes up the views as shown in the following image:
The green view is the view of the child view controller which as you can see is added in portrait mode. Instead of occupying the whole yellow view (which is the container view and it occupies the whole frame of the view controller beneath the grey top bar) it is being added in portrait mode and I cannot figure it why.
PS: I tried overriding shouldAutomaticallyForwardRotationMethods and shouldAutomaticallyForwardAppearanceMethods as written in the apple documentation but with no results.
As you'll see in Apple's documentation, you need to manually set the frame for the child view controller.

Removing view from superview iOS

I am struggling with understanding why the first method below works for hiding and removing a subview of a view. In this first method I pass the pointer by reference. In the second method, which is less general, I have a delegate method designed for removing a specific view. I would like to use the first method, because I have several views that I would like to apply this too. I should mention that the first method works without fail as long as it is called within the implementing class. It fails when I call it from the view controller that I wish to dismiss. I get an EXC_BAD_ACCESS on the removeFromSuperview line when it fails in the first method.
-(void)closeView:(UIViewController **)viewController
{
[UIView transitionWithView:self.view
duration:UINavigationControllerHideShowBarDuration
options:UIViewAnimationOptionCurveLinear
animations:^
{
[[*viewController view] setAlpha:0.0];
}
completion:^(BOOL finished)
{
[[*viewController view] removeFromSuperview];
[*viewController release], *viewController = nil;
}];
}
-(void)closeButtonClicked
{
[delegate closeView:&self];
}
//
// This method works without fail:
//
-(void)closeView
{
[UIView transitionWithView:self.view
duration:UINavigationControllerHideShowBarDuration
options:UIViewAnimationOptionCurveLinear
animations:^
{
// In this context viewController is defined in the class interface
[[viewController view] setAlpha:0.0];
}
completion:^(BOOL finished)
{
[[viewController view] removeFromSuperview];
[viewController release], viewController = nil;
}];
}
-(void)closeButtonClicked
{
[delegate closeView];
}
First of all, it is not according to the style guides, and not a good idea in general, to do a release of the viewController within a method like this. It will get you into trouble quickly. If the caller of this method is responsible for the viewController (it has done the retain), then it should release it as well. This is likely the cause of the first method not working from within the viewcontroller itself.
In the second method you do not pass in the viewController as parameter, which means it needs to be defined in the context.
If you don't release the viewController in this method, then you don't need to set its variable to nil either, and you can simply pass it as normal parameter:
-(void)closeView:(UIViewController *)viewController
{
[UIView transitionWithView:self.view
duration:UINavigationControllerHideShowBarDuration
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^
{
[[viewController view] removeFromSuperview];
}
completion:nil];
}
you would then do this at the call-site:
[self closeView:childViewController];
[childViewController release]; childViewController = nil;
It safe to release the child in this way before the animation is done, because the animations block implicitly retains all objects referenced from the block, including the viewController parameter. Therefore, the child's dealloc is not called until the animations block releases it.
This does not work in your first code example, because you pass a pointer to a variable. That is, the animations block does not know it needs to retain the child.
BTW, I am not sure why you want to set the alpha, in the example above I show that you can also remove the view already in the animations block. See more about that in the UIView Class Reference.
**viewcontroller and &self is not the way to go. In Objective-C, you do [self.view removeFromSuperview] in the subview itself, in the parent viewcontroller you do release or with ARC just replace the subview with another view.

Resources