Animate change of view controllers without using navigation controller stack, subviews or modal controllers?
Asked Answered
P

6

142

NavigationControllers have ViewController stacks to manage, and limited animation transitions.

Adding a view controller as a sub-view to an existing view controller requires passing events to the sub-view controller, which is a pain to manage, loaded with little annoyances and in general feels like a bad hack when implementing (Apple also recommends against doing this).

Presenting a modal view controller again places a view controller on top of another, and while it doesn't have the event passing problems described above, it doesn't really 'swap' the view controller, it stacks it.

Storyboards are limited to iOS 5, and are almost ideal, but cannot be used in all projects.

Can someone present a SOLID CODE EXAMPLE on a way to change view controllers without the above limitations and allows for animated transitions between them?

A close example, but no animation: How to use multiple iOS custom view controllers without a navigation controller

Edit: Nav Controller use is fine, but there needs to be animated transition styles (not simply the slide effects) the view controller being shown needs to be swapped completely (not stacked). If the second view controller must remove another view controller from the stack, then it's not encapsulated enough.

Edit 2: iOS 4 should be the base OS for this question, I should have clarified that when mentioning storyboards (above).

Pinochle answered 16/11, 2011 at 3:18 Comment(4)
You can do custom animation transitions with a navigation controller. If this would be acceptable, please remove that constraint from your question and I'll post a code example.Amputate
@Richard if it skips the hassle of managing the stack and accommodates different animated transition styles between the view controllers then navigation controller use is fine!Pinochle
Ok good. I got impatient and posted the code. Give it a try. Works for me.Amputate
@RichardBrightwell you said here that one could do custom animation transitions between view controllers using a navigation controller... how? Can you post an example? thanks.Blennioid
T
107

EDIT: New answer that works in any orientation. The original answer only works when the interface is in portrait orientation. This is b/c view transition animations that replace a view w/ a different view must occur with views at least a level below the first view added to the window (e.g. window.rootViewController.view.anotherView).

I've implemented a simple container class I called TransitionController. You can find it at https://gist.github.com/1394947.

As an aside, I prefer the implementation in a separate class b/c it's easier to reuse. If you don't want that, you could simply implement the same logic directly in your app delegate eliminating the need for the TransitionController class. The logic you'd need would be the same however.

Use it as follows:

In your app delegate

// add a property for the TransitionController

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    MyViewController *vc = [[MyViewContoller alloc] init...];
    self.transitionController = [[TransitionController alloc] initWithViewController:vc];
    self.window.rootViewController = self.transitionController;
    [self.window makeKeyAndVisible];
    return YES;
}

To transition to a new view controller from any view controller

- (IBAction)flipToView
{
    anotherViewController *vc = [[AnotherViewController alloc] init...];
    MyAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    [appDelegate.transitionController transitionToViewController:vc withOptions:UIViewAnimationOptionTransitionFlipFromRight];
}

EDIT: Original Answer below - only works for portait orientation

I made the following assumptions for this example:

  1. You have a view controller assigned as the rootViewController of your window

  2. When you switch to a new view you want to replace the current viewController with the viewController owning the new view. At any time, only the current viewController is alive (e.g. alloc'ed).

The code can be easily modified to work differently, the key point is the animated transition and the single view controller. Make sure you don't retain a view controller anywhere outside of assigning it to window.rootViewController.

Code to animate transition in app delegate

- (void)transitionToViewController:(UIViewController *)viewController
                    withTransition:(UIViewAnimationOptions)transition
{
    [UIView transitionFromView:self.window.rootViewController.view
                        toView:viewController.view
                      duration:0.65f
                       options:transition
                    completion:^(BOOL finished){
                        self.window.rootViewController = viewController;
                    }];
}

Example use in a view controller

- (IBAction)flipToNextView
{
    AnotherViewController *anotherVC = [[AnotherVC alloc] init...];
    MyAppDelegate *appDelegate = (MyAppDelegate *)[UIApplication sharedApplication].delegate;
    [appDelegate transitionToViewController:anotherVC
                             withTransition:UIViewAnimationOptionTransitionFlipFromRight];
}
Teetotalism answered 23/11, 2011 at 19:54 Comment(21)
Yes, very nice! Not only does it do the trick, but it's a very simple and clean code example. Many thanks!Pinochle
Doesn't his cause problems in landscape for you? Also: does this trigger the willAppear and didAppear methods of the viewControllers?Gryphon
I know the appear calls are being done because I logged them. I also don't see why this would affect orientation changes. Can you elaborate why you think it would?Pinochle
It turns out that @FelixLam is correct. My solution as posted here only works in portrait orientation b/c of how windows and the root view assigned to them work. I believe this to be a limitaiton (or requirement) of iOS. I have come up with a solution that works for all orientations and am adding that to my answer. It does require using a container class I called TransitionController. It works similarly to a UINavigationController without the navigation stack.Teetotalism
@javy, thanks for the question, it was a fun challenge figuring this out. On the other hand it's been a big time sync. :) Hope you can use it.Teetotalism
Thank for all your effort. It looks as though there is no actual way to change view controllers without using one of the methods mentioned in the question (in this case, it's now adding a view controller as a subview to another view controller). On the other hand, the portrait version of the code still works, at least workable for iPhone apps, so unless someone comes up with an alternative, I'll keep yours as the correct answer.Pinochle
The requirement is that the animations occur with views a level below the first view added to the window (e.g. window.rootViewController.view.anotherView). That's the true requirement. The easiest way to do that is by using a special view controller as the container (e.g. TransitionController, UINavigationController, etc). You can obscure this special controller in your app delegate by adding methods there that perform the transitions. That way the rest of your view controllers can be implemented with no knowledge of it though they would still call your appDelegate to do the transition.Teetotalism
Don't take my comments the wrong way, it's a great answer, and does the job nicely. I'm surprised that after all this time Apple hasn't expressed a way to do this already.Pinochle
No worries, was just explaining what I learned while working on this. If Apple's docs were a bit better this would have been much easier/quicker.Teetotalism
I noticed the new TransitionController works for iOS 5 but not for less than that. It's because the view will appear and other methods aren't being called on the new view controller. It will load the nib and display it, but view will appear will not be called in the new view controller so any code in those methods are ineffective.Pinochle
Just wanted to add my 2c and thank you for providing such an elegant solution. Being new to iOS, I found it very difficult to understand how UIViewControllers of complex scenes should be loaded and unloaded properly. This post and subclass really helped me understand what should be done. Thanks again!Reseat
@Teetotalism great solution, works pretty well in my iPad app except for one issue I am facing, after first initialisation transVC=[TransitionController ... initWithViewController:aUINavCtrlObj]; window.rootVC=transVC; the view in the aUINavCtrlObj is padded 20px from top (i.e. bottom 20 px are out of screen(UIWindow) bounds in all orientations), but after I do [transVC transitionToViewController:anotherVC] the padding is gone. I tried wantsFullScreenLayout=NO in TransitionController's loadView, what it does is it adds a 20 px black area just under statusBar.Meander
@AbduliamRehmanius: I had the same problem. I fixed it by changing line 25 of TransitionController.m to UIView *view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];, but I've only used this on the very newest version of iOS, so test carefully.Dodie
@PhilCalvin indeed it solves the problem, tested on iOS 5 simulator, thanks m8 :)Meander
@Teetotalism This is a great approach and I want you to keep getting rep for it, so I won't poach it and just change this one line. That said, the padding seems off in newer iOS versions. Have you experienced the same problem, and if so, does my fix work?Dodie
@Phil I had problems with padding also, adding this line self.viewController.view.frame = view.bounds; at the very end of the loadView method in TransitionController fixed it for me.Cocci
you are badly handling inserting the child, see github.com/xelvenone/UIViewController--Container-Bubalo
The question asked for a solution for iOS 4.x and the methods you use in your subclass require iOS 5.x.Teetotalism
Does anyone know some way of performing such behavior, but not calling viewDidAppear twice?? Thanks!Butternut
@AwaisFayyaz Sorry, but I'm not actively doing iOS development these days. Stack Overflow is great but answers such as this one are good at a moment in time for specific iOS and SDK versions. I'm sure there are much better ways to do this today with iOS 11 / Swift. Not to mention there's probably some great OSS libs out there. I'll have to leave it to the SO community to update this. Perhaps time for a new top level question?Teetotalism
@XJones: makes senseLacking
P
67

You can use Apple's new viewController containment system. For more in-depth information check out the WWDC 2011 session video "Implementing UIViewController Containment".

New to iOS5, UIViewController Containment allows you to have a parent viewController and a number of child viewControllers that are contained within it. This is how the UISplitViewController works. Doing this you can stack view controllers in a parent, but for your particular application you are just using the parent to manage the transition from one visible viewController to another. This is the Apple approved way of doing things and animating from one child viewController is painless. Plus you get to use all the various different UIViewAnimationOption transitions!

Also, with UIViewContainment, you do not have to worry, unless you want to, about the messiness of managing the child viewControllers during orientation events. You can simply use the following to make sure your parentViewController forwards rotation events to the child viewControllers.

- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers{
    return YES;
}

You can do the following or similar in your parent's viewDidLoad method to setup the first childViewController:

[self addChildViewController:self.currentViewController];
[self.view addSubview:self.currentViewController.view];
[self.currentViewController didMoveToParentViewController:self];
[self.currentViewController.swapViewControllerButton setTitle:@"Swap" forState:UIControlStateNormal];

then when you need to change the child viewController, you call something along the lines of the following within the parent viewController:

-(void)swapViewControllers:(childViewController *)addChildViewController:aNewViewController{
     [self addChildViewController:aNewViewController];
     __weak __block ViewController *weakSelf=self;
     [self transitionFromViewController:self.currentViewController
                       toViewController:aNewViewController
                               duration:1.0
                                options:UIViewAnimationOptionTransitionCurlUp
                             animations:nil
                             completion:^(BOOL finished) {
                                   [aNewViewController didMoveToParentViewController:weakSelf];

                                   [weakSelf.currentViewController willMoveToParentViewController:nil];
                                   [weakSelf.currentViewController removeFromParentViewController];

                                   weakSelf.currentViewController=[aNewViewController autorelease];
                             }];
 }

I posted a full example project here: https://github.com/toolmanGitHub/stackedViewControllers. This other project shows how to use UIViewController Containment on some various input viewController types that do not take up the whole screen. Good luck

Palawan answered 20/11, 2011 at 0:34 Comment(4)
A great example. Thank you for taking the time to post the code. On the other hand, it's limited to iOS 5 only. As mentioned in the question: "[Storyboards] are limited to iOS 5, and are almost ideal, but cannot be used in all projects." Considering a large percentage (around 40%?) of customers still use iOS 4, the goal is to provide something that works in iOS 4 and greater.Pinochle
Should you not call [self.currentViewController willMoveToParentViewController:nil]; in before the transition?Gryphon
@FelixLam - per the docs on UIViewController containment, you call willMoveToParentViewController only if you override the addChildViewController method. In this example I am calling it but not overriding.Palawan
In the demo at WWDC I seem to remember that they called it before calling starting the transition, because the transition does not imply that the currentVC will move to nil. In the case of a UITabBarController the transition would not change any vc's parent. The remove from parent calls the didMoveToParentViewController:nil, but there is no will... call. IMHOGryphon
A
7

OK, I know the question says without using a navigation controller, but no reason not to. OP wasn't responding to comments in time for me to go to sleep. Don't vote me down. :)

Here's how to pop the current view controller and flip to a new view controller using a navigation controller:

UINavigationController *myNavigationController = self.navigationController;
[[self retain] autorelease];

[myNavigationController popViewControllerAnimated:NO];

PreferencesViewController *controller = [[PreferencesViewController alloc] initWithNibName:nil bundle:nil];

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 0.65];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:myNavigationController.view cache:YES];
[myNavigationController pushViewController:controller animated:NO];
[UIView commitAnimations];

[controller release];
Amputate answered 16/11, 2011 at 3:41 Comment(12)
Doesn't this stack view controllers?Pinochle
Yes, because we are using a navigation controller. However, it gets around the limitation on what kind of transitions you can perform, which I thought was the core of your question.Amputate
Close, but one of the big problems is there are multiple view controller to manage on the stack. I'm hunting for way to change view controllers completely. =)Pinochle
Ah. I might have something like that too... give me a minute. If not, I'll delete this answer.Amputate
Ok, I cobbled together two different bits of code. I think this will do what you want.Amputate
This works! However, I'm curious...when the view is popped, why doesn't it disappear instantly? Is it because it's still in the UI thread when the animation is started?Pinochle
It's because I retain it first and then autorelease. The retain keeps it around long enough to do what we need and the autorelease lets it go when we're done so there is no memory leak.Amputate
I'm using ARC, so I guess it's applying the same principle? Also, before I try this, I check the nav controller stack count with a separate button action: count = 1, after this is done, I wait a few seconds, and check the count again, and it shows 2. Any ideas why?Pinochle
I have to go to sleep now. Way past bedtime or I'd be glad to help further. Here's a few parting thoughts: ARC - Never tried it with ARC turned on. Stack Count - Maybe you can iterate through and see what VCs are on the stack. Hope this helped you out. ZzzzzzzzzAmputate
I will dig into it further, but the stack always has two view controllers. I added a third button, popped the stack, and the count became 1, so I'll look into it more.Pinochle
The initial view controller isn't being removed from the stack. It can be manually removed in the new view controller, but then we're back to the same problem of stack management. Close, but not quite what I'm looking for.Pinochle
@RubberDuck, in response to your comment above - I posted it in this answer.Amputate
D
3

Since I just happened across this exact problem, and tried variations on all the pre-existing answers to limited success, I'll post how I eventually solved it:

As described in this post on custom segues, it's actually really easy to make custom segues. They are also super easy to hook up in Interface Builder, they keep relationships in IB visible, and they don't require much support by the segue's source/destination view controllers.

The post linked above provides iOS 4 code to replace the current top view controller on the navigationController stack with a new one using a slide-in-from-top animation.

In my case, I wanted a similar replace segue to happen, but with a FlipFromLeft transition. I also only needed support for iOS 5+. Code:

From RAFlipReplaceSegue.h:

#import <UIKit/UIKit.h>

@interface RAFlipReplaceSegue : UIStoryboardSegue
@end

From RAFlipReplaceSegue.m:

#import "RAFlipReplaceSegue.h"

@implementation RAFlipReplaceSegue

-(void) perform
{
    UIViewController *destVC = self.destinationViewController;
    UIViewController *sourceVC = self.sourceViewController;
    [destVC viewWillAppear:YES];

    destVC.view.frame = sourceVC.view.frame;

    [UIView transitionFromView:sourceVC.view
                        toView:destVC.view
                      duration:0.7
                       options:UIViewAnimationOptionTransitionFlipFromLeft
                    completion:^(BOOL finished)
                    {
                        [destVC viewDidAppear:YES];

                        UINavigationController *nav = sourceVC.navigationController;
                        [nav popViewControllerAnimated:NO];
                        [nav pushViewController:destVC animated:NO];
                    }
     ];
}

@end

Now, control-drag to set up any other kind of segue, then make it a Custom segue, and type in the name of the custom segue class, et voilà!

Dix answered 13/10, 2012 at 4:52 Comment(3)
IS there a way to to this programatically without storyboard?Surculose
As far as I know, to use a segue you have to define it and give it an identifier in a storyboard. You can invoke a segue in code with UIViewController's –performSegueWithIdentifier:sender: method.Dix
You should never call viewDidXXX and viewWillXXX directly.Exception
S
2

I struggled with this one for a long time, and one of my issues is listed here, I'm not sure if you have had that problem. But here's what I would recommend if it must work with iOS 4.

Firstly, create a new NavigationController class. This is where we'll do all the dirty work--other classes will be able to "cleanly" call instance methods like pushViewController: and such. In your .h:

@interface NavigationController : UIViewController {
    NSMutableArray *childViewControllers;
    UIViewController *currentViewController;
}

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL))completion;
- (void)addChildViewController:(UIViewController *)childController;
- (void)removeChildViewController:(UIViewController *)childController;

The child view controllers array will serve as a store for all the view controllers in our stack. We would automatically forward all rotation and resizing code from the NavigationController's view to the currentController.

Now, in our implementation:

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL))completion
{
    currentViewController = [toViewController retain];
    // Put any auto- and manual-resizing handling code here

    [UIView animateWithDuration:duration animations:animations completion:completion];

    [fromViewController.view removeFromSuperview];
}

- (void)addChildViewController:(UIViewController *)childController {
    [childViewControllers addObject:childController];
}

- (void)removeChildViewController:(UIViewController *)childController {
    [childViewControllers removeObject:childController];
}

Now you can implement your own custom pushViewController:, popViewController and such, using these method calls.

Good luck, and I hope this helps!

Seductress answered 23/11, 2011 at 0:59 Comment(4)
Again we must resort to adding view controllers as sub-views of existing view controllers. Granted, this is what the existing Navigation Controller does, but it means we basically have to rewrite it and all it's methods. In reality, we should avoid having to distribute viewWillAppear and similar methods. I'm beginning to think there is no clean way of doing this. However, I do thank you for taking the time and effort!Pinochle
I think, with this system of adding and removing view controllers as needed, this solution prevents you from having to forward those methods.Seductress
Are you sure? I used to swap view controllers in a similar manner before and I always had to forward messages. Can you confirm otherwise?Pinochle
No, I'm not sure, but I would assume that, as long as you remove the view controllers' views as they disappear, and add them as they do appear, that should automatically trigger viewWillAppear, viewDidAppear, and such.Seductress
S
-1
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
UINavigationController *viewController = (UINavigationController *)[storyboard instantiateViewControllerWithIdentifier:@"storyBoardIdentifier"];
viewController.modalTransitionStyle = UIModalTransitionStylePartialCurl;
[self presentViewController:viewController animated:YES completion:nil];

Try This Code.


This code gives Transition from a view controller to another view controller which having a navigation controller.

Sartor answered 7/6, 2016 at 9:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.