I present a modal view controller for various UI settings in an iOS app. One of those settings allows the user to select a different main view. When they hit "Done" I want to dismiss the modal view and have the newly-selected view controller appear, without a momentary delay where the old view controller segues to the new view controller. How could this be implemented?
Update:
Here is a method I successfully implemented using Eugene's technique, but without the app delegate. Instead, this implementation is specific to my scenario where a view controller in a navigation stack presents the modal view controller in a Utility app.
- (void)swapFrontSideViewController;
{
UINavigationController *navigationVC = (UINavigationController *)[self presentingViewController];
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:navigationVC.viewControllers];
UIViewControllerSubclass *selectedViewController = nil;
if ([self.selectedFrontSide isEqualToString:FRONT_SIDE_NAME1]) {
selectedViewController = [self.storyboard instantiateViewControllerWithIdentifier:FRONT_SIDE_NAME1];
} else if ([self.selectedFrontSide isEqualToString:FRONT_SIDE_NAME2]) {
selectedViewController = [self.storyboard instantiateViewControllerWithIdentifier:FRONT_SIDE_NAME2];
}
if (selectedViewController) {
[viewControllers replaceObjectAtIndex:viewControllers.count -1 withObject:selectedViewController];
[navigationVC setViewControllers:viewControllers];
self.delegate = selectedViewController;
} else {
NSLog(#"Error: Undefined Front Side Selected.");
}
}
- (IBAction)doDismiss:(id)sender {
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; // Get the app delegate
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:appDelegate.navigationController.viewControllers]; // fetch its navigationController viewControllers stack
UIViewController *replacementController; //initialize replacement controller
[viewControllers replaceObjectAtIndex:viewControllers.count -1 withObject:replacementController]; // replace the top view controller in stack with the replacement one
[appDelegate.navigationController setViewControllers:viewControllers]; //change the stack
[self dismissModalViewControllerAnimated:YES];
}
Related
Possibly simple request here but I can't find the solution and it is bugging me for days.
I'm building simple options page where users could jump to desired page and I'm using UINavigationController instance to manage hierarchy. My storyboard looks like this:
Viewcontrollers are connected with push segues fired on next button, while I use [self.navigationController popViewControllerAnimated:YES] for previous button. If I connect, for instance, button labeled 2 on 5VC with 2VC through push segue, I get to the second page, but if I want to use previous button I will land to options page or 5VC which is something I don't want. Instead, I would like to be able to use previous button to go to first page, while on second page.
The way I see it, if I am on third page (3VC) and I call options page (5VC) and select button 3, system should stack 1VC-2VC and present 3VC, so I would be able to go to 2VC through [self.navigationController popViewControllerAnimated:YES] request.
I think the solution is somehow connected with setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated, but I don't know the syntax how to make things work.
You have 3 cases
Back to one of ancestors in the middle with push
case 5VC=>2VC, 5VC=>3VC:
NSArray *vcs = self.navigationController.viewControllers;
for(NSInteger i = vcs.count - 2; i > 0; i--) {
// find the target and its parent view controller
// i.e. class of 2VC is ViewController2
if([vcs[i] isKindOfClass:[ViewController2 class]]) {
UIViewController *target = vcs[i];
UIViewController *parent = vcs[i - 1];
// pop to its parent view controller with NO animation
[self.navigationController popToViewController:parent animated:NO];
// push the target from its parent
[self.navigationController pushViewController:target animated:YES];
return;
}
}
Back to the root view controller with push
case 5VC=>1VC:
UIViewController *root = self.navigationController.viewControllers.firstObject;
// reset view controllers stack with self as root.
[self.navigationController setViewControllers:#[self] animated:NO];
// push target from self
[self.navigationController pushViewController:root animated:YES];
// reset navigation stack with target as root.
[self.navigationController setViewControllers:#[root] animated:NO];
Push new VC from one of ancestors
case 5VC=>4VC
NSArray *vcs = self.navigationController.viewControllers;
for(NSInteger i = vcs.count - 1; i >= 0; i--) {
// find the parent view controller
if([vcs[i] isKindOfClass:[ViewController3 class]]) {
UIViewController *parent = vcs[i];
// pop to the parent with NO animation
[self.navigationController popToViewController:parent animated:NO];
// perform segue from the parent
[parent performSegueWithIdentifier:#"push4VC" sender:self];
return;
}
}
On the particular your case(5VC=>4VC), you know 3VC is the self's parent, you can get the parent directly:
NSArray *vcs = self.navigationController.viewControllers;
UIViewController *parent = vcs[vcs.count - 2]; // [vcs.count-1] is self.
[self.navigationController popToViewController:parent animated:NO];
[parent performSegueWithIdentifier:#"push4VC" sender:self];
I have a login view that takes a username and password and grabs an webpage string. I want to then pass this information to the next view where itll load the html string. The problem is that the second view is managed by a tab bar controller (the one that was generated by default tab bar template in xcode). However, when i segue into it I lose the tab bar controller. My segue goes from the login view controller directly to the second view. I tried directing it to the tab bar controller itself but I wasn't able to pass the data from the login view to the first view. Any tips or workarounds for this?
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([[segue identifier] isEqualToString:#"ShowSchedule"]){
SocialSchedulerFirstViewController *scheduleController = (SocialSchedulerFirstViewController *)segue.destinationViewController;
NSString *webPageCode = htmlString;
scheduleController.htmlString = webPageCode;
}
}
You have to change the segue to go from your login view controller directly to tab bar controller. You had problem with passing data because you try to pass it to tab bar,I guess, but you should get reference to your view controller first. Try this:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([[segue identifier] isEqualToString:#"ShowSchedule"]){
UITabBarController *tabar=(UITabBarController*)segue.destinationViewController;
SocialSchedulerFirstViewController *scheduleController = (SocialSchedulerFirstViewController *)[tabar.viewControllers objectAtIndex:0];;
NSString *webPageCode = htmlString;
scheduleController.htmlString = webPageCode;
}
}
It will works if your SocialSchedulerFirstViewController is first tab of tab bar controller.
Remove segue login to tabbar controller, you need to do it programatically
Set your tabBarcontroller in storyboard as initial controller
Inside AppDelegate.m
if (authenticatedUser)
{
self.window.rootViewController = [[UIStoryboard storyboardWithName:#"Main" bundle:[NSBundle mainBundle]] instantiateInitialViewController];
}
else
{
UIViewController* rootController = [[UIStoryboard storyboardWithName:#"Main" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:#"LoginViewController"];
UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:rootController];
self.window.rootViewController = navigation;
}
Inside LoginController
- (IBAction)actionLogin:(id)sender {
AppDelegate *appDelegateTemp = [[UIApplication sharedApplication]delegate];
appDelegateTemp.window.rootViewController = [[UIStoryboard storyboardWithName:#"Main" bundle:[NSBundle mainBundle]] instantiateInitialViewController];
}
I'm trying to make a modal view controller in my app delegate (I created a function called showLoginView). But whenever I try to call it I get a warning in XCode:
Warning: Attempt to present <PSLoginViewController: 0x1fda2b40> on <PSViewController: 0x1fda0720> whose view is not in the window hierarchy!
Here's the method code:
- (void)showLoginView
{
PSLoginViewController *loginViewController = [[UIStoryboard storyboardWithName:#"MainStoryboard" bundle:NULL] instantiateViewControllerWithIdentifier:#"PSLoginViewController"];
[self.window.rootViewController presentViewController:loginViewController animated:NO completion:nil];
}
How can I add the view to the window hierarchy? Or maybe I'm doing something very wrong?
You can't display a modal view controller from the appDelegate. You need to display a modal ViewController from whichever viewController is currently displaying full-screen. In other words, you need to put that code into your root view controller, or whichever one you want to display the modal vc from...
Also, you'll want to use the method "presentModalViewController" to present the modal. You can set properties on the modal vc such as:
vC.modalPresentationStyle = UIModalPresentationFormSheet;
vC.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:vC animated:YES];
You can actually present a modal view Controller from the AppDelegate as long as you detect the current visible viewController and take care of the case where you current controller is a navigationController.
Here is what I do:
UIViewController *activeController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([activeController isKindOfClass:[UINavigationController class]]) {
activeController = [(UINavigationController*) activeController visibleViewController];
}
[activeController presentModalViewController:loginViewController animated:YES];
UIViewController *activeController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([activeController isKindOfClass:[UINavigationController class]])
{
activeController = [(UINavigationController*) activeController visibleViewController];
}
else if (activeController.modalViewController)
{
activeController = activeController.modalViewController;
}
[activeController presentModalViewController:vc animated:YES];
I ran into this problem on iOS 7 - the key to making any of the proposed solutions work was to call
[self.window makeKeyAndVisible];
in your AppDelegate.
After that call, presenting a modal view from the window's rootViewController worked.
Another reason for that warning can be that you want to present a view controller from an instance which is not the top most view controller.
So first you have to get the topmost UIViewController and using this instance to call presentViewController:
UIViewController *root = [UIApplication sharedApplication].keyWindow.rootViewController;
while (root.presentedViewController) {
root = root.presentedViewController;
}
You can NSLog(#"%#", self.window.rootViewController), and see what the rootViewController really is.
I came into this problem, when the rootViewController is a normal UIViewController.
Replace it with a UINavigationController, wish it will help.
Faced this issue while trying to present controller from the call of delegate of other controller . i.e : show search filter with delegate , once done back to my controller and receive data via the delegate then present controller , all I had to do is to dispatch the present code cause while in a delegate you're in another thread , that's why you're presenting on your view from main thread another controller from that other thread , so have to go back to main thread , just put the presenting code like this :
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController:searchVC animated:true completion:nil];
});
Hope this helps !
I am testing an application in IOS5 using OCUnit. Before posting my problem I read all the relevant posts here as much I could. It really helped. I am now facing the problem below ,which I tried to narrate as clearly as possible.
My application has a login screen which is nothing but a view controller say: initialController. The root controller of my app delegate is the Navigation controller. The property initWithRootViewController of the Navigation controller is set with this initialController.
Now after logging in , the application loads another controller and I want to do unit testing in that controller without having to go through this login process.
Post login, another controller say PostloginController gets loaded, this controller's Navigation item is customized with 2 button's say: button1 and button2.
These buttons are added as the subviews of UIBarButtonItem and this UIBarButtonItem is set as the rightBarButtonItem of the PostloginController.navigationItem.rightBarButtonItem.(pseudo code)
finally PostLoginController is pushed in the Navigation controller
navigationcontroller pushviewcontroller:postLoginController.(pseudo code)
I have to write unit test code which on button1's UIControlEventTouchUpInside should launch another controller say :Newcontroller.
Loading this new controller on button1's event complete one test case.
My problem is that I don't get the subview of UIBarButtonItem which should be 2 buttons. The log show me there is one controller in the Navigation Controller and only one subview. My code is as follows:
- (void)setUp
{
[super setUp];
// Set-up code here.
self.appDelegate = (MYAppDelegate*)[[UIApplication sharedApplication] delegate ];
self.navigationController = (UINavigationController *)self.appDelegate.window.rootViewController;
}
- (void)testAppDelegate
{
STAssertNotNil(self.appDelegate, #"Cannot find the application delegate");
NSArray *tempArray = [self.navigationController viewControllers];
NSLog(#"Number of controllers in navigationController = %i", [tempArray count]);
}
- (void) testButton1
{
//self.myController = (PostLoginController*)self.navigationController.topViewController;
UIBarButtonItem *barButtonItem = self.navigationController.topViewController.navigationItem.rightBarButtonItem;
UIView *customView = barButtonItem.customView;
NSArray *subviews1 = customView.subviews;
NSLog (# "Got subviews");
NSLog(#"Number of subviews = %i", [subviews1 count]);
if ([subviews1 count] == 0)
{
NSLog (# "no subviews");
}
for (UIView* view in subviews1)
{
NSLog(#"%#", view);
if([view isKindOfClass:[UIButton class]])
{
UIButton *btn = (UIButton*)view;
NSLog (# " UIButton parsed");
// Check if Button1 clicked.
NSData *data1=UIImagePNGRepresentation([btn backgroundImageForState:UIControlStateNormal]);
NSData *data2=UIImagePNGRepresentation([UIImage imageNamed:#"BUTTON1.png"]);
if([data1 isEqualToData:data2]) // CHECKING IF BUTTON1 OR BUTTON2 PRESSED BY IMAGE SINCE THEY DONT HAVE ANY TAGS SET.
{
[btn sendActionsForControlEvents:(UIControlEventTouchUpInside)];
STAssertTrue([self.navigationController.visibleViewController isMemberOfClass: [NewController class]], #" NewController failed to load!"); //BUTTON1 CLICK SHOULD LAUNCH NEWCONTROLLER - COMPLETES ONE TEST CASE.
}
}
}
}
I would like to know what kind of trick to use for "not pushing" a view controller into the navigation controller stack (iOS)
I have this :
If user is not logged, show view A then show B
If user is logged, show B
As I am using the storyboard, I used a performSegue if the user is logged so he goes directly to B. But with this method, the Navigation Controller gets a push of view A in the stack.
I was thinking of poping out a level of the stack in some void (but I don't know how to do this).
I was also thinking of not pushing the view into the nav controller stack (but I don't know how to do this).
Thanks
Update :
I tried this :
//The view B
TabBarMain* mainViewController = [[TabBarMain alloc] init];
//If already logged in
if([username length] == 0)
{
NSArray *viewControllers = [NSArray arrayWithObject:mainViewController];
[self.navigationController setViewControllers:viewControllers animated:NO];
}
The problem of this code is that it shows me a black screen (doesn't crash). It seems that I need to init something and I have nothing in my TabBarMain.m, I don't know what to write in there. This TabBarMain is linked to the Tab Bar Controller of the Storyboard.
Is there no other way ?
Try this on for size in your rootViewController's viewDidLoad.
- (void)viewDidLoad
{
NSArray *viewControllers
if (logged) {
NSArray *viewControllers = [NSArray arrayWithObject:viewControllerB];
} else {
NSArray *viewControllers = [NSArray arrayWithObject:viewControllerA];
}
[self.navigationController setViewControllers:viewControllers animated:NO];
}
Since your viewController is linked in Storyboard and not instantiated in code you need to instantiate it from the storyboard not your empty code. Make sure the identifier matches the identifier for your ViewController in your storyboard.
TabBarMain *mainViewController = [[UIStoryboard storyboardWithName:#"MainStoryboard" bundle:NULL] instantiateViewControllerWithIdentifier:#"tabBarMain"];