37

This might be a very simple question but didn't yield any results when searching for it so here it is...

I am trying to work out a way to check if a certain view controller can perform a segue with identifier XYZ before calling the performSegueWithIdentifier: method.

Something along the lines of:

if ([self canPerformSegueWithIdentifier:@"SegueID"])
    [self performSegueWithIdentifier:@"SegueID"];

Possible?

Rog
  • 18,602
  • 6
  • 76
  • 97
  • Exactly the question i'm looking for the answer to at the moment... – Dan F May 14 '12 at 17:28
  • 1
    Hey Dan I ended up using `@try @catch @finally`. It works fine. – Rog May 14 '12 at 22:49
  • I did as well, I just really hope there is actually a way of checking this. As a rule, I try to avoid situations where an exception COULD be thrown during normal runtime situations. – Dan F May 17 '12 at 20:11
  • 2
    Curious why you would not know in advance if a viewController could handle a segue. Is there a code design issue, etc. – spring Jun 28 '12 at 09:49
  • @TOMATO In my case, I'd like this for an automatic configuration. If a thing has an associated segue, I can configure its UI to show that (specifically, it's `UITableViewCell` instances, and wanting to configure their accessory). There are plenty of other ways to achieve the same goal (I'm using one), but the ways I can think of lead to repetition, which I like to avoid. – Benjohn Aug 01 '15 at 18:08

9 Answers9

24

To check whether the segue existed or not, I simply surrounded the call with a try-and-catch block. Please see the code example below:

@try {
    [self performSegueWithIdentifier:[dictionary valueForKey:@"segue"] sender:self];
}
@catch (NSException *exception) {
    NSLog(@"Segue not found: %@", exception);
}

Hope this helps.

Pang
  • 9,564
  • 146
  • 81
  • 122
  • 1
    This solution works great. Note that if you have an exception breakpoint, it will still break. You can however continue without crashing. – VaporwareWolf Mar 12 '14 at 18:23
  • 2
    You may get memory leaks under ARC because it does not clean up after exception. – pronebird Jul 04 '14 at 13:05
  • 1
    Do not use this. As Andy said it leaks the view controller sending the `performSegueWithIdentifier:sender:` message, and all its child view controller (because UIKit retain the view controller but never gets the chance to release it due to the exception). – Guillaume Algis Oct 28 '15 at 17:26
15
- (BOOL)canPerformSegueWithIdentifier:(NSString *)identifier
{
    NSArray *segueTemplates = [self valueForKey:@"storyboardSegueTemplates"];
    NSArray *filteredArray = [segueTemplates filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"identifier = %@", identifier]];
    return filteredArray.count>0;
}
Evgeny Mikhaylov
  • 1,696
  • 1
  • 19
  • 25
15

This post has been updated for Swift 4.


Here is a more correct Swift way to check if a segue exists:

extension UIViewController {
func canPerformSegue(withIdentifier id: String) -> Bool {
        guard let segues = self.value(forKey: "storyboardSegueTemplates") as? [NSObject] else { return false }
        return segues.first { $0.value(forKey: "identifier") as? String == id } != nil
    }

    /// Performs segue with passed identifier, if self can perform it.
    func performSegueIfPossible(id: String?, sender: AnyObject? = nil) {
        guard let id = id, canPerformSegue(withIdentifier: id) else { return }
        self.performSegue(withIdentifier: id, sender: sender)
    }
}

// 1
if canPerformSegue("test") {
    performSegueIfPossible(id: "test") // or with sender: , sender: ...)
}

// 2
performSegueIfPossible(id: "test") // or with sender: , sender: ...)
LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174
Arbitur
  • 38,684
  • 22
  • 91
  • 128
  • Great answer, I'm concerned about that self.valueForKey("storyboardSegueTemplates"), "storyboardSegueTemplates" would be a private property as it's not exposed/documented anywhere. Is this code passed through app store validation? Anybody used this in your production applications? – Naresh Reddy M Apr 29 '18 at 23:45
7

As stated in the documentation:

Apps normally do not need to trigger segues directly. Instead, you configure an object in Interface Builder associated with the view controller, such as a control embedded in its view hierarchy, to trigger the segue. However, you can call this method to trigger a segue programmatically, perhaps in response to some action that cannot be specified in the storyboard resource file. For example, you might call it from a custom action handler used to process shake or accelerometer events.

The view controller that receives this message must have been loaded from a storyboard. If the view controller does not have an associated storyboard, perhaps because you allocated and initialized it yourself, this method throws an exception.

That being said, when you trigger the segue, normally it's because it's assumed that the UIViewController will be able to respond to it with a specific segue's identifier. I also agree with Dan F, you should try to avoid situations where an exception could be thrown. As the reason for you not to be able to do something like this:

if ([self canPerformSegueWithIdentifier:@"SegueID"])
    [self performSegueWithIdentifier:@"SegueID"];

I am guessing that:

  1. respondsToSelector: only checks if you are able to handle that message in runtime. In this case you can, because the class UIViewController is able to respond to performSegueWithIdentifier:sender:. To actually check if a method is able to handle a message with certain parameters, I guess it would be impossible, because in order to determine if it's possible it has to actually run it and when doing that the NSInvalidArgumentException will rise.
  2. To actually create what you suggested, it would be helpful to receive a list of segue's id that the UIViewController is associated with. From the UIViewController documentation, I wasn't able to find anything that looks like that

As for now, I am guessing your best bet it's to keep going with the @try @catch @finally.

Community
  • 1
  • 1
Rui Peres
  • 25,741
  • 9
  • 87
  • 137
4

You can override the -(BOOL)shouldPerformSegueWithIdentifier:sender: method and do your logic there.

- (BOOL) shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
    if ([identifier isEqualToString:@"someSegue"]) {
        if (!canIPerformSegue) {
            return NO;
        }
    }
    return YES;    
}

Hope this helps.

ffxfiend
  • 41
  • 3
2

Reference CanPerformSegue.swift

import UIKit

extension UIViewController{
    func canPerformSegue(identifier: String) -> Bool {
        guard let identifiers = value(forKey: "storyboardSegueTemplates") as? [NSObject] else {
            return false
        }
        let canPerform = identifiers.contains { (object) -> Bool in
            if let id = object.value(forKey: "_identifier") as? String {
                return id == identifier
            }else{
                return false
            }
        }
        return canPerform
    }
}
yuelong qin
  • 139
  • 1
  • 4
1

Swift version of Evgeny Mikhaylov's answer, which worked for me:

I reuse a controller for two views. This helps me reuse code.

if(canPerformSegueWithIdentifier("segueFoo")) {
  self.performSegueWithIdentifier("segueFoo", sender: nil)
}
else {
  self.performSegueWithIdentifier("segueBar", sender: nil)
}


func canPerformSegueWithIdentifier(identifier: NSString) -> Bool {
    let templates:NSArray = self.valueForKey("storyboardSegueTemplates") as! NSArray
    let predicate:NSPredicate = NSPredicate(format: "identifier=%@", identifier)

    let filteredtemplates = templates.filteredArrayUsingPredicate(predicate)
    return (filteredtemplates.count>0)
}
JoeGalind
  • 3,545
  • 2
  • 29
  • 33
  • I believe this method's name is reserved in iOS, as I'm experiencing unexpected behavior using this approach. It's better to rename it, e.g. to `canRunSegueWithIdentifier` – Andrey Gordeev Jan 15 '16 at 10:14
0

It will be useful, before call performSegue, check native storyboard property on base UIViewController (for example screen was from StoryBoard or Manual Instance)

extension UIViewController {

func performSegueWithValidate(withIdentifier identifier: String, sender: Any?) {
    if storyboard != nil {
        performSegue(withIdentifier: identifier, sender: sender)
    }
}

} enter image description here

-1

There is no way to check that using the standard functions, what you can do is subclass UIStoryboardSegue and store the information in the source view controller (which is passed to the constructor). In interface builder select "Custom" as the segue type as type the class name of your segue, then your constructor will be called for every segue instantiated and you can query the stored data if it exists.

You must also override the perform method to call [source presentModalViewController:destination animated:YES] or [source pushViewController:destination animated:YES] depending on what transition type you want.

Hampus Nilsson
  • 6,692
  • 1
  • 25
  • 29
  • Calling present, push and pop programmatically is very dangerous because the segue can be changed at any time from a model to a pushed scene. When you pop a modal it will crash. It is best to be sure that the named segue exists or use try/catch which I think is still pretty sloppy since it is not truly exceptional. There should be a method to check if a segue identifier is defined. This appears to be missing on purpose. – Brennan Mar 22 '14 at 20:21