4

In swift you can use a cool feature of the switch statement in prepare(segue:) to create cases based on the type of the destination view controller:

Example:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    switch segue.destination {

    case let detailViewController as DetailViewController:
      detailViewController.title = "DetailViewController"
    }
    case let otherViewController as OtherViewController:
      otherViewController.title = "OtherViewController"
    }
}

However, what if the segue is triggered by a split view controller, so the destination is a navigation controller, and what you really want to do is switch on the class of the navigation controller's top view controller?

I want to do something like this:

case let nav as UINavigationController,
     let detailViewController = nav.topViewController as? DetailViewController:
    //case code goes here

Where I have the same construct that I use in a multiple part if let optional binding.

That doesn't work. Instead, I have to do a rather painful construct like this:

case let nav as UINavigationController
  where nav.topViewController is DetailViewController:
  guard let detailViewController = nav.topViewController as? DetailViewController
    else {
      break
  }
  detailViewController.title = "DetailViewController"

That works, but it seems needlessly verbose, and obscures the intent. Is there a way to use a multi-part optional binding in a case of a switch statment like this in Swift 3?

vadian
  • 274,689
  • 30
  • 353
  • 361
Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • removing the `where` clause from the switch statement would mean that that case would capture **any** segue to a navigation controller, which is not what I need. I'm looking for a way to do a multi-part optional binding in a case of a switch statement. – Duncan C Nov 17 '16 at 17:05
  • Oops, yeah you're totally right. – Hamish Nov 17 '16 at 17:06
  • The usual way is to switch on the **identifier** of the segue. – vadian Nov 17 '16 at 17:14

5 Answers5

3

I worked out a decent solution to this problem.

It involves doing some setup before the switch statement, and then using a tuple in the switch statement. Here's what that looks like:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let dest = segue.destination
    let navTopVC = (dest as? UINavigationController)?.topViewController
    switch (dest, navTopVC) {

    case (_, let top as VC1):
        top.vc1Text = "Segue message for VC1"

    case (_, let top as VC2):
        top.vc2Text = "Segue message for VC2"

    case (let dest as VC3, nil):
        dest.vc3Text = "Segue message for VC3"

    default:
        break
    }
}
Duncan C
  • 128,072
  • 22
  • 173
  • 272
2

You might find this extension useful…

extension UIStoryboardSegue {
    var destinationNavTopViewController: UIViewController? {
        return (destination as? UINavigationController)?.topViewController ?? destination
    }
}

Then you can simply…

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    switch segue.destinationNavTopViewController {
    case let detailViewController as? DetailViewController:
        // case code goes here
}

Note that the ?? destination makes sure the return value is non-optional, and also allows it to work in places where the destination could also be a non-navigation controller.

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
  • Nice solution. See my self-answer though. I think it's more flexible, and makes it more explicit what's going on. – Duncan C Sep 18 '18 at 00:25
1

I don't think there is a way to do this with switch and case, but you can do something closer to what you are looking for with if and case (Update: as Hamish pointed out, the case isn't even needed for this scenario) or just normal if let:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    if let nav = segue.destination as? UINavigationController, 
        let detailViewController = nav.topViewController as? DetailViewController {
        detailViewController.title = "DetailViewController"
    }

    if let otherViewController? = segue.destination as? OtherViewController {
        otherViewController.title = "OtherViewController"
    }
}

Since your switch statement in this example isn't really going to ever be verified by the compiler as handling all cases (because you need to create a default case), there is no added benefit to using switch instead of just if let

Daniel Hall
  • 13,457
  • 4
  • 41
  • 37
  • `if case let nav?` What does that mean outside of a switch statement? – Duncan C Nov 17 '16 at 17:09
  • 1
    Or just `if let nav = segue.destination as? UINavigationController, ...` – Hamish Nov 17 '16 at 17:09
  • @DuncanC `if case` performs a pattern match in the if statement, with the pattern of `nav?` being the [optional pattern](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Patterns.html#//apple_ref/doc/uid/TP40014097-CH36-ID520). It matches against the expression `segue.destination as? UINavigationController` when the result is non-nil (it's unwrapped and bound to `nav`). Although this is functionally the exact same as an optional binding. – Hamish Nov 17 '16 at 17:13
0

Optional binding doesn't really lend itself to switches like you're trying to do.

I understanding the desire to use switches rather than simple if and if else, but it's a bit different conceptually from what switch is meant to do.

Anyway, here are the two options I use for most situations

func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    switch segue.destination {
    case is DetailViewController:
        segue.destination.title = "DetailViewController"
    case is OtherViewController:
        segue.destination.title = "OtherViewController"
    default:
        break
    }
}

or

func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    if let controller = seuge.destination as? DetailViewController {
        controller.title = "DetailViewController"
    }
    else if let controller = seuge.destination as? OtherViewController {
        controllercontroller.title = "OtherViewController"
    }
}
GetSwifty
  • 7,568
  • 1
  • 29
  • 46
  • Thanks for your input, but I disagree. `case let xx as class` lends itself VERY well to this situation. It's tailor-made, even. Take a look at the first code block in my question. That syntax lets you switch on the class of the destination view controller and do optional binding at the same time. – Duncan C Nov 17 '16 at 17:35
  • The complicating factor here is that I really want to switch based on the class of the destination view controller's top view controller iff the destination is a navigation controller. – Duncan C Nov 17 '16 at 17:36
  • But you're not switching based on the values of a variable, you're simply creating new variables. It's identical to if-let – GetSwifty Nov 17 '16 at 18:18
  • I can imagine it being added at some point, but it goes against the current idea of a switch statement – GetSwifty Nov 17 '16 at 18:19
  • No, it is not identical to `if let`. It actually switches based on the CLASS of the destination view controller, and creates a local variable of the appropriate type inside that case, all in one go. Really. – Duncan C Nov 17 '16 at 18:20
  • My example switches on the class. Yours doesn't. If you replace case with if and add = segue.destination it's identical, and it would be evaluated with the same level of efficiency as an if let. Also, it should be as? – GetSwifty Nov 17 '16 at 18:23
0

In my opinion, you should use segue identifiers for controlling flow in this switch. But to answer your question, this should work for you.

switch (segue.destination as? UINavigationController)?.topViewController ?? segue.destination {
    ...
}

The thing is, according to grammar, you have only one pattern (in your case it is binding) per item in a case item list. Since you have only one item on the input but want to use two patterns, you either want to normalize input (which is in this case appropriate as you can see above) or extend item list (which is inappropriate in this case but I show an example below).

switch ((segue.destination as? UINavigationController)?.topViewController, segue.destination) {
case (let tvc, let vc) where (tvc ?? vc) is DetailViewController:
    // TODO: If you await your DetailViewController in navigation or without.
    break
case (let tvc as DetailViewController, _):
    // TODO: If you await your DetailViewController in navigation but other vc is not in a navigation vc.
default:
    fatalError()
}
Stanislav Smida
  • 1,565
  • 17
  • 23