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).
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:
You have a view controller assigned as the rootViewController of your window 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];
}
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
[self.currentViewController willMoveToParentViewController:nil];
in before the transition?
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];
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à!
–performSegueWithIdentifier:sender:
method.
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!
viewWillAppear
, viewDidAppear
, and such.
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.
Success story sharing
TransitionController.m
toUIView *view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
, but I've only used this on the very newest version of iOS, so test carefully.