A Swift Solution to View Controller Dismissal

One of the most common complaints I see from people new to iOS is how view controller dismissal works. If you’re a view controller that is being presented, either on a controller stack, modally, or in some other way, there are a few ways you can clean up once you need to be dismissed. The most obvious solution is to directly dismiss yourself, using something like the following

1
2
3
4
@IBAction func finished(_: AnyObject?)
{
  self.dismissViewControllerAnimated(true, completion: nil)
}

The Drawbacks

Apple notes that this is not how you are supposed to dismiss a view controller, but that if you do this on a view controller that is being presented, it will automatically forward it to the presenting view controller, and then do the dismissal from there.

This approach is effective, but has a few things that really need to be considered that kind of make it a bit of a kludge.

  • This approach is not generic - it doesn’t handle cases where the view controller is not being presented, but is instead in a navigation stack. While in practice, view controllers are often very specifically tied to the flow of the UI, we should try to keep this general unless we absolutely must not, as it makes it easier to refactor or reuse in the future
  • It is hard to hook into this to make any other changes during dismissal - you’re effectively backing yourself into a corner and using a semi-automatic delegation pattern instead of setting one up.
  • This can complicate the flow of your program through an implicit delegation, and make it harder to figure out exactly what is going wrong if there’s a problem in your view controller logic. The bug may appear in another view controller you never call dismiss on directly.

The way that this is generally avoided is to create a delegate in the view controller being presented. Before presentation, the view controller doing the presentation sets itself as the delegate, then displays the other view controller modally. There’s a few drawbacks to this approach as well, notably that you end up with a slightly heavier-weight delegate protocol that is really only used for this dismissal. You also end up having to wire this up for every time you present the view controller as well as write the logic to then dismiss it. This can get a bit heavy.

Thanks to Swift category extensions, there’s a pretty easy way around this. It’s nothing particularly new, and I’m sure others use it all the time, but it’s a useful trick.

The Swift Solution

What we really want to accomplish here is to have a view controller that does some form of presentation or showing of another view be able to be notified on dismissal and take appropriate action. To do this, we can create a set of two protocols, one for the thing we will be presenting on, and one for the thing we’ll be presenting, and then use a protocol extension to give us a standard implementation that works for most view controllers. This gives us an almost drop-in solution with minimal fuss - our only real work is setting the delegate where we need to.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protocol DismissalDelegate : class
{
    func finishedShowing(viewController: UIViewController);
}

protocol Dismissable : class
{
    weak var dismissalDelegate : DismissalDelegate? { get set }
}

extension DismissalDelegate where Self: UIViewController
{
    func finishedShowing(viewController: UIViewController) {
        if viewController.isBeingPresented() && viewController.presentingViewController == self
        {
            self.dismissViewControllerAnimated(true, completion: nil)
            return
        }

        self.navigationController?.popViewControllerAnimated(true)
    }
}

Using this is as simple as adding the category to the class definition for your presenting and presented view controllers - here’s an example of a set of view controller using this protocol.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PresentedController: UIViewController, Dismissable {

    weak var dismissalDelegate: DismissalDelegate?

    @IBAction func done(_: AnyObject?)
    {
        dismissalDelegate?.finishedShowing(self)
    }
}

class PresentingViewController: UIViewController, DismissalDelegate {
  override func prepareForSegue(segue: UIStoryboardSegue, sender _: AnyObject?) {
      if let vc = segue.destinationViewController as? Dismissable
      {
          vc.dismissalDelegate = self
      }
  }
}

Here we can see the only wiring required, as we noted, is to set the delegate, as well as to invoke the finishedShowing callback when we’re done in the presented view controller. In some cases, it can also makes sense to make a super class for the DismissalDelegate that implements prepareForSegue to do the work there automatically. Either way, you end up with a lot less glue code related to presentation and dismissal.

Another nice aspect of this, as we noted earlier, is that you no longer have a strong relationship in a view controller into how it is presented. This lets the view controller act more agnostically and make it easier to use throughout your application in diverse areas. We let whoever is doing the presentation worry about how to dismiss us, and we neither care who is presenting us, nor how they are doing so - we worry only on notifying our parent upon completion.

Conclusion

In this post, we’ve covered a simple way to wrap some common iOS glue code up using a Swift protocol extension to help us keep dismissal logic out of presented view controllers. Doing this helps us keep our view controllers agnostic to how they are being displayed and aids in reusability, helps save us unnecessary glue code in all our controllers, and saves us time and effort in implementing glue code in each view controller that needs to show another.

Oct 23rd, 2015
Follow Me on Twitter

Comments