Pass "function with parameter" as parameter - swift

This doesn't compile
func showAlert(_ title: String, message: String,
onOk: (()->())? = nil,
onAnotherAction:((anotherActionTitle : String)-> Void)? = nil) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default) { (action) in
onOk?()
}
let anotherAction = UIAlertAction(title: anotherActionTitle, style: .default) { (action) in
onAnotherAction?()
}
alertController.addAction(ok)
alertController.addAction(anotherAction)
...
}
This compiles
func showAlert(_ title: String, message: String,
onOk: (()->())? = nil,
onAnotherAction:((String)-> Void)? = nil)
However, I have to declare another parameter for the title anotherActionTitle of onAnotherAction().
Is there a way make the first approach work? Thanks!

However, I have to declare another parameter for the title anotherActionTitle of onAnotherAction()
No, you don't have to do that. Just make it a normal parameter of the function as a whole:
func showAlert(_ title: String, message: String,
onOk: (()->())? = nil,
anotherActionTitle: String? = nil,
onAnotherAction: (()->())? = nil) {
The rest of your function will then compile and work correctly.

Since the implementation of SE-0111 as part of Swift 3, it is no longer possible to have named parameters for closure types.
There is a conceptual roadmap that the Swift core team has laid out for restoring named closure parameters at some point in the future, but no timeline for implementation that I am aware of:
https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160711/024331.html

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.

Testing if UIAlertController has been presented

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.

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)

error cannot convert value of type `Void` to expected argument type `() -> Void` UIAlertController

Very often I have to show an alert to the user and I find myself writing the same code over and over again, so I built a convenience method.
When self.convenience.showAlertToUser() is called in viewDidLoad I get error for argument doThisAction cannot convert value of type Void to expected argument type () -> Void. I don't understand why, because I pass in an argument of that type. Also, I don't know if I am creating a retain cycle, so I would appreciate your help.
class ConvenienceMethods {
func showAlertToUser(alertMessage: String = "",actionOkTitle:String, actionCancelTitle:String, controller: UIViewController, cancelAction: Bool, doAction: #escaping (() -> Void)) {
let customAlert = UIAlertController(title: "", message: alertMessage, preferredStyle: .alert)
let actionCancel = UIAlertAction(title: actionCancelTitle, style: .cancel, handler: nil)
let actionOk = UIAlertAction(title: actionOkTitle, style: .default, handler: { (action: UIAlertAction) in
doAction()
})
if cancelAction == true {
customAlert.addAction(actionCancel)
}
customAlert.addAction(actionOk)
controller.present(customAlert, animated: true, completion: nil)
}
}
class ManageFeedbackTableViewController {
let convenience = ConvenienceMethods()
override func viewDidLoad() {
super.viewDidLoad()
let doThisAction = self.segueWith(id: "segueID")
self.convenience.showAlertToUser(alertMessage: "someMessage", actionOkTitle: "OK", actionCancelTitle: "No", controller: self, cancelAction: false, doAction: doThisAction)
}
//perform an action
func segueWith(id: String) -> Void{
self.performSegue(withIdentifier: id, sender: self)
}
}
Because you are to passing a reference to the function but the result itself.
Replace
let doThisAction = self.segueWith(id: "segueID")
By :
let doThisAction = { self.segueWith(id: "segueID") }
#bibscy,
doThisAction is a closure to which we can assign a block of code within "{ }" as follows:-
let doThisAction = { self.segueWith(id: "segueID") } which will work.

Can't give parameter values to extension method UIAlertController

I am trying to write an extension method that generates an UIAlertController based on what you give as a parameter.
extension UIAlertController {
func generate(messageText: String, messageTitle: String, buttonText: String) {
let alert = UIAlertController(title: messageTitle, message: messageText, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: buttonText, style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
}
This doesn't give an error.
But when I try to call it and add the parameter values
var alertTest = UIAlertController.generate("")
It gives the following error: Type 'UIAlertController' does not conform to protocol 'StringLiteralConvertible'
How can I fix this?
Or isn't it possible what i am trying to achieve?
There are some differences between how you have defined the generate method in the extension and how you are using it.
generate is declared as a function accepting 3 string parameters and returning void.
You are calling as a static method instead, passing one parameter, and expecting a return value.
The right way of using it is like this:
var alert = UIAlertController()
alert.generate("A Message", messageTitle: "A title", buttonText: "A button label")
However I think that the method implementation is incorrect, because you are creating a new instance of UIAlertController instead of (re)using the one the method is called on. What you probably need is a static method, in which case it should look like:
extension UIAlertController {
class func generate(# parent: UIViewController, messageText: String, messageTitle: String, buttonText: String) -> UIAlertController {
let alert = UIAlertController(title: messageTitle, message: messageText, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: buttonText, style: UIAlertActionStyle.Default, handler: nil))
parent.presentViewController(alert, animated: true, completion: nil)
return alert
}
}
and used as:
var alert = UIAlertController.generate(parent: self, messageText: "A Message", messageTitle: "A title", buttonText: "A button label")