I have an app which comprises of 2 main controller types:
1) A UITableViewController - which acts as a navigation screen
2) A UIViewController contained within a UINavigationController - which shows the main app content.
The TableViewController content looks a little like this:
Page 1
Page 2
Page 3
... etc
I've defined a property on AppDelegate - an array called PageViewControllers.
When the application starts, a new instance of UIViewController is created for each Page within the app.
The first Page's controller is set as the UINavigationController's rootViewController.
When the user selects a row in the UITableView controller, the UINavigationController pushes or pops to the relevant view controller for that row. (If I tap "Page 3", it pushes to the controller for Page three).
I have this working well - the only problem is the app occasionally crashes when trying to jump far back in the navigation stack. For example, from Page 15 to Page 2
The error message I'm getting is:
*** Terminating app due to uncaught exception 'RuntimeError', reason: 'NSInternalInconsistencyException: Tried to pop to a view controller that doesn't exist.
I think the UINavigationController might be releasing some controllers. I thought the app would hold all of the previous controllers in memory to allow the UINavigationController's back button to behave as expected?
Any idea how I can prevent this from happening or is there something I've missed?
Update
Here's the code where I push/pop the navigation controller after a table row is selected. (It's Rubymotion)
def tableView(tableView, didSelectRowAtIndexPath: indexPath)
# first we need to work out which controller was selected...
page = Page.current
currentPageController = appDelegate.pageControllers[page.absoluteIndex]
currentPageControllerIndex = appDelegate.pageControllers.index(currentPageController)
nextPageController = appDelegate.pageControllers[Page.pageAtIndexPath(indexPath).absoluteIndex]
nextPageControllerIndex = appDelegate.pageControllers.index(nextPageController)
case
# When we're moving forward in the stack...
when currentPageControllerIndex < nextPageControllerIndex
for controller in appDelegate.pageControllers[(currentPageControllerIndex + 1)..nextPageControllerIndex]
# push the next controller on to the nav stack, only animate if it's the last one
appDelegate.rootViewControllerNav.pushViewController(controller, animated: controller == nextPageController)
end
# When we're moving backward in the stack...
when currentPageControllerIndex > nextPageControllerIndex
appDelegate.rootViewControllerNav.popToViewController(nextPageController, animated: true)
# When we select the same step again...
else
NSLog("Selected the same page")
end
# close the side menu afterwards
appDelegate.rootViewControllerNav.sideMenu.toggleSideMenuPressed(self)
end
"I thought the app would hold all of the previous controllers in memory to allow the UINavigationController's back button to behave as expected?"
As long as you're going forward this is true -- as you push a new view controllers, it adds the new one to the previous ones on the stack. But, when you popViewController, it's removed from the stack, and if you don't have a strong reference to it, it will be deallocated. So if you go 1-->2-->3........-->15 and then pop to 2, it should work, but if you do something like this:
1-->2-->3 popTo 1 then push to-->15 then pop to 2, neither 2 nor 3 will exist any more.
So, you should pay attention to what sequence you're picking cells in your table, and note when you get the error. See if it corresponds to this explanation.
Cracked it!
The problem here was in the logic elsewhere in my app. You'll see that page is set from Page.currentPage.
page = Page.currentPage
Page.currentPage should always be set to the page that's currently being displayed.
The code to set current page was in the viewDidLoad method of the PagesViewController instead of the viewDidAppear:animated method - meaning that one a page had been viewed, it wouldn't set Page.currentPage again.
Fixed now - thanks a lot for the ideas and tips shared!
Related
This is kinda long but I want to give as many details as possible.
I have created a status bar app. One of the things I've done is made it to where there is a "Settings" menu item that if you click on, an NSWindow pops up with 5 different buttons -- these buttons are tied to their own custom NSViewController so that if you click them, the View Controller changes, but the window remains the same, just like any other StatusBar app.
On one of the view controllers is a popup button with several user selectable options. If I add a button to the specific VC (view controller) and link that to an ibaction that pulls the selected index of the popup button, it works just fine (i.e. I select item 2, it logs item 2).
Now here's where things get. . . odd. I've created a submenu that would in theory allow the user to quickly change the selected item of the popup linked to my custom view controller. Think of this submenu as being labeled 1-4 and clicking one of those items would change the index of the popup button, however every time I try this and follow it up with a NSLog to ensure the button has changed, it ALWAYS reads zero, in other words, it NEVER changes.
Here's what I tried.
Just like with every other view controller I've ever used, I create a new object of my view controller (alloc, init), and reference the popup button object that i've created as a property. This compiles just fine, but returns only zero no matter what is selected.
I've tried calling the method that I know works (the button that is on the view controller containing the popup), but again, this returns only zero no matter what is selected.
Any advice would be greatly appreciated.
tl;dr: can't change a popupbutton on a view controller from the app delegate or nsstatusmenu item, it just always returns zero.
EDIT: I have found a work around since I can communicate to my app delegate just fine, I created an int property and set it to zero. From there, whenever my popupbutton on my VC changes, I change the value of the app delegate int property and just ensure that whenever the nib launches, it set's the selected item to the app delegate property. I feel there has to be a more straight forward way to just get the value of the popup and change it from the app delegate.
EDIT 2: Some code as requested. I realized I needed to point the the view controller by using initwithnibname from the app delegate and then load the view. Doing so allows me to pull the first object in the popup button, any subsequent changes are not recognized unless I do so via my test button on the view controller itself. Also, I'm adding values to my popup in my awakefromnib in the view controller.
searches_VC = [[Searches_ViewController alloc] initWithNibName:#"Searches_ViewController" bundle:nil];
[searches_VC loadView];
NSLog(#"Selected Title: %#",[[searches_VC profilePopupOutlet] titleOfSelectedItem]);
I am trying to do if user give correct password it will go back to the current last view controller where i was,like in Ios if u enter background in your app and after sometime you enter foreground then you will go back to the last view i.e where you was before entering background.please tell me how to implement this.
if you want to pop to the root controller you can use popToRootViewControllerAnimated.
[self.navigationController popToRootViewControllerAnimated:YES];
From Apple doc of UINavigationController
popToRootViewControllerAnimated:
Pops all the view controllers on the stack except the root view controller and updates the display.
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated
If you have set up a UINavigationController and its root controller is called A, then if you navigate from A to B and then from B to C you have two possibilities to come back to a previous controller (you can have others but I list the main ones):
navigate back from C to B with popViewControllerAnimated
navigate back from C to A with popToRootViewControllerAnimated
I would suggest using a modal view controller that is not actually added to you parent controllers hierarchy (child view controllers) and can be shown and dismissed at your discretion take a look here for a great example.
I have two UICollectionViewControllers - lets call them master and slave. The master view controller is configured with layout A and the slave with layout B. I set useLayoutToLayoutNavigationTransitions on master to YES and push the slave onto the navigation stack. The layout changes from A to B and I can navigate < back and see layout A. All is good.
However, in response to the user typing some text into a search box in the master view controller, I need to change the layout of the master collection view controller to layout C. This works as expected, but when I push the slave collection view controller onto the stack (configured with layout B) and navigate < back, I end up back at the master collection view controller with layout A, not layout C, as expected.
So far, I've tried:
putting a symbolic breakpoint on -[UICollectionView setCollectionViewLayout:] (and it's related/sibling methods) to check whether something was messing with the collection view layout before pushing the slave onto the stack. I see the break point getting hit when I expect the layout to change, but nothing unusual in the process of popping the slave collection view controller off the navigation stack.
stopping in the debugger and verifying the type of the collection view's collectionViewLayout property and I can see it is set to an instance of layout C before pushing the slave collection view onto the stack. After navigating backwards, I can see the layout has returned to the type of layout A and not layout C (as expected), which tallies with what I'm seeing visually.
implementing -navigationController:willShowViewController:animated: delegate callback and setting the collection view layout to what's required. However this appears to get over-written.
implementing -navigationController:didShowViewController:animated: and setting the collection view layout, but this results in an infinite loop and errors logged to the console.
Does anyone have any ideas what's going on here and how to solve this? I'm wondering if it's a straight-up UIKit bug, and if so, if there are any potential workarounds.
Okay, so I'm building an universal iOS app with an initial login view (view controller named LoginVC), just a plain simple UIViewController. If the login is successful the app segues to an navigation controller (MainNavigationVC). I created this segue through the storyboard gui of XCode, so no programmatic creation of the nav controller is done. The nav controller is presented modally in fullscreen, so the rest of the app is run atop the login view, with this nav controller as the centerpiece of everything.
The navigation controller contains a view (with a view controller named UserStartPageVC), and in its navigation bar is a logout button. This button sends an target action to UserStartPageVC, with the goal of dismissing the nav controller thus bringing the user back to the login view.
So far everything works fine. I can login and use the app as intended. But! When I log out and then re-login XCode tells me this:
Warning! Attempt to present <MainNavigationVC: 0x753110> on
<LoginVC: 0x756fcf0> while a presentation is in progress!
I suppose this means that the login view is trying to modally display a MainNavigationVC navigation controller, but another one is already displayed, right? But how? Can a view be presented without showing?
And how can I get rid of the old nav controller when logging out? I've tried several ways of dismissing the modal view, for instance:
from within UserStartpageVC running
[x dismissViewControllerAnimated:YES completion:NULL]
[x dismissModalViewControllerAnimated:YES]
where x is either self, self.parentViewController or self.presentingViewController.
setting the LoginVC as a property in UserStartpageVC and running
[self.loginVC dismissViewControllerAnimated:YES completion:NULL]
and so on.
All of the tested calls actually brings me back to the login screen, so it's kind of working.
Any ideas? Relevant code samples can be provided if necessary, I just couldn't figure out which pieces that were of interest. The seguing to the navigation controller has no code (except for a performSegueWithIdentifier:sender:), and the code for dismissing it is the part I cannot seem to get straight.
As a sidenote. So far this isn't a REAL problem; the app runs, and it IS possible to logout and re-login without any other side-effects than an error message in XCode. But I suppose this will be a memory leak if users logout and login multiple times, and I'm not in the mood of an unnecessary rejection from Apple.
I discovered another way to get the exact same error message. Lucky me!
If you created a segue at one point and had it tied to a button (click button -> new view) and then later give that segue a name and invoke it directly using
[self performSegueWithIdentifier:#"identifierName" sender:self];
then you can get this error because you can effectively trigger the segue twice. I thought making the button invoke an IBAction would turn off the segue I had set up in the first place, but apparently not. Hitting the button triggered the segue twice, but after I deleted the segue and re-created it as a manual segue on the view with the same identifier then I was able to invoke it via the above code and not get the warning message.
Hoopla! My bad.
Seemed I had set up the notification observing from the login API call in a stupid way. For every time the user triggered a login (or re-login), it added itself as an observer for the notification; the result was that it performed one more segue for every time a login was done.
And doing multiple segues at the same time, is... well, obviously bad.
I'm writing a program with a UITableView with and add button in the Navigation Bar which leads to an edit page. When you click on an item in the table, a view (rView) is pushed with information pertaining to that item. This view has an edit button that also leads to the edit page. Is there a way that I could put an if statement for the done button on the edit page that says "if parentViewController is the UITableView to go to rView, else popViewController?" I would assume there is a way to do this, but I'm not sure of the syntax to do so. Thanks
If I understand correctly you have a UINavigationController and push onto it
a UITableView
an "rViewController" (you can't push a view, must be a controller)
an "EditController"
But there is a possibility that step 2 is omitted and you go directly to the edit screen.
Now when the last controller is popped, you want to be able to always go to a "rViewController", even if it's not on the stack.
First of all, the parentViewController is NOT the previous controller on the stack, but rather the UINavigationController itself, so it has nothing to do with the present problem.
The way to do this is by setting the UINavigationController's viewControllers property explicitly with an NSArray. I haven't tried this but this should work:
When a user presses the "add" button, instead of just pushing the edit view controller, do something like:
NSArray* stack = navigationController.viewControllers;
navigationController.viewControllers = [stack arrayByAddingObject:rViewController];
[navigationController pushViewController:editController animated:YES];
(By the way, I would suggest not using names like "rView" except maybe for very short-lived local variables, like in a loop. Using descriptive names is very much part of the Cocoa idiom and will help you a lot in the long run.)