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 Sponsored See booksIn 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 enum
s 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