I have an app where you can customize products to varying degrees. In some cases the options are split to two views, while in some other cases the first step isn't necessary.
What I would like is to treat all products the same and push the first customization step view controller to the navigation controller stack, let that view controller decide whether or not this step is necessary. If it is not necessary I want it to apply some default options to the product and immediately skip (before the transition animation) to step 2 while not allowing the user to back up to the first step.
The normal UINavigationController.viewControllers stack may look like this when at step 2:
[ListView (root)] -> [CustomizeStep1] -> [CustomizeStep2]
But I want it to apply the default values to the product and amend the view controller stack so that:
[ListView (root)] -> [CustomizeStep1]
----- becomes -----
[ListView (root)] -> [CustomizeStep2]
What I've tried is to use code like this in the CustomizeStep1 view controller:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (shouldSkipToStep2) {
UINavigationController *navController = self.navigationController;
// Move directly to step 2
UIStoryboard *storyboardLoader = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
UIViewController *customizeStep2VC = [storyboardLoader instantiateViewControllerWithIdentifier:#"customizeStep2"];
// Replace current view contoller
NSMutableArray *viewHierarchy = [NSMutableArray arrayWithArray:navController.viewControllers];
[viewHierarchy removeObject:self];
[viewHierarchy addObject:customizeVC];
// Apply new viewController stack
[navController setViewControllers:viewHierarchy animated:NO];
}
}
If I take a look at the navigation controller's viewControllers array after this has been set, everything looks as expected.
What happens in iOS 7
When doing this, the entire functionality of the UINavigationController breaks. The CustomizeStep1 view controller still animates in but is nonfunctional. Tapping the back button still shows CustomizeStep1. Trying to interact with the view controller crashes the app. (It works as expected if the view controller is displayed without the sliding transition, though.)
What happens in iOS 8
The CustomizeStep1 view controller still animates in, but immediately after the transition ends it snaps over to show CustomizeStep2. Other than that it works as intended.
So, my question is if there is a better place to add the code to amend the view controller stack on the navigation controller?
I obviously need to wait until the view controller has been added to the navigation controller, otherwise I can't replace the view controller in the stack. However, I need to be able to cancel the transition animation so that I can animate in CustomizeStep2 instead.
I appreciate if this is impossible, just wanted to check if anyone knows a good way around this.
Edit:
How I would like it to ideally appear to the user
Instead of viewWillAppear:, use viewDidAppear: which is called after the animation finishes.
You could have a boolean on your view controller denoting whether it is filled in or not:
#interface ViewControllerOne : UIViewController
#property (nonatomic, assign, getter = isInitiallyFilledIn) BOOL initiallyFilledIn;
#end
Then, when it is initially filled in, just denote this boolean value.
ViewControllerOne *viewController = [[ViewControllerOne alloc] init];
[viewController setInitiallyFilledIn:YES];
Now, in viewDidAppear:, check this boolean value and check whether that method has been launched before. If it hasn't been launched before (to allow editing) and it is initially filled in, push the next controller!
#interface ViewControllerOne
#property (nonatomic, assign) BOOL hasCheckedFillInStatusBefore;
#end
#implementation ViewControllerOne
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if ([self isInitiallyFilledIn] && ![self hasCheckedFillInStatusBefore]) {
// push the next view controller
}
[self setHasCheckedFillInStatusBefore:YES];
}
#end
Alternatively, if you want to display the two view controllers at the same time, you could alter the navigation stack:
// create instances of ViewControllerOne and ViewControllerTwo
NSMutableArray *viewControllers = [[[self navigationController] viewControllers] mutableCopy];
[viewControllers addObjectsFromArray:#[viewControllerOne, viewControllerTwo]];
[[self navigationController] setViewControllers:viewControllers animated:YES];
Note, the ViewControllerOne will not have viewDidLoad called so if you do any setup in that method (such as a back button title or the view controller title), you will either have to manually invoke that method before setting the view controllers or move that setup to the initializer.
To give some context, I have logic that programmatically decides what view controller to insert into the navigation controller. For example:
If(true){
MyViewController * MyObject = [[MyViewController alloc]init];
myNavigationController = [[UINavigationController alloc]initWithViewController:MyObject];
else {
MyOtherViewController * MyOtherObject = [[MyViewController alloc]init];
myNavigationController = [[UINavigationController alloc]initWithViewController:MyOtherObject];
}
self.tabBarController.viewControllers=[NSArray arrayWithObjects:myNavigationController,nil];
Hopefully that illustrates my point of how I insert views inside of navigation controller. Now onto my problem:
I have an action listener with a button inside of "MyViewController" that essentially replaces the navigation/tab bar index when the user clicks the button. Is it possible to update a navigation/tab bar index with just a button?
MyViewController.m
- (IBAction)MyActionListener:(id)sender {
MyOtherViewController *MyOtherObject = [[MyOtherViewController alloc] initWithNibName:#"MyOtherViewController" bundle:nil];
[self.view insertSubview:MyOtherViewController.view atIndex:2];
}
When I do this, I get a crash EXEC_BAD_ACCESS I'm just wondering if my implementation/approach is wrong. I noticed this question: Update UITabBar Views?
However, doesn't seem to fit the results I am looking for. Hopefully I am clear. Thanks!
Yes it is possible to switch your views on button click with navigation.
You currently in VC1 , you have other two vc VC2 & VC3 and on button click you choose VC2 or VC3 but you did not change the VC1 place.
This is the first time I'm trying to implement navigation from a tableView cell to another tableView using UINavigationController and it doesn't work for me.
I'm NOT using nib file and I have a simple tableView that I present it in a modal dialog in my app, it works fine, now I added the disclosureInidcator to one of it's cell, to make the user enable to choose from a fixed number of options available from another list(tableView). For this purpose I have another class that makes the second tableView. the problem is now navigation from the cell(contains disclosure icon)in first tableview to second tableView doesn't do anything, no error, no nothing. I guess the way I setup the navigation controller would be wrong, the code doesn't fall in delegate, or datasource of the second class at all.
in First TableView in method : didSelectRowAtIndexPath I tried to catch that row, then call the second tableView like this:
mySecondViewController *secondVC = [[[mySecondViewController alloc] initWithStyle:UITableViewStyleGrouped ] autorelease];
UINavigationController *navCont = [[UINavigationController alloc] initWithRootViewController: self];//not sure the first controller should act as the root controller?
[navCont pushViewController:secondVC animated:YES]; //it does nothing, no error,...
the second tableViewcontroller class contains all delegate and datasource methods, and initialization method:
- (id)initWithStyle:(UITableViewStyle)style
{
if ((self = [super initWithStyle:style])) {
}
return self;
}
and declared in interface as:
#interface stockOptionViewController : UITableViewController {
}
I tried to play with viewDidLoad, but didn't help.
Please help me cause I have no clue and all sample codes found is based on using nib files.
Thank,
Kam
Your navigation controller should be the root view controller of the app delegate's window, and the first view controller should be the root view controller of the navigation controller, then you can push new controllers onto it.
Please see the documentation for UINavigationController.
At the moment, you are creating a navigation controller but not putting it anywhere, so asking it to push new view controllers is a little pointless. You have the right code, just not in the right order.
You can present view control modally without nav controller
mySecondViewController *secondVC = [[[mySecondViewController alloc] initWithStyle:UITableViewStyleGrouped ] autorelease];
[self presentModalViewController:secondVC animated:YES];
UINavigationController should be the root view controller. In the current code, navCont is not on the view stack, so it won't work. Instead of pushing myFirstViewController in your appDelegate, push the UINavigationController to the stack and add myFirstViewController as its root view controller.
I have three view controllers. I used the flipsideproject template and then added another view controller.
There is a button on the first view controller that goes to the second view controller. There is a button on the second view controller that goes back to the first one. When switching between the first and second, those buttons always work.
It is the same situation with the second and third view controller. When I try to transfer between the first to second to third and then back to first, it does not work.
(1-->2-->3-->2-/->1) My poorly drawn diagram depicts the situation.
I had all of the back buttons connected to the back IBAction, which I thought was the problem. I then made another IBAction, but it has not fixed the problem.
1st view controller = MainViewController
2nd VC = FlipSideViewController
3rd VC = ChooseAlarmSound
This is for going 2->1 (this is the problem I think. It sometimes works)
- (IBAction)done:(id)sender
{
[self.delegate flipsideViewControllerDidFinish:self];
}
This is for going 2->3
- (IBAction)chooseSound:(id)sender
{
ChooseAlarmSound *controller = [[[ChooseAlarmSound alloc] initWithNibName:#"ChooseAlarmSound" bundle:nil] autorelease];
controller.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:controller animated:YES];
}
This is for going 3->2
- (IBAction)goBack:(id)sender
{
FlipsideViewController *controller = [[[FlipsideViewController alloc] initWithNibName:#"FlipsideViewController" bundle:nil] autorelease];
controller.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:controller animated:YES];
}
You presented your 3rd VC (going from 2 to 3) using modalviewcontroller. But then you tried to go back to 2nd VC (from 3rd to 2nd) using another modalVC. That will not let you go back to the previous instance of 2nd VC. You need to use dismissmodalviewcontrolleranimated method to do this. Checkout Apple website on modalviewcontroller class reference for detail info on this.
As suggested by user523234, all you need to do is call
[self dismissModalViewControllerAnimated:YES]
in the
- (IBAction)goBack:(id)sender
method of the 3rd view controller, instead of what you're doing, which is creating another instance of the 2nd view controller and presenting it.
The reason it's not working now, is because when you press the done button in the 2nd view controller it calls
- (IBAction)done:(id)sender
{
[self.delegate flipsideViewControllerDidFinish:self];
}
which is sending a message the 2nd view controller's delegate, which you haven't set in the case where you're going from 3->2.
I read SO about another user encountering similar error, but this error is in different case.
I received this message when I added a View Controller initially:
Unbalanced calls to begin/end appearance transitions for
<UITabBarController: 0x197870>
The structure of the app is as follow:
I got a 5-tab TabBarController linked to 5 View Controllers. In the initial showing tab, I call out a new View Controller to overlay as an introduction of the app.
I use this code to call the introduction view controller:
IntroVC *vc = [[IntroVC alloc] init];
[self presentModalViewController:vc animated:YES];
[vc release];
After this IntroVC view controller shows up, the above error shows.
p.s. I am using xCode 4.2 & iOS 5.0 SDK, developing iOS 4.3 app.
Without seeing more of the surrounding code I can't give a definite answer, but I have two theories.
You're not using UIViewController's designated initializer initWithNibName:bundle:. Try using it instead of just init.
Also, self may be one of the tab bar controller's view controllers. Always present view controllers from the topmost view controller, which means in this case ask the tab bar controller to present the overlay view controller on behalf of the view controller. You can still keep any callback delegates to the real view controller, but you must have the tab bar controller present and dismiss.
I fixed this error by changing animated from YES to NO.
From:
[tabBarController presentModalViewController:viewController animated:YES];
To:
[tabBarController presentModalViewController:viewController animated:NO];
As posted by danh
You can generate this warning by presenting the modal vc before the app is done initializing. i.e. Start a tabbed application template app and present a modal vc on top of self.tabBarController as the last line in application:didFinishLaunching. Warning appears. Solution: let the stack unwind first, present the modal vc in another method, invoked with a performSelector withDelay:0.0
Try to move the method into the viewWillAppear and guard it so it does get executed just once (would recommend setting up a property)
Another solution for many cases is to make sure that the transition between UIViewControllers happens after the not-suitable (like during initialization) procedure finishes, by doing:
__weak MyViewController *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf presentViewController:vc animated:YES];
});
This is general for also pushViewController:animated:, etc.
I had the same problem. I called a method inside viewDidLoad inside my first UIViewController
- (void)viewDidLoad{
[super viewDidLoad];
[self performSelector:#selector(loadingView)
withObject:nil afterDelay:0.5];
}
- (void)loadingView{
[self performSegueWithIdentifier:#"loadedData" sender:self];
}
Inside the second UIViewController I did the same also with 0.5 seconds delay. After changing the delay to a higher value, it worked fine. It's like the segue can't be performed too fast after another segue.
I had the same problem when I need to Present My Login View Controller from another View Controller If the the User is't authorized, I did it in ViewDidLoad Method of my Another View Controller ( if not authorized -> presentModalViewController ). When I start to make it in ViewDidAppear method, I solved this problem. I Think that ViewDidLoad only initialize properties and after that the actual showing view algorithm begins! Thats why you must use viewDidAppear method to make modal transitions!
If you're using transitioningDelegate (not the case in this question's example), also set modalPresentationStyle to .Custom.
Swift
let vc = storyboard.instantiateViewControllerWithIdentifier("...")
vc.transitioningDelegate = self
vc.modalPresentationStyle = .Custom
I had this problem because of a typo:
override func viewDidAppear(animated: Bool) {
super.viewWillAppear(animated)
instead of
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
It was calling "WillAppear" in the super instead of "DidAppear"
I had lot of problem with the same issue. I solved this one by
Initiating the ViewController using the storyboad instantiateViewControllerWithIdentifier method. i.e Intro *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"introVC"];
[self.tabBarController presentModalViewController : vc animated:YES];
I have the viewcontroller in my storyboard, for some reason using only [[introvc alloc] init]; did not work for me.
I solved it by writing
[self.navigationController presentViewController:viewController
animated:TRUE
completion:NULL];
I had this problem with a third party code. Someone forgot to set the super inside of viewWillAppear and viewWillDisappear in a custom TabBarController class.
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// code...
}
or
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// code...
}
I had the same error. I have a tab bar with 3 items and I was unconsciously trying to call the root view controller of item 1 in the item 2 of my tab bar using performSegueWithIdentifier.
What happens is that it calls the view controller and goes back to the root view controller of item 2 after a few seconds and logs that error.
Apparently, you cannot call the root view controller of an item to another item.
So instead of performSegueWithIdentifier
I used [self.parentViewController.tabBarController setSelectedIndex:0];
Hope this helps someone.
I had the same problem and thought I would post in case someone else runs into something similar.
In my case, I had attached a long press gesture recognizer to my UITableViewController.
UILongPressGestureRecognizer *longPressGesture = [[[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:#selector(onLongPress:)]
autorelease];
[longPressGesture setMinimumPressDuration:1];
[self.tableView addGestureRecognizer:longPressGesture];
In my onLongPress selector, I launched my next view controller.
- (IBAction)onLongPress:(id)sender {
SomeViewController* page = [[SomeViewController alloc] initWithNibName:#"SomeViewController" bundle:nil];
[self.navigationController pushViewController:page animated:YES];
[page release];
}
In my case, I received the error message because the long press recognizer fired more than one time and as a result, my "SomeViewController" was pushed onto the stack multiple times.
The solution was to add a boolean to indicate when the SomeViewController had been pushed onto the stack. When my UITableViewController's viewWillAppear method was called, I set the boolean back to NO.
I found that, if you are using a storyboard, you will want to put the code that is presenting the new view controller in viewDidAppear. It will also get rid of the "Presenting view controllers on detached view controllers is discouraged" warning.
In Swift 2+ for me works:
I have UITabBarViewController in storyboard and I had selectedIndex property like this:
But I delete it, and add in my viewDidLoad method of my initial class, like this:
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.selectedIndex = 2
}
I hope I can help someone.
This error will be displayed when trying to present an UINavigationController that is lazily initialized via a closure.
Actually you need to wait till the push animation ends. So you can delegate UINavigationController and prevent pushing till the animation ends.
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
waitNavigation = NO;
}
-(void)showGScreen:(id)gvc{
if (!waitNavigation) {
waitNavigation = YES;
[_nav popToRootViewControllerAnimated:NO];
[_nav pushViewController:gvc animated:YES];
}
}
As #danh suggested, my issue was that I was presenting the modal vc before the UITabBarController was ready. However, I felt uncomfortable relying on a fixed delay before presenting the view controller (from my testing, I needed to use a 0.05-0.1s delay in performSelector:withDelay:). My solution is to add a block that gets called on UITabBarController's viewDidAppear: method:
PRTabBarController.h:
#interface PRTabBarController : UITabBarController
#property (nonatomic, copy) void (^viewDidAppearBlock)(BOOL animated);
#end
PRTabBarController.m:
#import "PRTabBarController.h"
#implementation PRTabBarController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.viewDidAppearBlock) {
self.viewDidAppearBlock(animated);
}
}
#end
Now in application:didFinishLaunchingWithOptions:
PRTabBarController *tabBarController = [[PRTabBarController alloc] init];
// UIWindow initialization, etc.
__weak typeof(tabBarController) weakTabBarController = tabBarController;
tabBarController.viewDidAppearBlock = ^(BOOL animated) {
MyViewController *viewController = [MyViewController new];
viewController.modalPresentationStyle = UIModalPresentationOverFullScreen;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
[weakTabBarController.tabBarController presentViewController:navigationController animated:NO completion:nil];
weakTabBarController.viewDidAppearBlock = nil;
};
you need make sure -(void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated and -(void)endAppearanceTransition is create together in the class.
I had the same issue. When developing I wanted to bypass screens. I was navigating from one view controller to another in viewDidLoad by calling a selector method.
The issue is that we should let the ViewController finish transitioning before transitioning to another ViewController.
This solved my problem: The delay is necessary to allow ViewControllers finish transitioning before transitioning to another.
self.perform(#selector(YOUR SELECTOR METHOD), with: self, afterDelay: 0.5)
For me this error occurred because i didn't have UIWindow declared in the upper level of my class when setting a root view controller
rootViewController?.showTimeoutAlert = showTimeOut
let navigationController = SwipeNavigationController(rootViewController: rootViewController!)
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
Ex if I tried declaring window in that block of code instead of referencing self then I would receive the error
I had this problem when I had navigated from root TVC to TVC A then to TVC B. After tapping the "load" button in TVC B I wanted to jump straight back to the root TVC (no need to revisit TVC A so why do it). I had:
//Pop child from the nav controller
[self.navigationController popViewControllerAnimated:YES];
//Pop self to return to root
[self.navigationController popViewControllerAnimated:YES];
...which gave the error "Unbalanced calls to begin/end etc". The following fixed the error, but no animation:
//Pop child from the nav controller
[self.navigationController popViewControllerAnimated:NO];
//Then pop self to return to root
[self.navigationController popViewControllerAnimated:NO];
This was my final solution, no error and still animated:
//Pop child from the nav controller
[self.navigationController popViewControllerAnimated:NO];
//Then pop self to return to root, only works if first pop above is *not* animated
[self.navigationController popViewControllerAnimated:YES];
I encountered this error when I hooked a UIButton to a storyboard segue action (in IB) but later decided to have the button programatically call performSegueWithIdentifier forgetting to remove the first one from IB.
In essence it performed the segue call twice, gave this error and actually pushed my view twice. The fix was to remove one of the segue calls.
Hope this helps someone as tired as me!
Swift 5
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
//Delete or comment the below lines on your SceneDelegate.
// guard let windowScene = (scene as? UIWindowScene) else { return }
// window?.windowScene = windowScene
// window?.makeKeyAndVisible()
let viewController = ListVC()
let navViewController = UINavigationController(rootViewController: viewController)
window?.rootViewController = navViewController
}