Testing if UIAlertController has been presented - swift

I have a protocol I use to allow my ViewControllers to present an alert.
import UIKit
struct AlertableAction {
var title: String
var style: UIAlertAction.Style
var result: Bool
}
protocol Alertable {
func presentAlert(title: String?, message: String?, actions: [AlertableAction], completion: ((Bool) -> Void)?)
}
extension Alertable where Self: UIViewController {
func presentAlert(title: String?, message: String?, actions: [AlertableAction], completion: ((Bool) -> Void)?) {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
actions.forEach { action in
alertController.addAction(UIAlertAction(title: action.title, style: action.style, handler: { _ in completion?(action.result) }))
}
present(alertController, animated: true, completion: nil)
}
}
I call this something like
#objc private func didTapLogout() {
presentAlert(
title: nil, message: "Are you sure you want to logout?",
actions: [
AlertableAction(title: "No", style: .cancel, result: false),
AlertableAction(title: "Yes", style: .destructive, result: true),
],
completion: { [weak self] result in
guard result else { return }
self?.presenter.logout()
}
)
}
I'd like to write a unit test to assert when this is called, the presented view controller is UIAlertController.
I was trying something like, but it does not pass
func test_renders_alert_controller() {
sut.show()
XCTAssertNotNil(sut.presentedViewController)
}
class MockViewController: UIViewController, Alertable {
var presentViewControllerTarget: UIViewController?
func show() {
presentAlert(title: nil, message: "Are you sure you want to logout?", actions:
[AlertableAction(title: "No", style: .cancel, result: false)],
completion: nil
)
self.presentViewControllerTarget = self.presentedViewController
}
}

You need to wait for the UIAlertController to be fully visible before running your assertion.
Check out XCTWaiter.
Try something like the below:
let nav = UINavigationController.init(rootViewController: sut)
sut.show()
let exp = expectation(description: "Test after 1.5 second wait")
let result = XCTWaiter.wait(for: [exp], timeout: 1.5)
if result == XCTWaiter.Result.timedOut {
XCTAssertNotNil(nav.visibleViewController is UIAlertController)
} else {
XCTFail("Delay interrupted")
}

ViewControllerPresentationSpy avoids slow, flaky unit tests by capturing the information that would be used to present an alert, without actually presenting any alerts. All you need is to create an AlertVerifier, then call whatever presents your alert:
let alertVerifier = AlertVerifier()
sut.show()
alertVerifier.verify(
title: nil,
message: "Are you sure you want to logout?",
animated: true,
presentingViewController: sut,
actions: [
.cancel("No"),
.destructive("Yes"),
]
)
This verify method checks:
That one alert was presented, with animation.
That the presenting view controller was the System Under Test.
The alert title.
The alert message.
The preferred style of UIAlertController.Style (.alert by default)
The titles and styles of each action.
You can invoke each action by name:
try alertVerifier.executeAction(forButton: "Yes")
(Mark the test as throws. The test will fail if there is no button with the given name.)
Try it to see how fast it is compared to the 1.5 second timeout. Also compare how much you can test.

Related

Swift: How to call two times the same alert from another alert?

I have simplified my problem to the following code where the same behavior occurs.
What I want to do is calling a method which includes an alert (messageWindow() in my code) two times which means one behind the other. I want to call that method from another method which also includes an alert (userInput()) in my code.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
userInput()
}
func userInput() {
let alert = UIAlertController(
title: "Welcome",
message: "Do you want say hello?",
preferredStyle: .alert)
let actionYes = UIAlertAction(
title: "Yes",
style: .default) {_ in
print("hello")
self.messageWindow(title: "1st call", message: "Hello!")
self.messageWindow(title: "2nd call", message: "Hello!!")
}
let actionNo = UIAlertAction(
title: "No",
style: .default) { (action) in }
alert.addAction(actionYes)
alert.addAction(actionNo)
self.present(alert, animated: true)
}
func messageWindow (title: String, message: String) {
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: .alert)
let actionOk = UIAlertAction(title: "OK", style: .default) { (action) in }
alert.addAction(actionOk)
self.present(alert, animated: true)
}
}
My problem is that the second call won't be executed (code snippet below). That means I don't see a window popping up like in the first call.
self.messageWindow(title: "2nd call", message: "Hello!!")
I'm relative new in coding with Swift. Please excuse my question in case it is a really simple one. I didn't found anything which helped me solving this problem.
I appreciate your help.
Thanks.

How to create a reusable UIAlert ActionSheet as an UIViewController extension?

I would like to create an action sheet that can be used several time in my code. To do so, I need to be able to use functions according to the action sheet title. Is there a way to pass functions as a parameter array like the "title" parameter?
//MARK: - UIAlert action sheet title
enum ActionSheetLabel: String {
case camera = "Camera"
case photoLibrary = "Album"
case cancel = "Cancel"
}
class CameraHandler {
static let cameraHandler = CameraHandler()
func openCamera() { }
func openPhotoLibrary() { }
}
//MARK: - Alert that shows an action sheet with cancel
extension UIViewController {
func showActionSheetWithCancel(vc: UIViewController, title: [ActionSheetLabel] /*Make a function parameter here to match title*/) {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
for value in title {
actionSheet.addAction(UIAlertAction(title: value.rawValue, style: .default, handler: {
(alert: UIAlertAction!) -> Void in
//Use the parameter function here to match title
}))
}
actionSheet.addAction(UIAlertAction(title: ActionSheetLabel.cancel.rawValue, style: .cancel, handler: nil))
vc.present(actionSheet, animated: true, completion: nil)
}
}
For UIAlert you just need to change preferredStyle .alert it and it's working for UIAlert And and below code just copy and paste it working for UIActionSheet.
extension UIViewController {
func popupAlert(title: String?, message: String?, actionTitles:[String?], actionStyle:[UIAlertAction.Style], actions:[((UIAlertAction) -> Void)?]) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
for (index, title) in actionTitles.enumerated() {
let action = UIAlertAction(title: title, style: actionStyle[index], handler: actions[index])
alert.addAction(action)
}
self.present(alert, animated: true, completion: nil)
}
}
Check below code For Usage
self.popupAlert(title: "Alert"), message: “Error in Loading”, actionTitles: ["Okey", "Email"], actionStyle: [.default, .default], actions: [nil,{ action in
// I have set nil for first button click
// do your code for second button click
}])
if you have any query then please comment me. Thank You
I have find out the best way to add an action sheet with cancel and as much action as needed.
Create an UIViewController extension with type alias:
//MARK: - Alert that shows an action sheet with cancel
extension UIViewController {
typealias AlertAction = () -> ()
typealias AlertButtonAction = (ActionSheetLabel, AlertAction)
func showActionSheetWithCancel(titleAndAction: [AlertButtonAction]) {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
for value in titleAndAction {
actionSheet.addAction(UIAlertAction(title: value.0.rawValue, style: .default, handler: {
(alert: UIAlertAction!) -> Void in
value.1()
}))
}
actionSheet.addAction(UIAlertAction(title: ActionSheetLabel.cancel.rawValue, style: .cancel, handler: nil))
self.present(actionSheet, animated: true, completion: nil)
}
}
Then, in the class or other place where you want to use it, add the method this way:
//MARK: - UIAlert action sheet title
enum ActionSheetLabel: String {
case camera = "Camera"
case photoLibrary = "Album"
case cancel = "Cancel"
}
//MARK: - Class example where to use the action sheet action
class CameraHandler {
fileprivate let currentVC: UIViewController!
func openCamera() {
// Open user camera
}
func openPhotoLibrary() {
// Open user photo library
}
// Method example of this action sheet
func showActionSheetWithCameraAndLibrary(vc: UIViewController) {
//This is the way to use the extension
vc.showActionSheetWithCancel(titleAndAction: [
(ActionSheetLabel.camera, { [weak self] in self?.openCamera() }),
(ActionSheetLabel.photoLibrary, { [weak self] in self?.openPhotoLibrary() })
])
}
}
You can pass a closure and call it in the handler something like this should work.
Also not sure why you were passing the UIViewController , as you're already defining the function in a extension UIViewController therefore i allowed my self to remove it and used self.present instead .
extension UIViewController {
func showActionSheetWithCancel(title: [ActionSheetLabel], action: #escaping () -> ()?) {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
for value in title {
actionSheet.addAction(UIAlertAction(title: value.rawValue, style: .default, handler: {
(alert: UIAlertAction!) -> Void in
// action
action()
}))
}
let alertAction = UIAlertAction(title: ActionSheetLabel.cancel.rawValue, style: .cancel) { (_) in
action() // or for cancel call it here
}
actionSheet.addAction(alertAction)
self.present(actionSheet, animated: true, completion: nil)
}
}
As you can see #escaping () -> ()? is optional so you can pass nil too .
from what I understood you need to call a specific functions when the title of the alert changes & also you want to be able to do so from different viewControllers,
I hope this will help
extension UIViewController {
func showActionSheetWithCancel(vc: UIViewController, title: [ActionSheetLabel] ) {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let cameraHandler = CameraHandler()
for value in title {
switch value.rawValue {
case ActionSheetLabel.camera.rawValue:
actionSheet.addAction(UIAlertAction(title: ActionSheetLabel.camera.rawValue, style: .default, handler: { (alert) in
cameraHandler.openCamera()
}))
case ActionSheetLabel.photoLibrary.rawValue:
actionSheet.addAction(UIAlertAction(title: ActionSheetLabel.photoLibrary.rawValue, style: .default, handler: { (alert) in
cameraHandler.openPhotoLibrary()
}))
default:
actionSheet.addAction(UIAlertAction(title: ActionSheetLabel.cancel.rawValue, style: .cancel, handler: nil))
}
vc.present(actionSheet, animated: true, completion: nil)
}
}
}
and the call of the function will be like this:
showActionSheetWithCancel(vc: self, title: [UIViewController.ActionSheetLabel.camera])

Swift creating a function that runs another function that was implemented while called.(Not that complex)

Hello i am trying to create a kickass function to show alerts and run it's function. Buuut unfortunately Xcode and i am getting confused in here:
buttonAction:Array<(Any) -> Any)>
Expected '>' to complete generic argument list
func callAlert(_ view: UIViewController, title:String, message:String, buttonName:Array<String>, buttonAction:Array<(Any) -> Any)>) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
for index in 0..<buttonName.count{
alert.addAction(UIAlertAction(title: buttonName[index], style: .default, handler: { action in
switch action.style{
case .default:
print("default")
buttonAction()
case .cancel:
print("cancel")
case .destructive:
print("destructive")
}}))}
view.present(alert, animated: true, completion: nil)
}
How do i call function? Please check below:
callAlert(self,
title: "Donate type",
message: "Thanks for your support!",
buttonName: ["Buy me a coffee!","Something"]
)
First of all I highly recommend to implement the method as an extension of UIViewController.
Second of all I'd prefer presentAlert() over callAlert()
Third of all rather than two arrays for buttons and actions use one array of tuples for title, style and action.
By the way unspecified type (Any) -> Any is very, very bad because UIAlertAction handlers are clearly ((UIAlertAction) -> Void)?
Finally add an optional completion handler
extension UIViewController {
func presentAlert(title: String,
message: String,
alertActions: [(title: String, style: UIAlertAction.Style, action: ((UIAlertAction) -> Void)?)],
completion: (() -> Void)? = nil) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
for action in alertActions {
alert.addAction(UIAlertAction(title: action.title, style: action.style, handler: action.action))
}
self.present(alert, animated: true, completion: completion)
}
}
And use it inside an UIViewController
let buyCoffeeAction : (UIAlertAction) -> Void = { action in
// do something
}
let somethingAction : (UIAlertAction) -> Void = { action in
// do something
}
presentAlert(title: "Donate type",
message: "Thanks for your support!",
alertActions: [(title: "Buy me a coffee!", style: .default, action: buyCoffeeAction),
(title: "Something", style: .destructive, action: somethingAction)],
completion: nil)

use same UIAlertController in different ViewControllers

I have used side navigation menu(SWReveal). I have 4 ViewControllers. How can use same alertAction in different views.
You can create UIViewController extension like below:
extension UIViewController {
func showAlert(title: String?, message: String?, actionTitles:[String?], actions:[((UIAlertAction) -> Void)?]) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
for (index, title) in actionTitles.enumerated() {
let action = UIAlertAction(title: title, style: .default, handler: actions[index])
alert.addAction(action)
}
self.present(alert, animated: true, completion: nil)
}
}
And you can use this alert in UIViewController like below:
showAlert(title: "Your Title", message: "Your custom Message", actionTitles: ["Ok","Cancel"], actions: [{ action1 in
//OK Action
}, { action2 in
// Cancel Action
}
])
Hope will get your solution.
You can also use like this way.
class IOSPublicDefaultAlert: NSObject{
var viewController: UIViewController?
var actionCompletion: ((String) -> ())?
var alertTitle: String?
var alertMessage : String?
var alertType: UIAlertControllerStyle?
var actionTitleAndType: [String: UIAlertActionStyle]?
init(viewController : UIViewController,alertTitle: String?,alertMessage : String?,alertType: UIAlertControllerStyle = .alert,actionTitleAndType: [String: UIAlertActionStyle] ,actionCompletion : ((String)->())?){
super.init()
self.viewController = viewController
self.actionCompletion = actionCompletion
self.alertTitle = alertTitle
self.alertMessage = alertMessage
self.alertType = alertType
self.actionTitleAndType = actionTitleAndType
showAlert()
}
func showAlert(){
let alert = UIAlertController.init(title: alertTitle, message: alertMessage, preferredStyle: self.alertType ?? .alert)
for (actionTitle, actionType) in actionTitleAndType!{
let action = UIAlertAction(title: actionTitle, style: actionType) { (action) in
if let com = self.actionCompletion{
com(actionTitle)
}
}
alert.addAction(action)
}
viewController?.present(alert, animated: true, completion: nil)
}
}
and use add where you like as below sample
_ = IOSPublicDefaultAlert.init(viewController: self, alertTitle: "Warning!!!", alertMessage: alertMessage, actionTitleAndType: ["Ok" : .destructive, "Cancel" : .default], actionCompletion: { [unowned self] (title) in
if title == "Ok"{
}
})
In swift, your project, you can create a new .swift file and in this file create a class:
import UIKit
import Foundation
class yourFileName {
//Create a class function alerview
class func displayAlert(title: String, withMessage msg: String, andbtnTitle btntitle: String, in vc: UIViewController) {
let alert = UIAlertController(title: title, message: msg, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: btntitle, style: UIAlertActionStyle.default, handler: nil))
appDelegate.window?.rootViewController?.present(alert, animated: true, completion: nil)
}
}
//and now your any ViewController.swift file or any other file in your project you can access alert following way.
class viewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
yourfilename.displayAlert(title: "Alert", withMessage msg: "my alert view display", andbtnTitle btntitle: "Ok", in vc: self) // access your alertview
}
}
I hope it's work for you.
Create BaseController with a method that can show alert.
//Copyright © 2017 dip. All rights reserved.
import UIKit
class BaseController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
///This is common method to show alert with same action
func showAlert() {
let alert = UIAlertController(title: "Alert", message: "my msg on alert", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action) in
///This will be common alert ok aciton for all child controllers.
print("Do some userful common work..")
}))
self.present(alert, animated: true, completion: nil)
}
}
Inherit Your 4 controllers from BaseController
// Copyright © 2017 dip. All rights reserved.
//
import UIKit
class ChildVC: BaseController {
override func viewDidLoad() {
super.viewDidLoad()
//call show alert when ever you wish
///This method will call showAlert() method on super class (BaseController)
self.showAlert()
}
}
Call self.showAlert() method from child when you want show alert with common action.
// MARK: - Alertable View
protocol AlertableView {
// Use handler if need catch cancel alert action
typealias CompletionHandler = (() -> Void)
func displayAlert(with title: String, message: String, actions: [UIAlertAction]?)
func displayAlert(with title: String, message: String, style: UIAlertControllerStyle, actions: [UIAlertAction]?, completion: CompletionHandler?)
}
extension AlertableView where Self: UIViewController {
func displayAlert(with title: String, message: String, actions: [UIAlertAction]?) {
self.displayAlert(with: title, message: message, style: .alert, actions: actions, completion: nil)
}
func displayAlert(with title: String, message: String, style: UIAlertControllerStyle, actions: [UIAlertAction]?, completion: CompletionHandler?) {
let alertCancelAction = UIAlertAction(title: "Cancel".localized, style: .cancel) { (action) in
guard let completion = completion else { return }
completion()
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: style)
if let actions = actions {
for action in actions {
alertController.addAction(action)
}
alertController.addAction(alertCancelAction)
} else {
// If not any custom actions, we add OK alert button
let alertOkAction = UIAlertAction(title: "OK".localized, style: .cancel) { (action) in
guard let completion = completion else { return }
completion()
}
alertController.addAction(alertOkAction)
}
self.present(alertController, animated: true, completion: nil)
}
}
Create a common function ,
import UIKit
class AlertClass: NSObject {
func showAlertWithVC(_ VC : UIViewController, andMessage message: String ){
DispatchQueue.main.async {
let alert = UIAlertController(title: "APPLICATION_NAME", message: message , preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil))
VC.present(alert, animated: true, completion: nil)
}
}
}
Simply call AlertClass().showAlertWithVC() where you want to show Alert.

Call function in UIViewController from an extension

I have a UIViewController which loads up some json data from the server. If the server is down or the user has data turned off I throw up an alert telling the user such. This is done using a UIAlertController. This works great. So I put this into an extension since it is used by all UIViewControllers which need data. Now the UIAlertController has an action set as well
Alert code
extension UIViewController {
func connectionLost(){
var message = "Your device has lost connection to the server. Check that you have a valid internet connection and then retry."
let alertController = UIAlertController( title: "Connection Lost",
message: message,
preferredStyle: .alert)
let retryAction = UIAlertAction(title:"Retry", style: .default, handler: {
action in
//call function in the viewcontroller that raised this alert to reload the data
})
alertController.addAction(retryAction)
self.present(alertController, animated: true, completion: nil)
}
}
When the user taps the retry button I want to call a function in the uiviewcontroller that raised the alert.
I tried creating a delegate in the extension but struggled with getting it wired up like you do in a class. What sort of approaches are there to call a function from an extension in the viewcontroller that raised the alert?
You should create a BaseViewController and use Inheritance. It could be useful for other implementations too.
class BaseViewController: UIViewController {
func onRetryClick() {
// override to customize or write here the common behaviour
}
}
class FirstViewController: BaseViewController {
override func onRetryClick() {
// do something specific for FirstViewController
}
}
class SecondViewController: BaseViewController {
override func onRetryClick() {
// do something specific for SecondViewController
}
}
class ThirdViewController: BaseViewController {
// if you don't override this method, super class (BaseViewController) implementation will be executed
}
extension BaseViewController {
func connectionLost(){
var message = "Your device has lost connection to the server. Check that you have a valid internet connection and then retry."
let alertController = UIAlertController( title: "Connection Lost",
message: message,
preferredStyle: .alert)
let retryAction = UIAlertAction(title:"Retry", style: .default, handler: { action in
self.onRetryClick()
})
alertController.addAction(retryAction)
self.present(alertController, animated: true, completion: nil)
}
}
Hope this makes sense.
class MyVC: UIViewController {
func retry() {
}
func checkConnection() {
connectionLost { (retry) -> (Void) in
if retry {
self.retry()
}
}
}
}
extension UIViewController {
func connectionLost(completion: #escaping (_ retry: Bool) -> (Void)) {
let message = "Your device has lost connection to the server. Check that you have a valid internet connection and then retry."
let alertController = UIAlertController( title: "Connection Lost",
message: message,
preferredStyle: .alert)
let retryAction = UIAlertAction(title:"Retry", style: .default, handler: {
action in
completion(true)//may be 'false', you decide
})
alertController.addAction(retryAction)
self.present(alertController, animated: true, completion: nil)
}
}