Best practice for pushing a viewController in UINavigationController stack - ios

Sorry if this question was asked prior.
I wanted to know how do we handle navigationController stack like am looking for best practices
for now what i am doing is I check if a viewcontroller is already present and then in the navigation stack and then i pop it back by doing something like this
bool flag = NO;
for(LoginViewController *vc in self.navigationController.viewControllers)
{
if ([vc isKindOfClass:[LoginViewController class]]) {
flag = YES;
}
}
if (flag!=YES ){
LoginViewController *objVC = [[LoginViewController alloc]init];
[self.navigationController pushViewController:objVC animated:YES];
objVC = nil;
}else
{
[self.navigationController popViewControllerAnimated:YES];
}
Do i really need to check if any viewcontroller is already present in the navigation stack prior pushing it or does the navigation controller takes care of it and at sometime removes the viewcontroller which are not in use. Is there a better way to do this or am i already doing the right thing
In short how we can stop a viewcontroller from being pushed into the navigation stack twice.

popViewControllerAnimated will always pop the top most VC on the stack. It operates in LIFO fashion.
LoginViewController may not necessarily be the top most element on the stack hence in order to remove it you have to modify code this way.
LoginViewController *loginVC = nil;
NSUInteger index = [self.navigationController.viewControllers indexOfObjectPassingTest:^BOOL (id obj, NSUInteger idx, BOOL *stop) {
return [obj isKindOfClass:[LoginViewController class]];
}];
if (index != NSNotFound) {
//Found VC on stack
//Get LoginViewController instance
loginVC = [myArray objectAtIndex:index];
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
//Remove LoginViewController obj from stack
[viewControllers removeObjectIdenticalTo:loginVC];
//Reassign new array which does not contain LoginViewController
navigationController.viewControllers = viewControllers;
}
else {
//VC not found on stack
LoginViewController *objVC = [[LoginViewController alloc]init];
[self.navigationController pushViewController:objVC animated:YES];
}
Hope that helps!

Related

iOS Objective-C: How to identify the view controller is presenting thru UINavigationController?

I have multiple viewControllers on my implementation for example:
ViewControllerA
ViewControllerB
ViewControllerC
ViewControllerD
But the deeplinks I need to load them in ViewControllerC but I don't know if that viewcontroller is been load it (initialized) yet or if is present.
I have tried this from appDeelegate:
ViewControllerC *rootViewController = [[ViewControllerC alloc] init];
self.navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];
But it seems like is creating a new instance of the viewController.
My question to you guys, how can I grab the instance ViewControllerC load it in the app or how can I detect if ViewControllerC is not load it yet?
I'll really appreciate your help or work around.
As you pointed out, allocating a view controller in order to determine if it is presented makes no sense. Will your app always have a navigation controller at its root? If so, you can get it this way...
// in the app delegate
AppDelegate *appDelegate = self;
// or, if not in the app delegate
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
// either way
UINavigationController *navController = (UINavigationController *)[[appDelegate window] rootViewController];
Notice the potentially reckless cast of the root vc as a UINavigationController. That's reckless only if some other sort of VC can sometimes be at the root. If that's the situation in your app, then you need to test...
UIViewController *vc = [[appDelegate window] rootViewController];
if ([vc isKindOfClass:[UINavigationController self]]) {
UINavigationController *navController = (UINavigationController *)vc;
// carry on from here
} else {
// decide what your "deep link" function does when the wrong root vc is present. maybe start over?
}
Finally, and I think the problem you're getting at, how do we determine if a ViewControllerC is present, and how do we present it if not? The first part is easy because navigation controllers have a viewControllers property. That's an array representing the "stack", where the first item is the root and the last item is on top. So...
NSInteger index = NSNotFound;
for (UIViewController *vc in navController.viewControllers) {
if ([vc isKindOfClass:[UIViewController self]]) {
index = [navController.viewControllers indexOfObject:vc];
}
}
if (index != NSNotFound) {
// it's on the stack
}
Here's the way to ask if it's at the top of the stack...
[navController.viewControllers.lastObject isKindOfClass:[ViewControllerC self]]
What to do if its not on the stack is up to you. One idea is to just push one. Do that the way you do it in your app already. What if it is on the stack, but not on top? If you wanted animation to get there, you'd pop to it (animating the last pop). Since this is a deep link, you probably don't care about the animation. Just truncate the nav controllers view controller list...
if (index != NSNotFound) {
// it's on the stack
navController.viewControllers = [navController.viewControllers subarrayWithRange:NSMakeRange(0, index+1)];
}
For check if root view controller is a ViewControllerC
Swift:
if type(of: UIApplication.shared.keyWindow?.rootViewController) == ViewControllerC.self{
debugPrint("RootViewController is a ViewControllerC")
}
Objective-C:
if ([[[[UIApplication sharedApplication] keyWindow] rootViewController] class] == [ViewControllerC class]){
NSLog(#"RootViewController is a ViewControllerC");
}
For check if ViewControllerC is presented on root view controller
Swift:
if let rootViewController = UIApplication.shared.keyWindow?.rootViewController{
if type(of: rootViewController.presentedViewController) == ViewControllerC.self{
debugPrint("ViewControllerC is presented on rootViewController")
}
}
Objective-C:
UIViewController *viewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
if (viewController != nil){
if ([viewController.presentedViewController class] == [ViewControllerC class]){
NSLog(#"ViewControllerC is presented on rootViewController");
}
}
Set root view controller as ViewControllerC
Swift:
if UIApplication.shared.keyWindow != nil{
let viewController:ViewControllerC = ViewControllerC()
//You can get above instance from Storyboard if you wanna
UIApplication.shared.keyWindow!.rootViewController = viewController
}
Objective-C:
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
if (window != nil){
ViewControllerC *viewController = [[ViewControllerC alloc] init];
//You can get above instance from Storyboard if you wanna
window.rootViewController = viewController;
}
For push view controller on navigation controller from root if exists
Swift:
if UIApplication.shared.keyWindow != nil{
if let navigationController = UIApplication.shared.keyWindow!.rootViewController as? UINavigationController{
let viewController:ViewControllerC = ViewControllerC()
//You can get above instance from Storyboard if you wanna
navigationController.pushViewController(viewController, animated: true)
}
}
Objective-C:
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
if (window != nil){
UINavigationController *navigationController = (UINavigationController*)window.rootViewController;
if (navigationController != nil){
ViewControllerC *viewController = [[ViewControllerC alloc] init];
[navigationController pushViewController:viewController animated:true];
}
}
Now you can do a lot of stuff, for example get the instance of ViewControllerC from navigation controller if exists
Objective-C:
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
if (window != nil){
UINavigationController *navigationController = (UINavigationController*)window.rootViewController;
if (navigationController != nil){
UIViewController *viewController = [navigationController topViewController];
if ([viewController class] == [ViewControllerC class]){
NSLog(#"Do what you want with viewControllerC instance");
}
}
}

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 ViewController in NavigationController with special key

I have a NavigationController with different ViewControllers. Sometimes I want to push a ViewController to NavigationController, which is already included.
In this case I want to move to the existing from TopController, and pop all other ViewControllers between top and the existing one.
Is there a way to give a ViewController a special id (for example a NSString), to find him later in "NavigationController.viewControllers"?
Or should I use a seperate Dictionary to manage my ViewControllers?
Or is there a better way, I dont consider.
best regards
On your appDelegate, you can store all your ViewController and when you want to pop call :
[self.navigationController popToViewController:yourViewController animated:YES];
Or you can try to search
NSArray *viewControllers = self.navigationController.viewControllers
for (UIVIewController *anVC in viewControllers) {
if (anVC isKindOfClass:[yourController class] {
[self.navigationController popToViewController:anVC animated:YES];
break;
}
}
Hope it will help you.
Note : As the view controllers are the same class, most times
Add strUniqueID property on each controller while pushing view controller to navigation controller don't forget to set it
Now you know which unique ID's you want remove, so find it
//Firstly find your viewController
for(id viewcontroller in self.navigationController.viewControllers)
{
//For finding specific viewController use isKindOfClass
if(viewcontroller isKindOfClass:[YourViewControllerNameHere Class])
{
//Now find UniqueIDHere
YourViewController *objYourViewController = (YourViewController *)viewcontroller
if(YourViewControllerNameHere.strUniqueID isEqualToString:removeUniqueIDHere])
{
//Now pop to YourViewController
[self.navigationController popToViewController:viewcontroller animated:YES];
break;
}
}
else if(viewcontroller isKindOfClass:[YourDifferentViewControllerNameHere Class]) //Different ViewControllers here like this
{
//Now find UniqueIDHere
YourDifferentViewController *objYourDifferentViewController = (YourViewController *)viewcontroller
if(YourDifferentViewController.strUniqueID isEqualToString:removeUniqueIDHere])
{
//Now pop to YourViewController
[self.navigationController popToViewController:viewcontroller animated:YES];
break;
}
}
}
Try this :
//Firstly find your viewController
for(id viewcontroller in self.navigationController.viewControllers)
{
//For finding specific viewController use isKindOfClass
if(viewcontroller isKindOfClass:[YourViewControllerNameHere Class])
{
//Now pop to YourViewController
[self.navigationController popToViewController:viewcontroller animated:YES];
break;
}
}
Swift 5
let vc = navVC.viewControllers.first(where: { $0.hasKey })

back button doesn't go back

After pushing a ViewController using this code
UIViewController *vc = [[self storyboard] instantiateViewControllerWithIdentifier:#"frontCardViewController"];
[self.navigationController pushViewController:vc animated:YES];
I remove all ViewControllers I don't need anymore using this code
NSMutableArray *navigationArray = [[NSMutableArray alloc] initWithArray:self.navigationController.viewControllers];
NSArray *array = [[NSArray alloc] initWithArray:navigationArray];
for (UIViewController *viewController in array) {
if ([viewController isKindOfClass:[RITCardViewController class]]) {
RSLog(#"Is kind of Class RITCardViewController");
[navigationArray removeObject:viewController];
}
}
self.navigationController.viewControllers = navigationArray;
The navigation array now looks like this:
"RITMainViewController: 0x10d81fc1>",
"RITDetailViewController: 0x10d847880",
"RITTestResultViewController: 0x113d0e090"
But the problem is that if the back button in the navigation bar is pressed now, it goes back to the second screen. But when the back button is pressed again it just stays on this screen. It seems to go trough all the screens I have removed, but doesn't show them.
What am I doing wrong?
Try something like this instead:
NSMutableArray *viewControllers = [#[] mutableCopy];
for (UIViewController *vc in self.nagivationController.viewControllers)
{
if (NO == [vc isKindOfClass:[RITCardViewController class]])
[viewControllers addObject:vc];
}
self.navigationController.viewControllers = viewControllers;
Make sure you don't corrupt the stack by removing the view controller you are currently on from the view controllers array. Assuming your current view controller is not an instance of RITCardViewController you should be fine. Otherwise, you'll have to make up for it in your code.

Removing ViewController from navigation stack

I do it with code:
NSArray *viewControllersFromStack = [self.navigationController viewControllers];
NSMutableArray *viewControllersFromStackMutable = [NSMutableArray arrayWithArray:viewControllersFromStack];
NSMutableArray *viewControllersToRemove = [[NSMutableArray alloc]init];
for (UIViewController *currentVC in viewControllersFromStack)
{
if ([currentVC isKindOfClass:[TalksViewController class]])
{
[viewControllersToRemove addObject:currentVC];
if (viewControllersToRemove.count == 2)
{
UIViewController *oneVCtoRemove = [viewControllersToRemove objectAtIndex:0];
[viewControllersFromStackMutable removeObject:oneVCtoRemove];
[self.navigationController setViewControllers:viewControllersFromStackMutable];
}
}
}
Problem is that I have reference to removed VC's in navigation Item. How to fix it?
When you want to remove a view from the navigation stack you can simply just call this method on the navigation bar to pop the view from the stack:
[self.navigationController popViewControllerAnimated:YES];
To pop an external view use
for(UIViewController *currentVC in viewControllersFromStack)
{
if([currentVC isKindOfClass:[TalksViewController class]])
{
[currentVC.navigationController popViewControllerAnimated:YES];
}
}
The above answer is correct.
I have 'A' as rootview controller. 'B to F' are other view controllers. From 'F', if I wanted to go directly to 'A', it is as under.
[self.navigationController popToRootViewControllerAnimated:YES];
BUT if I wanted to jump to 'B' then the code in answer is helpful. I only changed the array of view controllers to run reverse with 'reverseObjectEnumerator' and Animated to NO with 'popViewControllerAnimated:NO'. the Code is as under
NSArray *viewControllersFromStack = [self.navigationController viewControllers];
for(UIViewController *currentVC in [viewControllersFromStack reverseObjectEnumerator])
{
if(![currentVC isKindOfClass:[A class]] && ![currentVC isKindOfClass:[B class]])
{
[currentVC.navigationController popViewControllerAnimated:NO];
}
}

Resources