I have a tabbed application with one of the tabs being a map view. The user places a pin in a location and in the annotation callout there is a disclosure button. I want this button to push to a detail view with more specific details about the annotation. I have searched for related questions but none of them seem to have the same problem with it as I do. When the disclosure button is clicked, the application crashes with the exception: 'NSInvalidArgumentException', reason: '-[MapView tableView]: unrecognized selector sent to instance 0x8434360'
This is where I attempt to push the detail view controller. The log statement returns a navigation controller so it is not an issue of sending a message to nil.
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
DetailViewController *dvc = [[DetailViewController alloc] init];
NSLog(#"%#", [self navigationController]);
[[SpotsStore defaultStore] setSelectedSpot:[view annotation]];
[[self navigationController] pushViewController:dvc animated:YES];
}
I instantiated the navigation controller in the app delegate:
UINavigationController *mapnc = [[UINavigationController alloc] initWithRootViewController:mv];
[mapnc setNavigationBarHidden:NO];
[[mapnc tabBarItem] setTitle:#"Map"];
[[mapnc tabBarItem] setImage:[UIImage imageNamed:#"mapicon.png"]];
[tbc setViewControllers:[NSArray arrayWithObjects:nc,mapnc, nil]];
// nc is a different navigation controller I instantiated earlier.
[[self window] setRootViewController:tbc];
I would really like any help with this, or just send me in the right direction. I could not find anything about the navigation controller sending the method tableView. Or why it doesn't work in this case.
I would stongly recommend using storyboards. Create a tabbarcontroller. Create a navigationcontroller with 2 viewcontrollers: the first is the map iewcontroller, the second is the detailviewcontroller. Ste this navigationcontroller to be one of the tabbarcontrollers viewcontrollers.
In your calloutaccessorytapped method call performSegue with the identifier that you defined in the storyboatd.
I found the problem. It was a piece of code in my detail view viewWillAppear method in which i reloaded the tableView. Thanks for your help though!
Related
I am working on an app whose main UI is based on a tab bar controller.
In one of the tabs I have a collection view, which drills down to a detail view via a navigation controller.
What I am trying to do is upon receipt of a push notification I would like to select this specific tab, fetch the latest data from the server, find the particular item to display, then push the detail view on to the screen to display said item.
My problem is I get the following message after collectionView:didSelectItemAtIndexPath:
Terminating app due to uncaught exception 'NSGenericException',
reason: 'Could not find a navigation controller for segue
'FavouriteItem'. Push segues can only be used when the source
controller is managed by an instance of UINavigationController.'
Here is what I am doing so far:
App Delegate application:didReceiveRemoteNotification:
[self selectFavouritesTab];
NHFavouritesViewController *favouritesViewController = [[UIStoryboard storyboardWithName:#"Main" bundle:nil] instantiateViewControllerWithIdentifier:#"Favourites"];
[favouritesViewController displayFavouriteForPushNotificationWithId:favouriteId];
From FavouritesViewController - After fetching the latest favourites, I send a message to displayFavouriteItemWithId:
- (void)displayFavouriteItemWithFavouriteId:(NSNumber*)favouriteId
{
NSArray* results = [_collectionViewData filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"SELF.favouriteId == %#", favouriteId]];
NSInteger row = [_collectionViewData indexOfObject:[results lastObject]];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
[[self collectionView] selectItemAtIndexPath:indexPath animated:YES scrollPosition:UICollectionViewScrollPositionNone];
[self.collectionView.delegate collectionView:self.collectionView didSelectItemAtIndexPath:indexPath];
[self performSegueWithIdentifier:#"FavouriteItem" sender:self];
}
And it is at this point it crashes. I understand what the crash message is saying, however what I don't know is how to place NHFavouritesViewController inside a navigation controller (which is embedded inside one in the storyboard) when I respond to the push notification in the app delegate?
You can wrap a view controller in a standard navigation controller with:
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:favouritesViewController];
But I can't see from your code above how favouritesViewController is presented in the tabBarController. If you are doing it in a storyboard, then just drag in a blank navigation controller, hook the relevant tab of your tabBarController to the navigation controller (Ctrl-drag, then select "Relationship segue: viewControllers", and then hook from the navigation controller to your FavouritesViewController (likewise).
EDIT:
If that is already done in the storyboard, then you need to amend your code to pickup the existing version of NHFavouritesViewController, instead of instantiating new. Something like (assuming you have a reference to your Tab Bar Controller in self.tabBarController, and the favouritesViewController is in the tab with index favouritesTab (I assume you can get these, since you already have a method to select the tab):
UINavigationController *navController = (UINavigationController *)self.tabBarController.viewControllers[favouritesTab];
NHFavouritesViewController *favouritesViewController = (NHFavouritesViewController *) navController.rootViewController;
The problem that you're having is that you are not instantiating the navigation controller.
By loading the favourites view using that method you are literally only creating that one view controller.
So then when you are telling it to push it can't because you didn't instantiate the navigation controller from the storyboard.
The chances are that the navigation controller already exists so you need to get hold of that instead of creating new controllers.
I'm on a mobile right now so can't answer fully but let me know if you're still struggling and I'll see if I can ad done code. Will prob need to see more code first though.
In my app I need to manage a navigation controller and move it in these viewcontrollers, so I do it
UINavigationController *navController = (UINavigationController*) [self.storyboard instantiateViewControllerWithIdentifier:#"navigationcontroller"];
[navController addChildViewController:firstViewController];
[navController addChildViewController:secondViewController];
[navController addChildViewController:thirdViewController];
[navController addChildViewController:fourthViewController];
[self presentViewController:navController animated:YES completion:nil];
first problem: navigation open at first fourthviewcontroller, why?
second problem: if from secondviewcontroller i do it to pass at first:
[[self navigationController] pushViewController:self.navigationController.viewControllers[0] animated:NO];
I have a crash that say:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Pushing the same view controller instance more than once is not supported
why? can you help me?
First question
Every time you use "addChildViewController:", the new controller is added at the top of the stack. The last one inserted, your fourthViewController, is at the top of the stack, so it is shown when you call the method
[self presentViewController:navController animated:YES completion:nil];
Second question
It depends on the pushViewController: method itself. In the Apple documentation the doc said that:
The viewController added cannot be an instance of tab bar controller and it must not already be on the navigation stack.
Your app crashes because self.navigationController.viewControllers[0] is already on navigation stack.
First Problem
You're pushing four view controllers onto the navigation stack. So, your stack, after each step, looks like this:
[navController addChildViewController:firstViewController];
Stack: firstViewController
[navController addChildViewController:secondViewController];
Stack: secondViewController, firstViewController
[navController addChildViewController:thirdViewController];
Stack: thirdViewController, secondViewController, firstViewController
You can see the pattern here. In other words, the fourthViewController is presented because it's on the top of the stack.
Second Problem
As for your second problem, you can't push a view controller onto the stack that already exists in the stack. [[self navigationController] pushViewController:self.navigationController.viewControllers[0] animated:NO]; seems absurd when you consider that fact. You're trying to push something from within the stack to the stack.
So I have a nav controller and I push a view on the screen, a settings view, but I want to remove it out of the hierarchy, so when the new viewcontroller comes onto screen and the back button is pressed it returns it to the main menu.
I tried this:
[self.navigationController popViewControllerAnimated:NO];
[self.navigationController pushViewController:tabBar animated:YES];
But the the viewController is just popped off and the new one is not presented.
You should try to modify the navigationcontrollers view controller array like the following way:
[self.navigationController pushViewController:tabBar animated:YES];
NSMutableArray *VCs = [NSMutableArray arrayWithArray: self.navigationController.viewControllers];
[VCs removeObjectAtIndex:[VCs count] - 2];
[self.navigationController setViewControllers: VCs];
Maybe the animation will be broken. If this happens you should move the part with the modification of the navigationcontroller VC array to the presented VC's viewDidAppear method.
The problem is that self is released as soon as you call popViewControllerAnimated:. The push call then sends a message to nil.
You could assign self.navigationController to a local variable first, and use that for pushing and popping instead.
Alternatively, and it's a bit of a hack, but before this, you could call [[self retain] autorelease] to give it an extra retain so that it won't disappear until the end of the run loop.
I'm using IOS5 Storyboard. My View Controller path is as follows:
tabbarVC --> navigationVC-1 --> tableVC-1 --(via segue push)-> tableVC-2 --(via segue modal)-> navigationVC-2 --> tableVC-3
In the cancel button callback action method in tableVC-3 I call [self dismissViewControllerAnimated:YES completion:nil]; that successfully gets me back to tableVC-2. However when I try to examine the navigation path backwards in the debugger, I don't see a way to access tableVC-2 from navigationVC-2. I expected navigationVC-2 to maintain a link to tableVC-2 or navigationVC-1 but it doesn't seem to. Please see my debugger output below.
Can someone explain the navigation hierarchy and how to traverse the chain backwards programatically?
(gdb) po self
<tableVC-3: 0x6d33340>
(gdb) po (UIViewController*) [self navigationController]
<UINavigationController: 0x6d33560>
(gdb) po (UIViewController*)[[self navigationController] navigationController]
Can't print the description of a NIL object.
(gdb) po (UIViewController*)[[self navigationController] topViewController]
<tableVC-3: 0x6d33340>
(gdb) po (UIViewController*)[[self navigationController] presentingViewController]
<UITabBarController: 0x6b2eba0>
(gdb) po (UIViewController*)[[self navigationController] presentedViewController]
Can't print the description of a NIL object.
(gdb) po (UIViewController*)[[self navigationController] visibleViewController]
<tableVC-3: 0x6d33340>
This is an old question, but just to help anyone else who comes across this issue, there's a single command which'll make your life easier..
[self.navigationController popToRootViewControllerAnimated:TRUE];
Easy when you stumble across the right command, isn't it !
So, supposing you had a series of three screens in a Navigation Controller, and on the third screen you wanted the "Back" button to take you back to the initial screen.
-(void)viewDidLoad
{
[super viewDidLoad];
// change the back button and add an event handler
self.navigationItem.leftBarButtonItem =
[[UIBarButtonItem alloc] initWithTitle:#"Back"
style:UIBarButtonItemStyleBordered
target:self
action:#selector(handleBack:)];
}
-(void)handleBack:(id)sender
{
NSLog(#"About to go back to the first screen..");
[self.navigationController popToRootViewControllerAnimated:TRUE];
}
After some research using this and a couple other questions for modal UIViewControllers in storyboard to go back two views I used
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
Going throw an old response tough of updating it to be more complet.
To address this question :
Can someone explain the navigation hierarchy and how to traverse the chain backwards programatically?
The structure of your navigation :
tabbarVC --> navigationVC-1 --> tableVC-1 --(via segue push)-> tableVC-2 --(via segue modal)-> navigationVC-2 --> tableVC-3
Can be explain like this :
The TabbarVC is showing it's 'selectedViewController' (navigationVC-1).
NavigationVC-1 has its navigation stack compose of TableVC-1 and TableVC-2 (topViewController of NavigagtionVC-1)
Then NavigationVC-2 is presented Modally over the tabbarVC, so tabbarVC is the presentingViewController and NavigationVC-2 is the presentedViewController
So in order to reach tableVC-2 from tableVC-3 you would need to do something like this :
[(UINavigationController *)[(UITabBarController *)[tableVC-3 presentingViewController] selectedViewController] topViewController];
(don't do that in production code)
[tableVC-3 presentingViewController] as well as [tableVC-3.navigationController presentingViewController] will give you back the UITabBarController.
If you are using a UINavigationController you should use it's push and pop method to put UIViewController on or off the "presentation stack".
You will be able to access the UINavigationController from those UIViewController like this:
self.navigationController
If you want to go back more than one UIViewController on the "presentation stack" you can use this method on the UINavigationController
popToViewController:animated:
Pops view controllers until the specified view controller is at the top of the navigation stack.
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
To dismiss a UIViewController that was presented modally, the UIViewController that have presented it need to dismiss it with :
- (void)dismissModalViewControllerAnimated:(BOOL)animated
So in this case it should be :
[tableVC-2 dismissModalViewControllerAnimated:YES];
Swift
If you are using a navigation controller the you can navigate back to the previous view controller with
self.navigationController?.popViewControllerAnimated(true)
or back to the root view controller with
self.navigationController?.popToRootViewControllerAnimated(true)
I'm obviously missing something...
In my iOS app, I push a UIViewController onto a navigation controller:
MyViewController *mvc = [[MyViewController alloc] initWithNibName:#"MyViewController"];
[self.navigationController mvc animated:YES];
MyViewController displays fine, and I can see and use the navigationBar, but when I try to get a pointer back to the navigation controller from within my view controller, I get a nil result.
UINavigationController *nav = [self navigationController];
if (!nav) {
NSLog(#"no nav");
}
I've been beating my head against this all day, but can't see that I'm doing anything wrong. I get no warnings or errors in Xcode. Am I completely missing something?
TIA: john
The navigationController won't be set properly on viewDidLoad. You have to check it in viewDidAppear or at some later stage. Which method are you calling to [self navigationController] in?
The reason for this is that when viewDidLoad is called, the UINavigationController is still processing the pushViewController:animated: method. It would appear to set the navigationController property after it initialises the controller's view. I can't recall whether the property is set by the time viewWillAppear runs, but it should definitely be set by viewDidAppear.
id rootViewController = [[[[[UIApplication sharedApplication] keyWindow] subviews] objectAtIndex:0] nextResponder];