71

I have a small iPhone app, which uses a navigation controller to display 3 views (here fullscreen):

Xcode screenshot

First it displays a list of social networks (Facebook, Google+, etc.):

list screenshot

Then it displays an OAuth dialog asking for credentials:

login screenshot

And (after that, in same UIWebView) for permissions:

permissions screenshot

Finally it displays the last view controller with user details (in the real app this will be the menu, where the multiplayer game can be started):

details screenshot

This all works well, but I have a problem, when the user wants to go back and select another social network:

The user touches the back button and instead of being displayed the first view, is displayed the second one, asking for OAuth credentials/permissions again.

What can I do here? Xcode 5.0.2 shows a very limited choice for segues - push, modal (which I can't use, because it obscures navigation bar needed for my game) and custom.

I am an iOS programming newbie, but earlier I have developed an Adobe AIR mobile app and there it was possible to 1) replace view instead of pushing and 2) remove an unneeded view from navigation stack.

How to do the same in a native app please?

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416

14 Answers14

59

To expand on the various segues above, this is my solution. It has the following advantages:

  • Can work anywhere in the view stack, not just the top view (not sure if this is realistically ever needed or even technically possible to trigger, but hey it's in there).
  • It doesn't cause a pop OR transition to the previous view controller before displaying the replacement, it just displays the new controller with a natural transition, with the back navigation being to the same back navigation of the source controller.

Segue Code:

- (void)perform {
    // Grab Variables for readability
    UIViewController *sourceViewController = (UIViewController*)[self sourceViewController];
    UIViewController *destinationController = (UIViewController*)[self destinationViewController];
    UINavigationController *navigationController = sourceViewController.navigationController;

    // Get a changeable copy of the stack
    NSMutableArray *controllerStack = [NSMutableArray arrayWithArray:navigationController.viewControllers];
    // Replace the source controller with the destination controller, wherever the source may be
    [controllerStack replaceObjectAtIndex:[controllerStack indexOfObject:sourceViewController] withObject:destinationController];

    // Assign the updated stack with animation
    [navigationController setViewControllers:controllerStack animated:YES];
}
ima747
  • 4,667
  • 3
  • 36
  • 46
  • Nice! How about a pop transition (or slide all to right effect) if you are doing a "previous" instead of a "next" scenario? – finneycanhelp May 05 '15 at 13:05
  • Not sure I really know what you're asking, but you can modify the stack to insert/replace the previous view with whatever you like and then simply dismiss the top view to trigger the normal transition... – ima747 May 05 '15 at 16:49
39

You could use a custom segue: to do it you need to create a class subclassing UIStoryboardSegue (example MyCustomSegue), and then you can override the "perform" with something like this

-(void)perform {
    UIViewController *sourceViewController = (UIViewController*)[self sourceViewController];
    UIViewController *destinationController = (UIViewController*)[self destinationViewController];
    UINavigationController *navigationController = sourceViewController.navigationController;
    // Pop to root view controller (not animated) before pushing
    [navigationController popToRootViewControllerAnimated:NO];
    [navigationController pushViewController:destinationController animated:YES];    
}

At this point go to Interface Builder, select "custom" segue, and put the name of your class (example MyCustomSegue)

In swift

override func perform() {
    if let navigationController = self.source.navigationController {
        navigationController.setViewControllers([self.destination], animated: true)
    }
}
Ratul Sharker
  • 7,484
  • 4
  • 35
  • 44
Daniele
  • 2,461
  • 21
  • 16
  • Thanks, but are you sure it (**replacing** the 2nd scene by the 3rd scene - instead of pushing - so that touching *Back* button returns user to the 1st scene) is possible by using a custom segue? I'm reading https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/Reference/Reference.html and just can't figure out, how to do it. – Alexander Farber Jan 29 '14 at 19:14
  • 1
    In your first suggestion you didn't save `sourceViewController.navigationController` to a variable and it turned to `nil` before last command in `perform` - and that's why it didn't work for me. But the current version works perfectly, thank you! The answers by Lauro and Bhavya are very insightful too. – Alexander Farber Jan 30 '14 at 17:05
  • @Daniele. Wherefrom did you get reference to destinationViewController ? – zulkarnain shah Oct 25 '17 at 12:00
  • While it works it looks odd as the animation to the destination is animated from rootViewController so the story flow is broken. – Teddy Jan 07 '18 at 08:32
27

The custom segue didn't work for me, as I had a Splash view controller and I wanted to replace it. Since there was just one view controller in the list, the popToRootViewController still left the Splash on the stack. I used the following code to replace the single controller

-(void)perform {
    UIViewController *sourceViewController = (UIViewController*)[self sourceViewController];
    UIViewController *destinationController = (UIViewController*)[self destinationViewController];
    UINavigationController *navigationController = sourceViewController.navigationController;
    [navigationController setViewControllers:@[destinationController] animated:YES];
}

and now in Swift 4:

class ReplaceSegue: UIStoryboardSegue {

    override func perform() {
        source.navigationController?.setViewControllers([destination], animated: true)
    }
}

and now in Swift 2.0

class ReplaceSegue: UIStoryboardSegue {

    override func perform() {
        sourceViewController.navigationController?.setViewControllers([destinationViewController], animated: true)
    }
}
christophercotton
  • 5,829
  • 2
  • 34
  • 49
  • I have a hamburger menu as a navigationViewController. Your code works but it replaces the tableView that I use in my menu instead of the viewController (root). – Paul Bénéteau Apr 11 '17 at 09:10
  • Interesting @greenpoisononeTV What controller are you calling "performSegue" on? Since the sourceViewController and destination controllers are set with the segue. – christophercotton Apr 12 '17 at 15:42
  • I created a segue with ctrl+drag from one of my `tableViewCell` to a `viewController`. The segue is set as custom with the `ReplaceSegue` class. – Paul Bénéteau Apr 12 '17 at 16:28
21

For this problem, I think the answer is just simple as

  1. Get the array of view controllers from NavigationController
  2. Removing the last ViewController (current view controller)
  3. Insert a new one at last
  4. Then set the array of ViewControllers back to the navigationController as bellow:

     if let navController = self.navigationController {
        let newVC = DestinationViewController(nibName: "DestinationViewController", bundle: nil)
    
        var stack = navController.viewControllers
        stack.remove(at: stack.count - 1)       // remove current VC
        stack.insert(newVC, at: stack.count) // add the new one
        navController.setViewControllers(stack, animated: true) // boom!
     }
    

works perfectly with Swift 3.

Hope it helps for some new guys.

Cheers.

Dani Pralea
  • 4,545
  • 2
  • 31
  • 49
18

the swift 2 version of ima747 answer:

override func perform() {
    let navigationController: UINavigationController = sourceViewController.navigationController!;

    var controllerStack = navigationController.viewControllers;
    let index = controllerStack.indexOf(sourceViewController);
    controllerStack[index!] = destinationViewController

    navigationController.setViewControllers(controllerStack, animated: true);
}

As he mentioned it has the following advantages:

  • Can work anywhere in the view stack, not just the top view (not sure if this is realistically ever needed or even technically possible to trigger, but hey it's in there).
  • It doesn't cause a pop OR transition to the previous view controller before displaying the replacement, it just displays the new controller with a natural transition, with the back navigation being to the same back navigation of the source controller.
Dr.Agos
  • 2,229
  • 2
  • 15
  • 17
9

Using unwind segue would be the most appropriate solution to this problem. I agree with Lauro.

Here is a brief explanation to setup an unwind segue from detailsViewController[or viewController3] to myAuthViewController[or viewController1]

This is essentially how you would go about performing an unwind segue through the code.

  • Implement an IBAction method in the viewController you want to unwind to(in this case viewController1). The method name can be anything so long that it takes one argument of the type UIStoryboardSegue.

    @IBAction func unwindToMyAuth(segue: UIStoryboardSegue) {
    println("segue with ID: %@", segue.Identifier)
    }
    
  • Link this method in the viewController(3) you want to unwind from. To link, right click(double finger tap) on the exit icon at the top of the viewController, at this point 'unwindToMyAuth' method will show in the pop up box. Control click from this method to the first icon, the viewController icon(also present at the top of the viewController, in the same row as the exit icon). Select the 'manual' option that pops up.

  • In the Document outline, for the same view(viewController3), select the unwind segue you just created. Go to the attributed inspector and assign a unique identifier for this unwind segue. We now have a generic unwind segue ready to be used.

  • Now, the unwind segue can be performed just like any other segue from the code.

    performSegueWithIdentifier("unwind.to.myauth", sender: nil)
    

This approach, will take you from viewController3 to viewController1 without the need to remove viewController2 from the navigation hierarchy.

Unlike other segues, unwind segues do not instantiate a view controller, they only go to an existing view controller in the navigation hierarchy.

Lester
  • 731
  • 11
  • 12
5

As mentioned in previous answers to pop not animated and then to push animated won't look very good because user will see the actual process. I recommend you first push animated and then remove the previous vc. Like so:

extension UINavigationController {
    func replaceCurrentViewController(with viewController: UIViewController, animated: Bool) {
        pushViewController(viewController, animated: animated)
        let indexToRemove = viewControllers.count - 2
        if indexToRemove >= 0 {
            viewControllers.remove(at: indexToRemove)
        }
    }
}
Alex Shubin
  • 3,549
  • 1
  • 27
  • 32
5

Here a quick Swift 4/5 solution by creating a custom seque that replaces the (top stack) viewcontroller with the new one (without animation) :

class SegueNavigationReplaceTop: UIStoryboardSegue {

    override func perform () {
        guard let navigationController = source.navigationController else { return }
        navigationController.popViewController(animated: false)
        navigationController.pushViewController(destination, animated: false)
    }
}
HixField
  • 3,538
  • 1
  • 28
  • 54
2

Use below code last view controller You can use other button or put it your own instead of cancel button i have used

- (void)viewDidLoad
{
 [super viewDidLoad];

 [self.navigationController setNavigationBarHidden:YES];

 UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(dismiss:)];

self.navigationItemSetting.leftBarButtonItem = cancelButton;

}

- (IBAction)dismissSettings:(id)sender
{

// your logout code for social media selected
[self.navigationController popToRootViewControllerAnimated:YES];

}
bhavya kothari
  • 7,484
  • 4
  • 27
  • 53
  • You probably mean `setNavigationBarHidden:NO`? Your code works well, thank you and it is a good spot for the social network logout code as well. – Alexander Farber Jan 31 '14 at 13:56
2

What you should really do is modally present a UINavigationController containing the social network UIViewControllers overtop of your Menu UIViewController (which can be embedded in a UINavigationController if you want). Then, once a user has authenticated, you dismiss the social network UINavigationController, showing your Menu UIViewController again.

Mark
  • 7,167
  • 4
  • 44
  • 68
2

In swift3 create one segue -add identifier -add and set in segue(storyboard) custom storyboard class from cocoatouch file -In custom class override perform()

override func perform() {
    let sourceViewController = self.source
    let destinationController = self.destination
    let navigationController = sourceViewController.navigationController
    // Pop to root view controller (not animated) before pushing
    if self.identifier == "your identifier"{
    navigationController?.popViewController(animated: false)
    navigationController?.pushViewController(destinationController, animated: true)
    }
    }

-You also have to override one method in your source viewcontroller

  override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
    return false
}
Mahesh Giri
  • 1,810
  • 19
  • 27
1

Well, what you can also do is to use the unwind view controller stuff.

Actually I think that this is exactly what you need.

Check this entry: What are Unwind segues for and how do you use them?

Community
  • 1
  • 1
  • 1
    I don't believe you can get the desired behavior with unwind segues, as unwinds are just for going back. A replace is essentially going back then forwards to a different controller in 1 step. – ima747 Oct 21 '14 at 03:30
  • The question is asking "how to go back from view 3 directly to view 1" and not forwards again? – zc246 Apr 11 '16 at 09:19
1

This worked for me in Swift 3:

class ReplaceSegue: UIStoryboardSegue {
    override func perform() {
        if let navVC = source.navigationController {
            navVC.pushViewController(destination, animated: true)
        } else {
            super.perform()
        }
    }
}
jeffbailey
  • 137
  • 6
1

How about this :) I now it's old question, but this will work as a charm:

UIViewController *destinationController = [[UIViewController alloc] init];
UINavigationController *newNavigation = [[UINavigationController alloc] init];
[newNavigation setViewControllers:@[destinationController]];
[[[UIApplication sharedApplication] delegate] window].rootViewController = newNavigation;
Denis Kozhukhov
  • 1,205
  • 8
  • 16