Managing and dismissing Multiple View Controllers in iOS - ios

I am a novice programmer for the iPhone. In developing my first game/app, I have come up with a problem (created a problem) for myself. I have researched this and think I have seen answers but I don't understand how to make them work for my application.
I have a game that has a few view controllers: Welcome, Play, High Scores, Preferences and About. I don't have a navigation controller or a root controller of which I am aware. EDIT - I now have a rootview controller - I added [self.window setRootViewController:viewController]; to my appDelegate.m file
From the Welcome screen/viewcontroller I can go to the Play view controller, the about view controller, the high scores view controller or the prefs view controller. I would like to make whichever new controller I go to now be in charge, so that I can dismiss the Welcome view controller. In fact, any time I go to a view controller, I would like to be able to dismiss from memory any of the other view controllers - especially the one I just came from. I can go to most of the viewcontrollers from most of the viewcontrollers.
I go to the new view controllers using this:
- (IBAction)playGameAction:(id)sender {
FlipTestViewController *myViewController = [[FlipTestViewController alloc]
initWithNibName:#"FlipTestViewController" bundle:nil];
[UIView beginAnimations:#"flipview" context:nil];
[UIView setAnimationDuration:1];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft
forView:self.view cache:YES];
[self.view addSubview:myViewController.view];
[UIView commitAnimations];
}
What do I do to let the WeclomeViewController go, once I am safely in the FlipTestViewController (the play controller)? And how do I tell the new controller that it is now in charge and the others can go away? Am I even asking a question that makes sense? Please use small words and concrete examples when responding! Thank you!
On rdelmar's advice, I tried adding a call to change the RootvVewController into my button code - the code I use to bring up the new viewController:
- (IBAction)playGameAction:(id)sender {
FlipTestViewController *myViewController = [[FlipTestViewController alloc]
initWithNibName:#"FlipTestViewController"
bundle:nil];
[UIApplication sharedApplication].delegate.window.rootViewController = myViewController;
[UIView beginAnimations:#"flipview" context:nil];
[UIView setAnimationDuration:1];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft
forView:self.view cache:YES];
[self.view addSubview:myViewController.view];
[UIView commitAnimations];
}
Sure enough, then I look in the comments, the FlipTestViewController (my Play Controller) is the new RootViewController. Unfortunately, I get a blank white screen, no transition of any kind and log message telling me that my ViewDidDisappear in my FlipTestViewController is being called somehow. Probably because the Welcome Screen is still there somehow. Memory management problem perhaps? Ought I to take this to a new question?
I just changed the background color of the main.xib file and apparently that is the screen that is showing up (the blank white screen).

To present a view controller modally, you might want to try this method:
- (IBAction)playGameAction:(id)sender {
FlipTestViewController *myViewController = [[FlipTestViewController alloc] initWithNibName:#"FlipTestViewController" bundle:nil];
myViewController.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:myViewController animated:YES completion:nil];
}
This puts FlipTestViewController on top of the current view with a flip transition style.
Then to dismiss the view controller, you'd hook this up to a control (usually a button) in FlipTestViewController:
- (IBAction)dismissViewController:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
This dismisses the view controller with the same flip animation

It sounds like the easiest way to achieve what you want is to just reset the rootViewController property of the app's window. From anywhere in the app you can get a reference to the root view controller with [UIApplication sharedApplication].delegate.window.rootViewController. So, in whatever action method you're using to switch to the next view controller, you could alloc init that controller and then set the window's rootViewController property to that controller. If you keep no other reference to a controller, the old one should be deallocated when you reset the property (which may or may not be what you want -- you might want to have some properties that would keep track of the high score or where the player was in a game, for instance).
In the app delegate there is usually code something like this (when you start with a single view project):
WelcomeController *welcome = [[WelcomeController alloc] initWithNibName:#"ViewController" bundle:nil];
self.window.rootViewController = welcome;
So I would put something like that in your app delegate with whatever view controller you want to show at start up. Then in your view controller code (or wherever your putting your buttons to switch view controllers) you would have something similar to (I used a segmented control in my test project):
-(IBAction)selectController:(UISegmentedControl *)sender {
if (sender.selectedSegmentIndex == 0) {
PlayViewController *player = [[PlayViewController alloc] initWithNibName:#"PlayViewController" bundle:nil];
[UIApplication sharedApplication].delegate.window.rootViewController = player;
}else{
HighScores *scorer = [[HighScores alloc] initWithNibName:#"HighScores" bundle:nil];
[UIApplication sharedApplication].delegate.window.rootViewController = scorer;
}
}

An easy approach would be to use a Navigation Controller. You can get the benefits of pushing/popping, which helps for sequences of screens.
You can then pop to a specific controller with:
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
This allows you to go to a specific view controller that has already been presented, without going through all the others.
Also, a bit less well known, you can just replace the entire stack of controllers with
- (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated
which also replaces the base controller with the controller at index zero.
So, you can easily get the top toolbar and all the nav controller functionality, and when you just want to reset everything to some single controller, you just do...
[navController setViewControllers:#[viewController] animated:YES];
EDIT
It sounds like you're not really sure what you want. Thus, you should give yourself some leeway, and not try to reinvent the wheel.
I think a navigation controller is the way to go. It already knows how to manage multiple controllers, and with setViewControllers you can make it looks like you are just going from any controller to any other, but you get to keep the same nav controller as your main center of work.
Otherwise, you can still do all this yourself with
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
and
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
You can call them from your root view controller. When you want to change controllers, it will then just dismiss the current controller and present the new one.

Related

Remove viewcontroller & move to next view controller

Hi i have been working on an iOS app.What i am doing is navigating among diffrent view controllers. But the problem is i want finish the current view controller from emoery and then move to the next view controller.
I am using `[self.view removeFromSuperview]; for finishing the cureent view & using
self.loginView = [self.storyboard instantiateViewControllerWithIdentifier:#"LOGIN"];
[self presentViewController:self.loginView animated:NO completion:nil];
for moving to next view controller but the thing is i am not able to remove it from memory.
Please tell me how can i do it?
Thanks in advance.
`
It's better to create a container view controller which manages your view controllers. For example, in viewDidLoad: of container controller you add current controller:
[self addChildViewController:self.currentViewController];
[self.currentViewController didMoveToParentViewController:self];
[self.view addSubView:self.currentViewController.view];
//here set up currentViewController view's frame or constraints if needed
When you need to open login controller, do this:
[self addChildViewController:loginViewController];
[self.loginViewController didMoveToParentViewController:self];
[self.view addSubView:loginViewController.view];
//here set up loginViewController view's frame or constraints if needed
//then remove current view controller
[self.currentViewController willMoveToParentViewController:nil];
[self.currentViewController removeFromParentViewController];
[self.currentViewController.view removeFromSuperview];
Remove from superview will remove it from the current view, but OS won't remove it until he needs to (this is topic for more explanation, let's say it won't remove it asap).
If you want something deleted just call it nil:
self.view = nil;
This will make the pointer to nil, so view won't be there any more. (the view really will be somewhere but you won't have access to it)
I am revising your code
self.loginView = [self.storyboard instantiateViewControllerWithIdentifier:#"LOGIN"];
[self presentViewController:self.loginView animated:NO completion:nil];
What you are doing here is presenting your login viewcontroller.
self: This is the instance of the viewcontroller you are currently working on. So how could you remove self from memory. (Not Possible)
You can approach alternate ways.
For example: 1. Changing root view controller
Pop to root view controller and then Push Login View controller.
If you try to remove not only UIView but the whole UIViewController from a navigation controller stack use this snippet
NSMutableArray *stack = [[NSMutableArray alloc] initWithArray: self.navigationController.viewControllers];
[stack removeObject:yourController];
self.navigationController.viewControllers = stack;
Be aware of using this only when you've already pass to the next controller view.
UPD:
Oh, now I see what you are trying to do. And I can't figure out, why you're trying to step your controllers this way (modally). I think you should use UINavigationController with navigation segues defined directly from your storyboard. Look at this article where apple purely explains what navigation is. That article is about modal views

Moving from a scene to a view controller - Attempt to present ViewController whose view is not in the window hierarchy

I am working on an iPhone app using Storyboard and I need to handle view changes from one view controller to another one. I have:
-INTROViewController.m
-INTROscene.m (this is a SKScene laid out by the above controller)
-UpgradeViewController.m
There is a sprite button in INTROscene.m and when I press it, it triggers a notification which is seen by its view controller (INTROViewController.m) and this triggers the switch to another view controller (UpgradeViewController.m). If I use Option 1, (which even adds a delay in order to make sure that the first view has appeared), it triggers the error below:
“Attempt to present ViewController whose view is not in the window hierarchy!”
I’ve found a way of switching view controllers without triggering this error (Option 2) but the visual effect is horrible, with a little lag showing an empty screen between the two views. Moreover I cannot use any of the nice transitions which are available using modalTransitionStyle.
What is the correct way of switching between views in this situation?
In my AppDelegate I don’t have a root view controller (and I don’t know how this should be set up). Is that the reason why I get the error? If so, how could I implement it? Cheers!
//Option 1 (triggers the error above)
-(void)TransitionTo_Upgrades_ViewController:(NSNotification *)notification
{
//Take from INTROViewController to UpgradeViewController
UpgradeViewController *controllerUPGRADES = [self.storyboard instantiateViewControllerWithIdentifier:#"Upgrades_storyboard"];
controllerUPGRADES.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self performSelector:#selector(NowGotoUPRADES) withObject:nil afterDelay:2.0];
}
- (void)NowGotoUPRADES
{
[self presentViewController:controllerUPGRADES animated:YES completion:nil];
}
//Option 2 (no error but horrible effect)
-(void)TransitionTo_OPTIONS_ViewController:(NSNotification *)notification
{
[self performSelector:#selector(NowGotoOPTIONS) withObject:nil afterDelay:2.0];
}
- (void)NowGotoOPTIONS
{
//Take from INTROViewController to UpgradeViewController
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:#"Main_iPhone" bundle:nil];
UIViewController *DesiredViewController = [storyBoard instantiateViewControllerWithIdentifier:#"Options_storyboard"];
[[[[UIApplication sharedApplication] delegate] window] setRootViewController:DesiredViewController];
}
I have experienced this same problem and have found that there is no solution as once you move from scene to scene in a view controller, you are unable to move to other view controllers as you have moved out of the proper hierarchy. My solution to this was to have my other view controllers as skscenes, however you may want to have most of your skscenes as view controllers whilst also not moving between scenes whilst on the same view controller.
Hope this helps. Reply if you need any clarification.

How to dismiss view controllers at any time (even during transitions) or when it is safe to dismiss a view controller ?

I have an iOS app that has a connection to a server. If we get disconnected, I want to be able to dismiss the top view controllers to get back to a "connecting to server" view controller. The problem is that a disconnection can occur at any time, including during a transition between view controllers.
The view controller hierarchy is like so:
ConnectingToServerViewController
SignInViewController
MainAppViewController
Other view controllers
When a disconnection is detected I want the view hierarchy to collapse back to:
ConnectingToServerViewController
So when a disconnection is detected, this method is called on the ConnectingToServerViewController to dismiss anything that it has presented and go back to attempting to connect to server:
- (void)restartSession
{
if (self.presentedViewController) {
[self dismissViewControllerAnimated:NO completion:nil];
}
}
However, if I try to dismiss while a view transition is occurring, I get errors such as
*** Assertion failure in -[UIWindowController transition:fromViewController:toViewController:target:didEndSelector:], /SourceCache/UIKit/UIKit-2380.17/UIWindowController.m:211
attempt to dismiss modal view controller whose view does not currently appear. self = <YYYYYViewController: 0x2089c8a0> modalViewController = <XXXXXViewController: 0x208e6610>
attempt to dismiss modal view controller whose view does not currently appear. self = <WWWWWWViewController: 0x1fd9e990> modalViewController = <YYYYYViewController: 0x2089c8a0>
The first of which will crash the app, the second will just not dismiss anything and continue to show the current presented view controller.
Thoughts:
delays won't work since we don't know when to start the delay
is there a way to track when view transitions complete?
should all view controllers override willAppear, didAppear and alert the app when it is safe to dismiss?
perhaps instead of dismiss, I should just set a new root view controller?
I've made sure that all overridden view(will|did)(dis)?appear methods call the appropriate super method.
Any solution that requires all view controllers to override view(did|will)appear methods to track state sounds like it could cause issues if we forget to set the base class for a new view controller.
Do something like this. Try this out once,
UIViewController *controller = self.presentingViewController; //THIS LINE IS IMP
[self dismissViewControllerAnimated:YES
completion:^{
[controller presentViewController:adminViewController animated:YES completion:nil];
adminViewController.view.superview.frame = CGRectMake(1024/2 - 400, 768/2 - 280, 800 , 560);//it's important to do this after
[adminViewController release];
}];
One way that has worked for me is to assign a new view controller to the root view controller. That way, views in the old hierarchy can animate and transition to their hearts content while we have new controllers.
eg
- (void)restartSession
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
ConnectingToServerViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"ConnectingToServerViewController"];
vc.modalPresentationStyle = UIModalPresentationFullScreen;
[UIApplication sharedApplication].delegate.window.rootViewController = vc;
}
I'm not sure if I'm aware of all the downsides to this though. Perhaps the old view controllers will never get freed because of a dangling strong reference? We're no longer reusing ConnectingToServerViewController, we have to recreate that each time.
I based the code on what I saw in this answer for Managing and dismissing Multiple View Controllers in iOS.
It seems like you are trying to dismiss the view controller when it is not currently on screen. To check if it is on screen you could use:
if (self.presentedViewController.view.window)
{
[self dismissViewControllerAnimated:NO completion:nil];
}
else
{
self.presentedViewController = nil;
}
I will answer in order.
is there a way to track when view transitions complete?
You could try with the UINavigationControllerDelegate (if you are using one of those). Other approach could be using a custom animator.
should all view controllers override willAppear, didAppear and alert the app when it is safe to dismiss?
That's an option. You are free to do it if you want. Another option is not to do that. I think that container view controllers such as navigation controller has better approaches.
I should just set a new root view controller?
I would suggest to do the opposite. I would set the SignInViewController / MainAppViewController as the root flow, and present modally ConnectingToServerViewController on demand. In my opinion that's a healthier approach.
Hope it helps.

I am really confused regarding few things in UIViewController

I am really confused regarding few things in UIViewController, I have already read the View Controller Programming Guide and searched lot on the Internet but still confused.
When I want to jump or switch from firstVC to secondVC how many types of methods are available? I am listing which I know:
UINavigationController
UITabBarController
presentModalViewController:
Add secondVC to root view
If secondVC is added to root view then how firstVC object will be released?
Is it a good practice to add every view I want to jump/switch to root view?
transitionFromView:
I dont understand this portion from Apple doc:
This method modifies the views in their view hierarchy only. It does
not modify your application’s view controllers in any way. For
example, if you use this method to change the root view displayed by a
view controller, it is your responsibility to update the view
controller appropriately to handle the change.
If I do this:
secondViewController *sVc = [[secondViewController alloc]init];
[transitionFromView:self.view toView:sVc.view...
Still viewDidLoad:, viewWillAppear:, viewDidAppear: are working fine: I don't need to call them. So why did Apple say this:
it is your responsibility to update the view controller appropriately to handle the change.
Are there any other methods available?
Actually the standard methods used are :
1) Using NavigationController
//push the another VC to the stack
[self.navigationController pushViewController:anotherVC animated:YES];
//remove it from the stack
[self.navigationController popViewControllerAnimated:NO];
//or presenting another VC from current navigationController
[self.navigationController presentViewController:anotherVC animated:YES completion:nil];
//dismiss it
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
2) Presenting the VC
//presenting another VC from current VC
[self presentViewController:anotherVC animated:YES completion:nil
//dismiss it
[self dismissViewControllerAnimated:YES completion:nil];
Never use the method you described in points 4. It's not a good practice to change root view controller's dynamically. window's root VC is usually defined on applicationdidfinishlaunchingwithoptions after that it shouldn't be changed , if you are to follow apple standards.
Example for transitionFromView:toView
-(IBAction) anAction:(id) sender {
// assume view1 and view2 are some subviews of self.view
// view1 will be replaced with view2 in the view hierarchy
[UIView transitionFromView:view1
toView:view2
duration:0.5
options:UIViewAnimationOptionTransitionFlipFromLeft
completion:^(BOOL finished){
/* do something on animation completion */
}];
}
}

Show presenting view controller with partial curl

I have a view controller with several textfields/text views and would like to allow the user to populate these fields with things they find on the web. To allow this I have a different web view presented modally. My problem is that I would like to use a partial page curl to display the first view controller (with any populated information). However, you can't present a live view controller a second time, and the new instance is not pre populated with whatever information was already placed in the textfields.
It seems like I could just pass that information back and forth (from the first instance, to the second view controller, then copy the info into the second instance, then copy back when I dismiss the second instance, etc) but it seems like there should be a better way.
I could also just add the web view controller as a subview, but I miss the functionality of the partial curl, which I really like.
Thanks!
However, you can't present a live view controller a second time, and
the new instance is not pre populated with whatever information was
already placed in the textfields.
You dont need to create a new view controller each time. Just store that view controller object as an instance variable and dont release it until your user is done.
if (myViewController == nil) {
myViewController = [UIViewController alloc] init];
}
[self presentViewController:myViewController animated:YES];
So I've kind of combined the comments and the posted answer.
First: I store an instance of the vc with the web view and add its view to the first view controller and then hide the view:
self.webVC = [self.storyboard instantiateViewControllerWithIdentifier:#"webView"];
self.webVC.delegate = self;
self.webVC.view.frame = self.view.bounds;
[self.view addSubview:self.webVC.view];
[self.webVC.view didMoveToSuperview];
self.findPoemVC.view.hidden = YES;
Then I used a category on UIView to uncurl and curl the view. I found the code here:
To show the view I perform a CurlDown animation like this:
self.findPoemVC.view.hidden = NO;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1.0];
[UIView setAnimationTransition:UIViewAnimationTransitionCurlDown
forView:self.webVC.view
cache:YES];
self.webVC.view.hidden = NO;
[UIView commitAnimations];
On the webVC I implemented a protocol and implement the delegate methods in the first view controller:
In the webVC:
#protocol PoemFinderDelegate <NSObject>
-(void)shouldUncurl;
-(void)doneWasPressed;
#end
The delegate was set earlier (see above) and the implementation of the delegate methods to partially uncurl and totally uncurl the webview were done like this inside the first view controller:
-(void)shouldUncurl{
[self.webVC.view animationPartialCurlUp];
}
-(void)doneWasPressed{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.50];
[UIView setAnimationTransition:UIViewAnimationTransitionCurlUp
forView:self.webVC.view
cache:YES];
self.webVC.view.hidden = YES;
[UIView commitAnimations];
}

Resources