Refactoring UIAlertController: Helper class and protocol approaches

Detailed look at two possible approaches to avoid writing boiler plate code over and over.

Published: Oct. 5, 2020
See books

In this post I would like to take a look at UIAlertController. This is like one of the foundational component of UIKit apps. It is useful when you need to present important information to the user, let the user confirm the deletion of something, make a choice etc. In each case UIAlertController works very well.

The repetitive code

Chances are you have seen code like this many times:

let ac = UIAlertController(title: "Important", message: "This is an important message for the user", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Got it!", style: .default, handler: nil))
present(ac, animated: true)

Or something along these lines:

let ac = UIAlertController(title: "Delete?", message: "Really delete this item?", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Delete", style: .destructive, handler: { (_) in
            // delete here
}))
ac.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(ac, animated: true)

This is a longer example which also deals with deletion of some item. This type of code will quickly clutter your codebase and make view controllers gain lines. Imagine you need to display the alert from the first example many times in your app, just with different texts. If it is in one view controller, you can create helper method which will take the texts as a parameters and display the alert. Which is better but still not ideal.

I think that would be enough for the introduction and let's see how we can refactor UIAlertController for greater flexibility. This will save you lines of code but also helps you keep alerts consistent. If you are going to show numerous info alerts, you don't have to think about the text on the button to close it. You can once set something like "Ok" or "Got it" and done.

There is also another potential benefit to having UIAlertController code in single file. Imagine you want to roll out your custom alerts with design that better suits your app. If all the code is contained within single file, you can just change it here and suddenly every alert in your app just got a new style. I think there are broadly two approaches to abstracting away UIAlertController and those are using helper class with static methods (or singleton) and using protocols.

You cannot show alerts from any part of code, because you need the present method available on UIViewController.

Helper class approach

Let's see how to create helper class that will show alerts.

The minimal basic example can look like this:

class AlertHelper {
    static func showAlert(title: String?, message: String?, over viewController: UIViewController) {
        let ac = UIAlertController(title: title, message: message, preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "Got it", style: .default, handler: nil))
        viewController.present(ac, animated: true)
    }
} 

Notice we need to send UIViewController as a parameter so we have something to present our alert.

We could improve this a bit:

class AlertHelper {
    static func showAlert(title: String?, message: String?, over viewController: UIViewController) {

        assert((title ?? message) != nil, "Title OR message must be passed in")

        let ac = UIAlertController(title: title, message: message, preferredStyle: .alert)
        ac.addAction(.gotIt)
        viewController.present(ac, animated: true)
    }
}

I have added assert to catch when someone does not pass title nor message because such alert does not make sense. And I refactored the "Got it" action as an extension on UIAlertAction:

extension UIAlertAction {
    static var gotIt: UIAlertAction {
        return UIAlertAction(title: "Got it", style: .default, handler: nil)
    }
}

Creating similar extension for "Cancel" would also be useful.

Our previous three lines of code to show alert can be accomplished as a single line:

AlertHelper.showAlert(title: "Important", message: "This is an important message for the user", over: self)

Now all the info alerts will be consistent and we can decide to use custom alerts later in the future.

However so far we have covered just a single use case and as you can imagine, there are countless of ways of using UIAlertController. You can configure it as a sheet, make one of the button the preferred one (with bold font) and much more.

It is likely that your app does not need all of them. So instead of building all-purpose refactored alerts, create just a helper methods for the alerts you are using. Otherwise you can end up with dozens of confusing methods.

Let's see how to build more complex alerts using this helper class approach and then we will take a look at the protocols approach.

Alert with confirm action to delete stuff

Let's see how to refactor alert that will let us present confirm button and react when user taps confirmation. We are going to use closures. In my projects I usually have this useful typealias:

typealias Action = () -> ()

This represents closure that does not take any arguments and does not return anything.

Now with this ready we can build another helper method.

static func showDeleteConfirmation(title: String, message: String?, onConfirm: @escaping Action, over viewController: UIViewController) {
    let ac = UIAlertController(title: title, message: message, preferredStyle: .alert)

    ac.addAction(UIAlertAction(title: "Delete", style: .destructive, handler: { (_) in
        onConfirm()
    }))

    ac.addAction(.cancel)

    viewController.present(ac, animated: true)
}

And usage:

AlertHelper.showDeleteConfirmation(title: "Delete?", message: "Really delete this item?", confirmedAction: {
            // delete here
}, over: self)

Compare to the initial code:

let ac = UIAlertController(title: "Delete?", message: "Really delete this item?", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Delete", style: .destructive, handler: { (_) in
            // delete here
        }))
ac.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(ac, animated: true)

This is another nice example, but UIAlertController has another complication up its sleeve. This only applies the iPads but the technique can be well useful in iPhone apps too. If you are using .actionSheet variant of the UIAlertController then this is presented as a popover on iPads and you need to set its source otherwise the app will crash.

Source can be either UIBarButton or any UIView together with source rect. This is tricky to have as a method parameter. One way would be to utilize enum with associated values like this:

enum PopoverAnchor {
    case barButton(button: UIBarButtonItem)
    case view(view: UIView)
}

And the method to present action sheet would look like this:

static func showActionSheet(title: String, anchor: PopoverAnchor, over viewController: UIViewController) {
    let ac = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)

    switch anchor {
    case .barButton(let button):
        ac.popoverPresentationController?.barButtonItem = button
    case .view(let view):
        ac.popoverPresentationController?.sourceView = view
        ac.popoverPresentationController?.sourceRect = view.bounds
    }

    ac.addAction(.gotIt)

    viewController.present(ac, animated: true)
}

I like enums very much for configuration because you can neatly represent totally different ways of configuring something. But in this case, I prefer a closure that lets us provide additional UIAlertController configuration on the call site.

Let's modify the first example for showing info alerts with the option to provide extra configuration.

static func showConfigurableSheet(title: String, over viewController: UIViewController, configuration: (UIAlertController) -> ()) {
    let ac = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
    ac.addAction(.gotIt)

    configuration(ac)

    viewController.present(ac, animated: true)
}

I chose this slightly convoluted meaning to better distinguish the methods on the example.

With this, we can freely do any additional configuration later:

AlertHelper.showConfigurableSheet(title: "This is important", over: self) { (alert) in
    alert.popoverPresentationController?.barButtonItem = navigationItem.leftBarButtonItem
}

I think this is a great example of the power of closures. Of course the configuration parameter could be optional with default value of nil for even greater flexibility.

This brings us to the end of helper class approach regarding alerts. I think it works pretty well, just the part with passing around view controllers does not seem right to me.

Protocol approach

We can leverage the power of protocol extensions to achieve the same goal, without any intermediary helper class.

Let's see how. We start by creating AlertsPresenting protocol.

protocol AlertsPresenting: UIViewController {
}

extension AlertsPresenting {
}

That is basic skeleton done. Notice we constrained the protocol to only work with UIViewController. This will give us an access to the present method.

Next we can "port over" the showAlert method from our AlertHelper:

extension AlertsPresenting {
    func showAlert(title: String?, message: String?) {

        assert((title ?? message) != nil, "Title OR message must be passed in")

        let ac = UIAlertController(title: title, message: message, preferredStyle: .alert)
        ac.addAction(.gotIt)

        present(ac, animated: true)
    }
}

It is almost same. To use it, we need add the protocol AlertsPresenting to the view controllers where we want to use this method.

class ViewController: UIViewController, AlertsPresenting

And with this, new showAlert method is available and we can use it:

showAlert(title: "Important", message: "This is an important message")

I prefer this approach since there is no need to use the helper class and pass view controllers around. The other methods from AlertHelper would work the same way, feel free to port them yourself.

Another improvement might be to just conform UIViewController to our new protocol like this:

extension UIViewController: AlertsPresenting { }

And now all view controllers in our project can present alerts. We could debate whether this is a correct choice regarding responsibilities but I think it makes sense. There is not a lot of methods on view controllers we are regularly calling and showing alerts seems like valid responsibility.

Conclusion

I think this is a good place to end it. We have seen two ways of refactoring UIAlertController and I think they are both great choices. Certainly much better to writing the boiler plate all over again in view controllers.

There is a third option that comes to mind and that is creating a sort of a factory for UIAlertController. In this case you would have possibly static methods somewhere that would return configured instances of UIAlertController and presenting it would be responsibility of the caller.

Do you have different approach to UIAlertController? Let me know over at Twitter. Thanks for reading!

Uses: Xcode 12 & Swift 5.3

Bluesky logo

Follow on Bluesky to not miss new posts

Filip Němeček profile photo

WRITTEN BY

Filip Němeček Mastodon

iOS blogger and developer with interest in Python/Django.

iOS blogger and developer with interest in Python/Django.