Can not pass data when adding navigation controller to view - ios

I have a TabBarController with three tabs. The views for all tabs were embedded in their own navigation controller, except one, the Map view.
To navigate from one of the other views to the Map view and pass data I used:
- (IBAction)mapButton:(id)sender {
MapViewController *destView = (MapViewController *)[self.tabBarController.viewControllers objectAtIndex:0];
if ([destView isKindOfClass:[MapViewController class]])
{
MapViewController *destinationViewController = (MapViewController *)destView;
destinationViewController.selectedTruck = _truck;
}
[self.tabBarController setSelectedIndex:0];
}
And it was working. Now I need to embed the Map view in a navigation controller as well, to add a detail view, but when I do no data is passed, it only goes to the Map view.
Can anyone see what am I missing?

[self.tabBarController.viewControllers objectAtIndex:0] is no longer an instance of MapViewController. It is a UINavigationController with a MapViewController as its root view controller.
You could access the MapViewController via the UINavigationController like so but all these type casting assumptions are messy -
- (IBAction)mapButton:(id)sender {
UINavigationController *navigationController = (UINavigationController *)[self.tabBarController.viewControllers firstObject];
if ([navigationController isKindOfClass:[UINavigationController class]])
{
MapViewController *rootViewController = (MapViewController *)[navigationController.viewControllers firstObject];
if ([rootViewController isKindOfClass:[MapViewController class]])
{
MapViewController *destinationViewController = (MapViewController *)rootViewController;
destinationViewController.selectedTruck = _truck;
}
}
[self.tabBarController setSelectedIndex:0];
}
Instead, a better design would be to keep a reference to the MapViewController (as a property) when you set up the tab bar. That way you would simply call self.destinationViewController.selectedTruck = _truck; instead.

Related

UISplitViewController with nav stacks in both master and detail - how to remove a VC from one of the stacks?

I am debugging legacy code which is always fun. The old code tried to mock the splitView delegate methods, causing all sorts of issues - mainly crashing: on a Plus device in Portrait, rotating to landscape caused the crash - if there was no detail view set, old code attempted to create one in a dodgy hack and it was just useless...
My app is UISplitViewController based, where I have a navigation stack in both master and detail sides of the splitView.
By reading though SO and using this example and was able to implement UISplitViewController delegate methods and everything is working correctly in regards to rotation, and showing the correct master/detail views when appropriate. Here is my implementation: (apologies for wall of code snippets)
- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
if ([secondaryViewController isKindOfClass:[UINavigationController class]]
&& [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[AECourseHTMLTableViewController class]]
&& ([(AECourseHTMLTableViewController *)[(UINavigationController *)secondaryViewController topViewController] htmlContentEntry] == nil)) {
// If the detail controller doesn't have an item, display the primary view controller instead
return YES;
}
return NO;
}
And the other splitView delegate method - see comments in code for where I'm stuck.
- (UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController {
// If detail view already exists
if ([primaryViewController isKindOfClass:[UINavigationController class]]) {
for (UIViewController *controller in [(UINavigationController *)primaryViewController viewControllers]) {
if ([controller isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)controller visibleViewController] isKindOfClass:[AECourseHTMLTableViewController class]]) {
return controller;
}
}
}
// Create detail view
UINavigationController *navController = [self.storyboard instantiateViewControllerWithIdentifier:#"CourseHTMLNav"];
if ([navController.viewControllers.firstObject isKindOfClass:[AECourseHTMLTableViewController class]]) {
AECourseHTMLTableViewController *courseViewController = navController.viewControllers.firstObject;
[self configureViewController:courseViewController entry:self.contentSection.sections[0] indexPath:courseViewController.currentIndexPath];
}
// Enable back button
UIViewController *controller = [navController visibleViewController];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
if (!self.splitViewController.isCollapsed) {
UINavigationController *navController = self.splitViewController.viewControllers.firstObject;
AEContentMenuTableViewController *contentMenuVC = navController.viewControllers.firstObject; // This controller needs to be master in Landscape
NSMutableArray<UIViewController *> *controllers = [navController.viewControllers mutableCopy]; // Contains 3 controllers, first needs removed
NSMutableArray *toDelete = [NSMutableArray new];
for (UIViewController *viewController in controllers)
if ([viewController isKindOfClass:[contentMenuVC class]] || [viewController isKindOfClass:[AECourseHTMLTableViewController class]]) {
[toDelete addObject:viewController]; // Remove first VC, so master should become AEContentMenuVC?
break;
}
// Remove the object
[controllers removeObjectsInArray:toDelete];
// Set viewControllers
navController.viewControllers = controllers;
}
return navController;
}
AECourseHTMLTableViewController has next/prev buttons to select the next row in the tableview of the tableview menu class class (AEContentMenuTableViewController). I have a delegate function which can tell me the current indexPath in which AECourseHTML... is using from AEContentMenu..., and when calling it, it selects the menu tableview row and instantiates a new AECourseHTML... and pushes it.
This is where I'm stuck. In Portrait, pressing next/prev is fine, it selects the correct row and works as expected. But once I rotate the device, both master and detail views show the detail view. I can press "Back" on the master view, and it takes me to the correct AEContentMenu... class. As noted in the code snippet comments, I need to remove a ViewController from the master stack (the first object actually), and AEContentMenu... should become the first object of that stack - so when rotating, that should be the master view.
Apologies for such a long post, I've been banging my head with this for weeks now and I want to include as much info as possible in this question. Thanks in advance.
I found a solution which works well for my use cases. It may not be the cleanest code, but I'm happy with what I've got.
splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:
remains unchanged. I have updated my splitViewController:separateSecondaryViewControllerFromPrimaryViewController: delegate method with the solution. Any feedback is welcome.
- (UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController {
// If detail view already exists
if ([primaryViewController isKindOfClass:[UINavigationController class]]) {
for (UIViewController *controller in [(UINavigationController *)primaryViewController viewControllers]) {
if ([controller isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)controller visibleViewController] isKindOfClass:[AECourseHTMLTableViewController class]]) {
return controller;
}
}
}
// Return CourseVC
UINavigationController *navController = splitViewController.viewControllers.firstObject;
UIViewController *viewController;
for (viewController in navController.viewControllers) {
if ([navController.viewControllers.lastObject isKindOfClass:[AECourseHTMLTableViewController class]]) {
return viewController;
} else {
// Create detail view
UINavigationController *navController = [self.storyboard instantiateViewControllerWithIdentifier:#"CourseHTMLNav"];
if ([navController.viewControllers.firstObject isKindOfClass:[AECourseHTMLTableViewController class]]) {
// Enable back button
UIViewController *controller = [navController visibleViewController];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
AECourseHTMLTableViewController *courseViewController = navController.viewControllers.firstObject;
// If next/prev has been tapped, configure current ContentHTML
if (self.currentContentHTML) {
[self configureViewController:courseViewController entry:self.currentContentHTML indexPath:courseViewController.currentIndexPath];
} else {
// Create new ContentHTML from first row of AEContentMenuVC
[self configureViewController:courseViewController entry:self.contentSection.sections[0] indexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
}
return navController;
}
}
}
return navController;
}
Your top if statement should return nil. Since you were returning the nested navigation controller you were missing out on the default behaviour of popping the master navigation's top controller which is required so it can then be placed on the right.
The default behaviour will find that nested nav controller and pop it. However the reason you still need to search for it yourself is if it isn't there then you need to load the detail nav from the storyboard as you have done.

Find the current top view controller and push view controller

I have a UICollectionView like this:
On each cell, I add a button. When I click that button, it will push to an other view controller. It works fine. But when I try to click 2 button at the same time(eg: Cell 1 and Cell 7). It call "push" twice. And I receive warning:
nested push animation can result in corrupted navigation bar.
Finishing up a navigation transition in an unexpected state. Navigation Bar
subview tree might get corrupted.
Here is my code:
AppDelegate.m
+ (AppDelegate *)shareInstance{
return (AppDelegate *)[[UIApplication sharedApplication] delegate];
}
- (UIViewController *)currentVisibleController{
id rootController = self.window.rootViewController;
if ([rootController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navigationController = (UINavigationController *)rootController;
return navigationController.topViewController;
}
if ([rootController isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabbarController = (UITabBarController *)rootController;
id topViewController = [tabbarController.viewControllers objectAtIndex:tabbarController.selectedIndex];
if ([topViewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navi = (UINavigationController *)topViewController;
return navi.topViewController;
}
return topViewController;
}
return self.window.rootViewController;
}
When I press on a cell:
CustomCell.m
- (IBAction)pressOnCell:(id)sender {
SecondViewController *secondViewController = [[SecondViewController alloc] initWithNibName:NSStringFromClass([SecondViewController class]) bundle:nil];
[[AppDelegate shareInstance].currentVisibleController.navigationController pushViewController:secondViewController animated:YES];
}
You can set animated option in pushViewController to NO. Or you can disable user interaction on collection view on viewWillDisAppear and then enable it in viewWillAppear. Or you can call pressOnCell in didSelectItemAtIndexPath and set allowsMultipleSelection property of collection view to NO.

Passing data to UIViewController when using modal

I have two view controllers, MainViewController and SecondViewController.
In my main view controller, I have data I pass to the second view controller like so:
-(IBAction)shareToSocial:(id)sender {
SecondViewController *view2 = (SecondViewsController *)[storyboard instantiateViewControllerWithIdentifier:#"PushToNext"];
view2.secTitle = self.MainTitle;
[self.navigationController setModalPresentationStyle: UIModalPresentationCurrentContext];
[self setModalPresentationStyle:UIModalPresentationCurrentContext];
}
Where both secTitle and MainTitle are NSStrings for each controller. From what I've read, by setting view2.secTitle to self.MainTitle, when SecondViewController pops in I my view2.secTitle will have the same value as MainTitle. So if MainTitle is "Hello World" then I open secondViewController, secTitle would also be "Hello World". However, I've been getting null with secTitle even though I am setting the data for it.
If I use pushViewController instead of modal, the values are passed between the two controllers fine. So I'm not sure if I'm doing anything wrong here exactly.
I've also tried using [view2 setSecTitle:MainTitle] to no avail.
There's not much code on my SecondViewController but here's how it looks:
-(IBAction)showMessage:id(sender){
NSLog(#"test: %#", self.secTitle);
}
-(IBAction)closeMessage:id(sender){
[self dismissViewControllerAnimated:YES completion:nil];
}
Also, I've noticed that when I used dismissViewControllerAnimated on SecondViewController to close it and go back to the MainViewController, a black empty screen shows for a few seconds before showing the Main View and touch events on MainViewController is not detected anymore.
Please help.
Could you use performSegueWithIdentifier and prepareForSegue? If you've got your views in your storyboard and set up a segue between them, given it an identifier and set the style to modal you should be able to do this:
-(IBAction)shareToSocial:(id)sender {
[self performSegueWithIdentifier:#"modelToNextSegue" sender:self];
}
Then in prepareForSegue method you pass your values:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
SecondViewController *view2 = [segue destinationViewController];
view2.secTitle = self.MainTitle;
}
Try this:
SecondViewController *view2 = (SecondViewsController *)[storyboard instantiateViewControllerWithIdentifier:#"PushToNext"];
view2.secTitle = self.MainTitle;
UINavigationController *nav = [[UINavigationController alloc]initWithRootViewController:view2];
[nav setModalPresentationStyle: UIModalPresentationCurrentContext];
[self setModalPresentationStyle:UIModalPresentationCurrentContext];

Pop to root SplitViewController in TabBarController - iOS

TableViewController Context:
Here's my app:
Tab 1: NavigationController -> ViewController
Tab 2: SplitViewController -> NavigationController -> Master : TableViewController ->TableViewController
-> NavigationController -> Detail : TableViewController -> TableViewController
Tab 3: NavigationController -> ViewController (I'm Here)
How do I pop to the root of each tab from a method in the tab 3 (basically a logout button)?
// Based on UIviewController tag you can pop to navigate
// try this..
MyClass *obj = [self.navigationController.viewControllers objectAtIndex:0];
[self.navigationController popToViewController:obj animated:YES];
I managed to accomplish what I was trying to do, here's the code for others looking for the answer :
- (void) logout{
for(UIViewController *viewController in tabBarController.viewControllers)
{
if([viewController isKindOfClass:[UINavigationController class]]){
[(UINavigationController*)viewController popToRootViewControllerAnimated:NO];
}
else if([viewController isKindOfClass:[UISplitViewController class]]){
UISplitViewController *splitView = (UISplitViewController *)viewController;
for (UIViewController *navControllerInSplit in splitView.viewControllers) {
if([navControllerInSplit isKindOfClass:[UINavigationController class]]){
[(UINavigationController*)navControllerInSplit popToRootViewControllerAnimated:NO];
}
}
}
}
}
Add following code for pop to rootview for UINavigationController where UITabbarController
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
if ([self.tabBarController.selectedViewController isKindOfClass:[UINavigationController class]])
{
[(UINavigationController*)self.tabBarController.selectedViewController popToRootViewControllerAnimated:YES];
}
}
Reference: View Controller Catalog for iOS
A split view controller must always be the root of any interface you
create.
In other words, you must always install the view from a UISplitViewController object as the root view of your application’s window.
The panes of your split view interface may then contain navigation
controllers, tab bar controllers, or any other type of view controller
you need to implement your interface.
Split view controllers cannot be presented modally.
So, what you are attempting is incorrect, and in fact your app may get rejected by Apple.

Access UIViewController inside ContainerController

I have the following setup:
SplitViewController as rootController.
Detail part is first
ViewController with View > ContainerView (later, View will have ImageView, but that is not the issue here).
The ContainerView has segue (embed) to another view controller (NavigationController).
This is graphically represented in IB as:
Now the thing is I want to access the NavigationController from rootController (eg SplitViewController). I was unable to navigate down the hierarchy of "subViews" and so.
Is there some convenient way to get hold of the NavigationController?
Without the ViewController (together with ContainerView), I was able to access it like:
UISplitViewController *splitViewController = (UISplitViewController *) self.window.rootViewController;
UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
// now i have the controller, i can delegate to it or use it in any other way:
splitViewController.delegate = (id) navigationController.topViewController;
When you add the NavigationController, you could pass it upwards (delegate methods) to your subclass of UISplitViewController and store a reference there.
You could add a #property (MySplitViewController *) delegate; your MyContentView and set it to the splitviewcontroller. When the segue is fired, you could then do the following:
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"ShowNavigationController"])
{
UINavigationController *controller = segue.destinationViewController;
[self.delegate setNavigationController:controller];
}
}
Edit: If you want to stick to your code, you could do something like this:
UISplitViewController *splitViewController = (UISplitViewController *) self.window.rootViewController;
UIViewController *container = [splitViewController.viewControllers lastObject];
UINavigationController *navigationController = [container.childViewControllers lastObject];
splitViewController.delegate = (id) navigationController.topViewController;
In that case you should really include some error handling.

Resources