How to "cancel" a UIStoryBoardSegue
Asked Answered
C

10

46

Does anyone know how to "stop" a segue transition conditionally:

My table view cells represent products which can be viewed in a drill-down "detail" view... or cannot! (It depends on a couple of things)

Now my App considers all products "unlocked":

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    NSIndexPath *selectedRowIndex = [self.tableView indexPathForSelectedRow];
    ListaProdottiController *prodottiViewController = [segue destinationViewController];
    prodottiViewController.blocco = [self.fetchedResultsController objectAtIndexPath:selectedRowIndex];
}

How can I cancel the row selection => drilldown, at this point?

Chaos answered 19/10, 2011 at 10:20 Comment(1)
If you are using iOS 6 or greater, see my answer below for a proper way to handle this.Kersey
K
82

If you are targeting iOS 6 or greater, then my knowledge of the cleanest way to do this is the following:

-(BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
{
    if([identifier isEqualToString:@"show"])
    {
        NSIndexPath *selectedRowIndex = [self.tableView indexPathForSelectedRow];
        Blocco *blocco = [self.fetchedResultsController objectAtIndexPath:selectedRowIndex];
        return [blocco meetRequiredConditions];
    }
    return YES;
}

Where there is a method

-(BOOL) meetsRequiredConditions;

Defined on your Blocco class returns YES if the "couple of things" which permit a drill-down are valid.

Kersey answered 18/3, 2013 at 20:26 Comment(1)
Looking at the Apple reference it seems this is how it is intended to be used. In my case if the segue is blocked I would like to do a dialog box. One thing at a time though.Neckcloth
A
32

I don't know if it is the right way to do it but I discovered a workaround.

From the storyboard I associate(control+click) a segue from the status bar in the view controller. Give the segue an ID (for example: switchSegue).

Now, from an action in your code (in my code I use a button), I call:

 [self performSegueWithIdentifier:@"switchSegue" sender:sender];

That way you can control if your segue is performed or not. Try tutorials that helped me from here and here

Hope this helps.

Abed answered 20/10, 2011 at 16:37 Comment(2)
It's the simplest way I have found :) Cool! Thank you!Acaulescent
A simple and fast fixed, good explanation and sources - I wish every answer would be this greatBeatabeaten
D
18

I am using an much easier and tidy approach.

Storyboard

  1. Create two identical cell with different identifiers. Eg: "cellWithSegue" and "cellWithoutSegue".
  2. Connect the first cell ("cellWithSegue") with the segue you want to display.
  3. Do not connect the second cell with any segue.

Table View

  1. On cellForRowAtIndexPath, implement a logic to determine if the cell should be linked a segue or not.
  2. For cells that should be linked with the segue use the "cellWithSegue" identifier, for the rest the "cellWithoutSegue".

This way looks a lot easier to implement and also does not alter the way segues are supposed to work.

Distaste answered 22/1, 2012 at 13:25 Comment(5)
This is by far the most robust solution and still very easy. Much thanks. I would up vote it again if I could.Crankpin
wow, this is the most tedious way around - now you have to maintain the looks of two cell types, the code that determines which one to show them instead of if (this) doSegue() kind of thing.Hypodermic
-1 This is a horrible solution. "Create two identical cells" is a direct violation of the DRY principle.Rhetorical
and what makes you think this is "tidy"?Picasso
I don't any more. I was not aware of the accepted solution at the time.Distaste
P
14

I may be wrong here, but after struggling myself with this, I just disabled the cell's user interaction on the cells where I didn't want the seque triggered (in cellForRowAtIndexPath:). Seems to work perfectly, and it's only 1 line of code!

cell.userInteractionEnabled = NO;
Pearlpearla answered 29/2, 2012 at 11:34 Comment(2)
Great tip! Perfect solution in my case.Goerke
Note that this solution is OK for cellForRowAtIndexPath: but is not enough when used in a different method, for instance when attempting to prevent double-triggering in prepareForSegue:sender:, as the cell may be recreated without user interaction disabled.Francis
C
11

The easiest solution is to create manual segue in story board and use that as seen below.

[self performSegueWithIdentifier:@"loginSuccessSegue" sender:self];

Or


@Fabio: I was trying to get a solution for same kind of use-cases and I almost found a solution.

Use-cases 1. Stop segue transition conditionally 2. Change destination viewController conditionally

Solution:

Use "Custom" segue. Follow below steps to create Custom segue 1. Create a subclass of UIStoryboardSegue "MyCustomSegue.h"

@interface MyCustomSegue : UIStoryboardSegue
@end

"MyCustomSegue.m"

Override initWithIdentifier for implementing use-case 1 and 2 If you return nil, segue will be cancelled/no action will be taken You instantiate your ViewController and set that as a destination. You can set destination as your old xib file also.. that code is commented, but I ensured that will work.

@implementation MyCustomSegue
- (id)initWithIdentifier:(NSString *)identifier source:(UIViewController *)source destination:(UIViewController *)destination{

    UIStoryboard *storyBoard= [UIStoryboard storyboardWithName:@"MainStoryboard_iPhone" bundle:nil];

    UIViewController *viewController = [storyBoard instantiateViewControllerWithIdentifier:@"testIdentifier"];

    // MyViewController* viewController= [[MyViewController alloc]initWithNibName:@"MyViewController" bundle:nil];
    return [super initWithIdentifier:identifier source:source destination:viewController];
}
  1. You must override "perform".

You can implement use-case 1 here also..

- (void)perform {
    // if either source or destination is nil, stop
    if (nil == self.sourceViewController || nil == self.destinationViewController) return;
    // return; //No Action. Segue will be cancelled
    UINavigationController *ctrl = [self.sourceViewController navigationController];
    [ctrl
     pushViewController:self.destinationViewController
     animated:YES];
}

Hope this helps. Plz write if you are not clear.

Chaperone answered 4/11, 2011 at 3:30 Comment(4)
This seems to be a lot of extra work than what is needed. Wouldn't it be easier to choose which seque you wanted to invoke (or not) based on what table cell is selected? You would not have create a custom segue and your view controller would lose its dependency on other view controllers. You can request that a seque be perfomed in code by using: [self performSegueWithIdentifier:@"SegueIdentiferName" sender:self];.Avertin
Agreed this is not needed. Just manually call performSegueWithIdentifier and you will not need to override UIStoryboardSegueExpansion
in my opinion it is not a lot of code, it is just one class than you can reuse for other segue and controllers by use of categories and protocols for example. I found it much better than writing lot of it-then-else case for deciding which button or cell has been pressed in the xib.Grave
A better approach would be connect the thing that triggers the segue to an IBAction that determines if the segue should occur and triggers [self performSegueWithIdentifier:@"Whatever" sender:self]Fichu
S
4

A nice and lightweight way of doing this is inside the UITableViewDelegate method tableView:willSelectRowAtIndexPath:—where applicable. YMMV.

Here's how I'm doing it (iOS 5, ARC). I use a BOOL instance variable in my view controller, initially set to False in viewDidLoad. The destination view controller that the table cells are set to segue to in Interface Builder rely on a bit of data having gotten loaded from a server, so I don't want the segue to happen until I have the data.

Simplified, this is what it looks like:

@implementation SomeViewController {
    BOOL okayToSegue;
}

...

- (void)viewDidLoad
{
    [super viewDidLoad];
    okayToSegue = NO;
    // The success block for the data retrieval
    void(^successBlock)(void) = ^{
        // Other code...
        okayToSegue = YES;
    }
    [[ServerClient sharedClient] getDataFromServerSuccess:successBlock];
}

...

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (!okayToSegue) {
        return nil;
    }
    return indexPath;
}

Ignore details like that sharedClient bit there, it's just how I'm calling out to my AFHTTPClient subclass with the success block, in real life I'd have a failure block and other things as well.

Returning nil in tableView:willSelectRowAtIndexPath: causes tapping on a table cell to do nothing. Only after I've gotten word from my AFHTTPClient instance (through successBlock) that the data the segued-to view controller needs is ready and waiting, I change the instance variable and future taps will work just fine. In real-life code, you'll want to have some user-visible notification or visually obvious tell-tale sign that segueing is not yet possible.

So whatever logic you need to determine if segueing from a table cell is OK or not is, in many cases, possible to do in this manner.

Shad answered 28/5, 2012 at 12:53 Comment(4)
Probably the most generically applicable solution.Carpeting
Although this will not work as expected if you still want the cell to be selected without performing the segue.Dissertate
jweyrich: True. Programmatically doing the cell selection in willSelectRow... (perhaps with a timed unselection) is still possible I think, and in any case, you'd want to give the user some type of indication of why the cell tap isn't doing what they expect.Shad
Imagine a picker, but on its own screen. The user taps an item on the table, it calls a delegate passing the selected item, which then handles it and pops the controller. This required me to extend UITableViewController, add a property inSelectionMode and conditionally change accessoryType from Disclosure to Mark. Consequently I wanted the cell to be visually selected, without performing the segue. Both seem to require didSelectRowAtIndexPath to be called. The easiest solution I found was to link the segue directly to the controller and call performSegueWithIdentifier conditionally.Dissertate
P
2

The way Apple's template does it for the iPad popOver is by using a manual segue, as opposed to a automatic segue that triggers on a touch the manual one needs to be triggered with performSegueWithIdentifier:

To create a manual segue instead of ctrl-dragging from the element you have in mind ctrl-drag from the view's controller icon, set a identifier for the segue and you are done in IB.

manual segues

Pester answered 6/2, 2013 at 13:39 Comment(0)
B
0

Here another solution I just found.

In your Mainstoryboard TableView, as you use automatic segue from your cell (identifier=Cell) to the destination view, you can also add another cell with another identifier (identifier=CellWithoutSegue). So when creating new cell in cellForRowAtIndexPath, just reuse the cell identifier you want (with or without segue).

Hope it helps!

(Tell me if you want some code source examples).

Regards,

Bustamante answered 8/11, 2012 at 10:1 Comment(0)
D
0

You can cancel segue by returning false in

override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
    let someCondition = ...
    return someCondition
}

This method is called whenever a segue is about to occur. This method is not called when a segue is triggered explicitly by performSegue(withIdentifier: ..., sender: ...).

Danutadanya answered 15/12, 2021 at 19:55 Comment(0)
B
-1

In your - (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender; method, you could add some code such as:

if ([[segue identifier] isEqualToString:@"DemoSegue"] && !self.canOpenDemo) {
    id *nc = [segue destinationViewController]; // UIViewController, UINavigationController, etc. (keeping "id" will return warning)
    [nc dismissModalViewControllerAnimated:NO];
}

And this will stop the view from opening, however I have not checked, but it seems like it will have already called your destination view controllers initialize function (again, I haven't checked in Xcode, so I'm not entirely sure).

Becht answered 4/11, 2011 at 23:35 Comment(2)
This just doesn't work at all. You can't dismiss the view controller because you're running before the segue happens.Fichu
For some reason it still works when the segue is presented modally though, even though it doesn't seem like it would. It worked in my application that I discontinued. It may not work now that 5.0.1 and 5.1 are released. There is a shouldPerformSegueWithIdentifier:sender: method in iOS 6 but nothing more can be explained because of NDA.Becht

© 2022 - 2024 — McMap. All rights reserved.