Whats the Swift best practice for reusable UIAlertController configuration via Enum - ios

I'm trying to make my life a little bit easier and my app more maintainable while at the same time reducing the amount of duplicated code. So I thought it would be nice to put the some code to display a certain type of UIAlertController into its own class.
The thing is, that I have basically the same alert which only differs really slightly based on where I display it in my app. So I thought to myself: why not use a enum and every time I want to display an alert of that kind, just give it the enum value. Get rid of all the duplicated strings everywhere in my code.
I love enums in Swift. They are just so schwifty - ehh I mean swifty. So I came up with this example enum:
enum MyAlertType {
case a
case b
}
Next I created a simple class to handle the display:
class MyAlert {
static func showAlert(ofType type: MyAlertType, inViewController viewController: UIViewController, handler: ((UIAlertAction) -> ())? = nil, completion: (() -> ())? = nil) {
var message: String
switch type {
case .a:
message = "A is a nice letter!"
case .b:
message = "B is a nice letter!"
}
let alert = UIAlertController(title: "Do you know which letter is nice?", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: handler))
viewController.present(alert, animated: true, completion: completion)
}
}
//Somewhere in my code (obviously a UIViewController)
MyAlert.showAlert(ofType: .a), inViewController: self)
But wait, I still have to give this function the ViewController where I want to display the alert in. For me thats always the same (self) so the next logical step was to make this an extension:
extension UIViewController {
func showAlert(ofType type: MyAlertType, handler: ((UIAlertAction) -> ())? = nil, completion: (() -> ())? = nil) {
var message: String
switch type {
case .a:
message = "A is a nice letter!"
case .b:
message = "B is a nice letter!"
}
let alert = UIAlertController(title: "Do you know which letter is nice?", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: handler))
present(alert, animated: true, completion: completion)
}
}
//Somewhere in my UIViewController)
showAlert(ofType: .a)
But this makes the code available for all UIViewControllers, even those where I don't explicitly need/want to display that kind of alert. I mean, yeah sure, I am the developer and I can decide not to use this but on the other hand isn't it always a good practice to hide everything as much as possible and as least as necessary? Who knows who might in the future join my dev team and start misusing my beautiful code in ways I haven't thought of?
The other day I learned about Swifts protocol oriented programming approach (which to be honest I still have not fully understood by now) and now I think, I maybe should make this a protocol with a default implementation and then let the only those UIViewControllers implement the protocol, where I need to show this alert.
protocol MyAlertProtocol {
func showAlert(ofType type: MyAlertType, handler: ((UIAlertAction) -> ())?, completion: (() -> ())?)
}
extension MyAlertProtocol where Self : UIViewController {
func showAlert(ofType type: MyAlertType, handler: ((UIAlertAction) -> ())? = nil, completion: (() -> ())? = nil) {
var message: String
switch type {
case .a:
message = "A is a nice letter!"
case .b:
message = "B is a nice letter!"
}
let alert = UIAlertController(title: "Do you know which letter is nice?", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: handler))
present(alert, animated: true, completion: completion)
}
}
extension MyViewController: MyAlertProtocol {}
I know that this might sound as an opinion based question so I'm not asking you if you think if this or that is better but to tell me if there actually is a best practice for this scenario and how it looks like.
Protocol with default implementation via protocol extension? Simple UIViewController extension? Custom Enum, Struct or Class with static function (and if so, which one)? Or maybe even just a function somewhere in a Swift file? I feel overwhelmed. The agony of choice...
UPDATE/SOLUTION
After reading the given answers, I decided that my Sergeys answer was indeed the most suitable one. I wanted to make my like "easier" by having "less lines of duplicated code". For me this included the "presend(controller:animated:)" inside my ViewController.
However, I think you guys are right. I should use a struct (a class is really not necessary for one static function) with a static func to generate the alert man make it "ready to use" but still let the caller decide where to present it.
By doing it like this, I could use my alert-generation-struct anywhere I want and for example let a delegate present it or pass it around until I reach a UIViewController who can present it, if my caller isn't one.
Therefore in my very simple case, I'll go with:
enum MyAlertType {
case a
case b
}
struct MyAlert {
static func showAlert(ofType type: MyAlertType, handler: ((UIAlertAction) -> ())? = nil) -> UIAlertController {
var message: String
switch type {
case .a:
message = "A is a nice letter!"
case .b:
message = "B is a nice letter!"
}
let alert = UIAlertController(title: "Do you know which letter is nice?", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: handler))
return alert
}
}
Thanks to everybody participating in dissolving the blockade in my head.

Making static function is common and convenient approach. Please, pay attention that type you created is only for namespacing.
Alec O's answer is nice, but sometimes you want to pass some action you want to perform when you press OK button. Also I would pick struct instead of class.
Here is my version of making alert:
struct Alert {
static func errorAlert(title: String, message: String?, cancelButton: Bool = false, completion: (() -> Void)? = nil) -> UIAlertController {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let actionOK = UIAlertAction(title: "OK", style: .default) {
_ in
guard let completion = completion else { return }
completion()
}
alert.addAction(actionOK)
let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
if cancelButton { alert.addAction(cancel) }
return alert
}
And usage in ViewController's subclass:
// Creating alerts:
let simpleAlert = Alert.errorAlert(title: "Error", message: "Simple message")
let alertWithCompletionAndCancel = Alert.errorAlert(title: "Message", message: "Message", cancelButton: true) {
// do something awesome
}
// Presenting alerts:
present(simpleAlert, animated: true)
present(alertWithCompletionAndCancel, animated: true)

I would suggest returning the alert from the function for the view controller to show. The custom alert class should not be presenting anything, if your aim is to use best practices.
class MyAlert {
static func generateAlert(ofType type: MyAlertType) -> UIAlertController {
var message: String
switch type {
case .a:
message = "A is a nice letter!"
case .b:
message = "B is a nice letter!"
}
let alert = UIAlertController(title: "Do you know which letter is nice?", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: handler))
return alert
}
}
This will be the code that you repeat throughout the application. If multiple alerts are used in one view controller then you can initialized MyAlert globally for that view controller.
let myAlert = MyAlert()
let alert = myAlert.generateAlert(ofType: MyAlertType.a)
self.present(alert)

![This is the way I did
]1
When I want to present an alert, I did this:
let alertControl = AlertController.shared.alert(alertTitle: "Error Login", msg: "Please provide your email and password to login", style: .cancel, titleForAlertBtn: "OK")
present(alertControl, animated: true, completion: nil)

Related

Passing the function of the UIAlertAction to the UIAlertController extension

I want to have a base UIAlertController and I want to use it in different classes by just passing the buttons and their closures. To achieve this, I created an extension from UIAlertController like below:
extension UIAlertController {
func showAlert(buttons: [ButtonsAction]?) -> UIAlertController {
let alert = self
guard let alertButtons = buttons else {
return alert
}
for button in alertButtons {
let alertAction = UIAlertAction(title: button.title, style: button.style, handler: {action in
button.handler()
})
alert.addAction(alertAction)
}
return alert
}
}
for my buttons I have a struct:
struct ButtonsAction {
let title: String!
let style: UIAlertAction.Style
let handler: () -> Void
}
In one of my viewControllers I have a function which shows the alert. In that function I have a title and a message then I want to have 1 button to dismiss the alert. The function is something like this:
func fetchFaild(title: String, message: String) {
let buttons = ButtonsAction.init(title: "cancel", style: .cancel, handler: {action in
//here I want to dissmiss the alert I dont know how
})
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert).showAlert(buttons: buttons)
alert.show(self, sender: nil)
}
I have problem adding buttons to the Alert and I don't know how to add actions to the buttons.
I know this is not the best practice here. If any one knows any example or any tutorial that can help me achieve this I really appreciate it.
An extension of UIViewController might be a more reasonable solution and the ButtonsAction struct seems to be redundant.
extension UIViewController {
func showAlert(title: String, message: String, actions: [UIAlertAction], completion: (() -> Void)? = nil) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
actions.forEach{alertController.addAction($0)}
self.present(alertController, animated: true, completion: completion)
}
}
class MyController : UIViewController {
func fetchFailed(title: String, message: String) {
let actions = [UIAlertAction(title: "Cancel", style: .cancel, handler: { (action) in
print("Cancel tapped")
})]
showAlert(title: title, message: message, actions: actions)
}
}

How to add a function as a parameter in a custom function - swift?

I am trying to refactor my UIAlertViewController and pass a function to be executed when a user chooses to tap on one of two options which triggers an action.
My question is how do I go about adding a function as a parameter to a custom function? My effort is below. It is incomplete but any guidance would be much appreciated. I would like to have the function 'performNetworkTasl' as a parameter to 'showBasicAlert'.
import Foundation
import UIKit
struct Alerts {
static func showBasicAlert(on vc: UIViewController, with title: String, message: String, function: ?????){
let alert = UIAlertController.init(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction.init(title: "OK", style: .default) { (UIActionAlert) in
performNetworkTasl()
vc.dismiss(animated: true, completion: nil)
}
alert.addAction(okAction)
}
}
func performNetworkTasl(){
// DO SOME NETWORK TASK
}
You wouldnt pass a function as such, rather would pass a closure as a argument. Functions in swift are special cases of closure. Closure can be assumed to be a anonymous function. Closures,instant methods and static methods they all vary only in their context capturing ability other than their obvious syntactic differences.
struct Alerts {
static func showBasicAlert(on vc: UIViewController, with title: String, message: String, okAction: #escaping (() -> ())){
let alert = UIAlertController.init(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction.init(title: "OK", style: .default) { (UIActionAlert) in
okAction()
//dismiss statement below is unnecessary
vc.dismiss(animated: true, completion: nil)
}
alert.addAction(okAction)
}
}
And you call the function as
Alerts.showBasicAlert(on: your_viewController, with: "abcd", message: "abcd", okAction: {
//do whatever you wanna do here
})
Hope this helps
BTW, you dont have to have a explicit vc.dismiss(animated: true, completion: nil) as a last statement in any action, once the action is triggered, UIAlertController is dismissed by default

Attempting to create an object to act as an UIAlertController wrapper

I am currently trying to create a class that will simplify the process of defining an UIAlert.
As the traditional way of initializing an alert is
let alert = UIAlertController(title:"hello world", message: "how are you",preferedStyle: .actionSheet)
let ok = UIAlertAction(title:"ok",style:.default,handler: {(action) -> Void in print("ok")})
alert.addAction(ok)
self.presentViewController(alert,animated:true,completion:nil)
However, as i am going to be having the same format of alert in alot of places through out my app, I was thinking of making a single class that contains my entire alert object with all actions added so i can simply do:
let alert = MyAlert()
self.presentViewController(alert,animated:true,completion:nil)
I have
class myAlert: UIAlertController{
init() {
super.init(title:"hello world", message: "how are you", preferredStyle: .actionSheet)
}
}
But I seem to be getting an error "Must call a designated initliazer of the superclass 'UIAlertController'
Can you explain to me what I am doing wrong and send me in the right direction. I am fairly new to swift so any help is much appreciated. Thank you
You could just create an extension and whenever you want to display a UIAlertController, just call the method. With an extension, it can be used throughout your app.
extension UIViewController {
func displayMessageAlert(withAlertTitle alertTitle: String, andMessage message: String) {
let alert = UIAlertController(title: alertTitle, message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .destructive, handler: nil)
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
}
}
Usage on a UIViewController:
self.displayMessageAlert(withAlertTitle: "Your Title", andMessage: "Display your message here.")

How can I set accessibilityIdentifier to UIAlertController?

This is how I simply create UIAlertController and present it on the screen:
private class func showAlertWithTitle(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert)
//alert.accessibilityLabel = "my string here" //doesnt work
let action = UIAlertAction(title: "OK", style: .Default) { action in
alert.dismissViewControllerAnimated(true, completion: nil)
}
alert.addAction(action)
UIStoryboard.topViewController()?.presentViewController(alert, animated: true, completion: nil)
}
and this is how I access it under UITests:
emailAlert = app.alerts["First Name"] //for title "First Name"
but I would like to set there custom identifier and access this by firstName like this:
emailAlert = app.alerts["firstName"]
Is it possible?
This is an old thread but someone might use this.
I was able to set the accessibility identifier like this:
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.view.accessibilityIdentifier = "custom_alert"
alert.view.accessibilityValue = "\(title)-\(message)"
alert.addAction(
UIAlertAction(
title: "ALERT_BUTTON_OK".localized,
style: .default,
handler: handler
)
)
present(alert, animated: true)
That way I can access the alert by accessibility identifier and check its contents in accessibility value.
It is not perfect of course, but it works - at least for my testing using Appium.
The only way I figured out to do this was to use Apple's private APIs. You call valueForKey on the UIAlertAction object with this super secret key: "__representer" to get whats called a _UIAlertControllerActionView.
let alertView = UIAlertController(title: "This is Alert!", message: "This is a message!", preferredStyle: .Alert)
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertView.addAction(okAction)
self.presentViewController(alertView, animated: true, completion: {
let alertButton = action.valueForKey("__representer")
let view = alertButton as? UIView
view?.accessibilityIdentifier = "okAction_AID"
})
This has to be done in the completion handler because that that _UIAlertControllerActionView won't exist until the view is presented. On a side note in my project I used these following extensions to make things easier / more readable:
extension UIAlertController {
func applyAccessibilityIdentifiers()
{
for action in actions
{
let label = action.valueForKey("__representer")
let view = label as? UIView
view?.accessibilityIdentifier = action.getAcAccessibilityIdentifier()
}
}
}
extension UIAlertAction
{
private struct AssociatedKeys {
static var AccessabilityIdentifier = "nsh_AccesabilityIdentifier"
}
func setAccessibilityIdentifier(accessabilityIdentifier: String)
{
objc_setAssociatedObject(self, &AssociatedKeys.AccessabilityIdentifier, accessabilityIdentifier, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
func getAcAccessibilityIdentifier() -> String?
{
return objc_getAssociatedObject(self, &AssociatedKeys.AccessabilityIdentifier) as? String
}
}
So the above code would be rewritten:
let alertView = UIAlertController(title: NSLocalizedString("NMN_LOGINPAGECONTROLLER_ERROR_TITLE", comment: ""), message: message as String, preferredStyle:.Alert)
let okAction = UIAlertAction(title: NSLocalizedString("NMN_OK", comment: ""), style: .Default, handler: nil)
okAction.setAccessibilityIdentifier(InvalidLoginAlertView_AID)
alertView.addAction(okAction)
self.presentViewController(alertView, animated: true, completion: {
alertView.applyAccessibilityIdentifiers()
})
My first attempt involved trying to navigate the view hierarchy but that became difficult since UIAlertControllerActionView was not a part of the public API. Anyway I'd probably would try to ifdef out the valueForKey("__representer") for builds submitted for the app store or Apple might give you a spanking.
Right now I have a UIAlertAction called addCamera and I'm just doing:
addCamera.accessibilityLabel = "camera-autocomplete-action-photo"
That allows me to tap it in UI Tests as follows:
app.sheets.buttons["camera-autocomplete-action-photo"].firstMatch.tap()
From Apple docs...
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/UIKitUICatalog/UIAlertView.html
Making Alert Views Accessible
Alert views are accessible by default.
Accessibility for alert views pertains to the alert title, alert message, and button titles. If VoiceOver is activated, it speaks the word “alert” when an alert is shown, then speaks its title followed by its message if set. As the user taps a button, VoiceOver speaks its title and the word “button.” As the user taps a text field, VoiceOver speaks its value and “text field” or “secure text field.”

Placing common methods in a separate file using Swift

I like to place often used methods in a separate file. I found this answer Use function from one class in another class in Swift but I get errors using it the way I want.
Say i want to create a method called msgBox that pops up an alert box.
I created a separate empty Swift file and place this code in it.
import UIKit
class Utils: UIViewController {
class func msgBox (titleStr:String = "Untitled", messageStr:String = "Alert text", buttonStr:String = "Ok") {
var alert = UIAlertController(title: titleStr, message: messageStr, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: buttonStr, style: .Default, handler: { (action) -> Void in
self.dismissViewControllerAnimated(true, completion: nil)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
}
I want to call it like this, but I get errors doing so. Does anyone know what I'm doing wrong?
Utils.msgBox(titleStr: "Hello!", messageStr: "Are you sure?")
The error looks like this:
The error is because you are using self in a class method. There is no self instance, in this case.
One thing you could do in this situation is make a class extension. In the following example, you would be able to call the alert method from any UIViewController instance:
extension UIViewController {
func alert(title: String?, message: String?, buttonTitle: String = "OK") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: buttonTitle, style: .Default, handler: { action in
self.dismissViewControllerAnimated(true, completion: nil)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
}
Notice that I changed a couple names and types, but you can use what you like.

Resources