8

I have a question about exception handling in Swift. The UIKit documentation for the UIStoryboard class states that the instantiateViewControllerWithIdentifier( identifier: String ) -> UIViewController function will throw an exception if the identifier is nil or does not exist in the storyboard. However, if I use a do/try/catch like the following, I receive a warning "No calls to throwing functions occur within 'try' expression."

It is only a warning so I figured that it was a intellisense issue; but when I run the following code and deliberately use an invalid identifier no exception is caught and a SIGABRT is generated.

        let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
    do {
        let controller = try storyboard.instantiateViewControllerWithIdentifier("SearchPopup")

        // This code is only included for completeness...
        controller.modalPresentationStyle = .Popover
        if let secPopoverPresentationController = controller.popoverPresentationController {
            secPopoverPresentationController.sourceView = self.view
            secPopoverPresentationController.permittedArrowDirections = .Any
            secPopoverPresentationController.barButtonItem = self.bSearchButton
        }
        self.presentViewController(controller, animated: true, completion: nil)
        // End code included for completeness.
    }
    catch {
        NSLog( "Exception thrown instantiating view controller." );
        return;
    }

How are you supposed to do/try/catch for functions that throw exceptions like this?

Thanks in advance.

Bryan

Bryon
  • 939
  • 13
  • 25
  • Thanks for the quick response. The main point of my question got lost in all the detail. The documentation says an exception IS thrown, but XCode warns that NO exception is thrown - why the difference? – Bryon Dec 29 '15 at 13:07
  • Yes, you get RuntimeException, But for static function itsn't it. – Kirit Modi Dec 29 '15 at 13:10
  • 1
    Sorry - I took longer than 5 minutes to write this so am reposting. Thanks for the quick response. The main point of my question got lost in all the detail. The documentation says an exception IS thrown, but XCode warns that NO exception is thrown - why the difference? My catch does not have a pattern and according to the Swift language reference " If a catch clause doesn’t have a pattern, the clause matches any error and binds the error to a local constant named error." I had a look at your post and I don't think that I am doing anything different to what you have documented. – Bryon Dec 29 '15 at 13:17
  • Does this answer your question? [Catching NSException in Swift](https://stackoverflow.com/questions/32758811/catching-nsexception-in-swift) – Sindre Sorhus Mar 09 '20 at 14:45

4 Answers4

8

This is a specific case of the more general issue discussed in Catching NSException in Swift

The summary seems to be that swift exceptions and objc exceptions are different.

In this instance, the swift documentation says it throws an Exception, but this cannot be caught; which sounds like a documentation bug at the very least.

I don't agree with the other answers here that a missing VC is clearly a programmer error. If the behaviour was as documented, one can postulate a design where common code reacts differently depending on whether a VC is present or not in a particular case|product|localisation. Having to add additional config to ensure that loading the VC is only attempted when it is present is an invitation to edge case bugs and the like. c.f. update anomalies.

Gordon Dove
  • 2,537
  • 1
  • 22
  • 23
  • Indeed when looking at the documentation of the UIStoryboard init method, I coudn't understand why the doc would say that it throws but that I wouldn't be able to `try?` on that init. Only to find your answer. Thank you. – itMaxence Oct 07 '19 at 21:56
1
let storyboard = UIStoryboard(name: "StoryboardName", bundle: nil)

This method doesn't throw error and doesn't return nil if storyboard is not found so it will just crash the app in runtime. That's inevitable. But I have figured out the way to make sure runtime crashes due to this exception won't happen. And it's through Unit-test.

There are some conventions though:

enum AppStoryboard:String, CaseIterable {
    case Main
    case Magazine
    case AboutUs
    
    var storyboard:UIStoryboard {
        return UIStoryboard(name: self.rawValue, bundle: nil)
    }
}
  1. You should only initialize storyboard through this enum. This enum is CaseIterable so you can access all cases with AppStoryboard.allCases

After this make a unit test class for checking if all the storyboard that we need exists.

class AppstoryboardTests: XCTestCase {
    func testIfAllStoryboardExistInBundle() throws {
        let storyboards = AppStoryboard.allCases
        for sb in storyboards {
            _ = sb.storyboard
        }
        XCTAssert(true)
    }
}

If all the storyboard defined in Appstoryboard doesn't exist in bundle, the test will just fail with an exception. If all storyboard is available the test will pass.

This method is little unorthodox but, better than app crashing in runtime. :)

Note: If you are not comfortable with Unit Test Put this in AppDelegate in didFinishLaunchingWithOptions method like this:

   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let storyboards = AppStoryboard.allCases
        for sb in storyboards {
            _ = sb.storyboard
        }
        return true
    }

The first thing app will do if storyboard is not found is crash.

Prajeet Shrestha
  • 7,978
  • 3
  • 34
  • 63
0

You do not recover from that exception, that exception is a RuntimeException.
Simply ask yourself: "how would I react to it?" - if the answer is "I do not know" then why would you even want to catch it?

Take the example of the incorrect identifier - what would you do when catching the error???

You cannot recover in any way that makes sense. If the identifier you pass in cannot be found that is something you as the developer did wrong when creating the app. That is something that would and should be obvious when testing the app. If you somehow miss it your app will crash in Review by Apple or on the client's device.

luk2302
  • 55,258
  • 23
  • 97
  • 137
  • 3
    I have to disagree. If you dinamically receive the identifier, say, through an http request, and you have two versions of your app up: NewVersion and OldVersion (because you always have a handful of users who do not update), you have to catch and react to the error in OldVersion. I've had several situations where I use fallback code to react, in old versions, to code only new versions have. For instance, the catch block could very well trigger a warning telling the user he needs to update to check the new content, and that's only scratching the surface. – Uzaak Jan 09 '18 at 17:44
  • 1
    @Uzaak using try-catch for detecting updates sounds extremely bad. That is still not a valid use case for having to catch exceptions like this. *If* you get the identifier from an extern source you should *verify* it first, not just *use* it and react to the resulting exceptions. Sounds like a bad design imho. – luk2302 Jan 09 '18 at 22:29
  • 4
    I completely agree that it should be verified beforehand, but Apple does not give us a way to verify it. Originally, storyboard.instantiateViewControllerWithIdentifier was meant to return nil when they did not find a controller (as documentation stated back then), which is fine. But it erroneously threw an exception. Later on, they fixed the documentation (lol) and made the exception throw be the general case. So, unless there is a non try-catch way of finding out if the view exists, I currently stick to this. Do you know of any better way? – Uzaak Jan 19 '18 at 17:08
0

The instantiateViewControllerWithIdentifier is not a throwing function and you can't handle it using do...try...catch. If the view controller is not available in the storyboard, there is nothing you can do. It's a programmer mistake, the one who created that, should handle such issues. You can't blame iOS runtime for such kind of errors.

Midhun MP
  • 103,496
  • 31
  • 153
  • 200
  • 2
    Thanks Midhun and @luk2302. That makes perfect sense for hard-coded identifiers. For data-driven identifiers that leaves a bit of a gap for making an app bullet proof but understanding this behaviour will drive the right testing. Thanks! – Bryon Dec 29 '15 at 13:25