There are a couple of existing questions on this topic but they aren't quite what I'm after. I've written a little Swift app rating prompt for my app which presents two UIAlertController instances, one triggered by the other.
I'm now trying to unit test this, and trying to reach that second alert in the tests. I've written a simple spy to check the first controller, but I'd like a way to trigger one of the actions on the first alert, which in turn shows the second.
I've already tried alert.actions.first?.accessibilityActivate(), but it didn't seem to break inside the handler of that action – that's what I'm after.
A solution that doesn't involve changing the production code to allow programmatic tapping of UIAlertActions in unit tests, which I found in this SO answer.
Posting it here as well as this question popped up for me when Googling for an answer, and the following solution took me way more time to find.
Put below extension in your test target:
extension UIAlertController {
typealias AlertHandler = #convention(block) (UIAlertAction) -> Void
func tapButton(atIndex index: Int) {
guard let block = actions[index].value(forKey: "handler") else { return }
let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self)
handler(actions[index])
}
}
Here's roughly what I did:
Created a mocked version of my class that would present the alert controller, and in my unit tests, used this mock.
Overrode the following method that I'd created in the non-mocked version:
func alertActionWithTitle(title: String?, style: UIAlertActionStyle, handler: Handler) -> UIAlertAction
In the overridden implementation, stored all the details about the actions in some properties (Handler is just a typealias'd () -> (UIAlertAction))
var didCreateAlert = false
var createdTitles: [String?] = []
var createdStyles: [UIAlertActionStyle?] = []
var createdHandlers: [Handler?] = []
var createdActions: [UIAlertAction?] = []
Then, when running my tests, to traverse the path through the alerts, I implemented a callHandlerAtIndex method to iterate through my handlers and execute the right one.
This means that my tests look something like this:
feedback.start()
feedback.callHandlerAtIndex(1) // First alert, second action
feedback.callHandlerAtIndex(2) // Second alert, third action
XCTAssertTrue(mockMailer.didCallMail)
I took a slightly different approach based on a tactic I took for testing UIContextualAction—it's very similar to UIAction but exposes its handler as a property (not sure why Apple wouldn't have done the same for UIAction). I injected an alert actions provider (encapsulated by a protocol) into my view controller. In production code, the former just vends the actions. In unit tests, I use a subclass of this provider which stores the action and the handler in two dictionaries—these can be queried and then triggered in tests.
typealias UIAlertActionHandler = (UIAlertAction) -> Void
protocol UIAlertActionProviderType {
func makeAlertAction(type: UIAlertActionProvider.ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction
}
Concrete object (has typed titles for easy retrieval later):
class UIAlertActionProvider: UIAlertActionProviderType {
enum ActionTitle: String {
case proceed = "Proceed"
case cancel = "Cancel"
}
func makeAlertAction(title: ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction {
let style: UIAlertAction.Style
switch title {
case .proceed: style = .destructive
case .cancel: style = .cancel
}
return UIAlertAction(title: title.rawValue, style: style, handler: handler)
}
}
Unit testing subclass (stores actions and handlers keyed by ActionTitle enum):
class MockUIAlertActionProvider: UIAlertActionProvider {
var handlers: [ActionTitle: UIAlertActionHandler] = [:]
var actions: [ActionTitle: UIAlertAction] = [:]
override func makeAlertAction(title: ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction {
handlers[title] = handler
let action = super.makeAlertAction(title: title, handler: handler)
actions[title] = action
return action
}
}
Extension on UIAlertAction to enable typed action title lookup in tests:
extension UIAlertAction {
var typedTitle: UIAlertActionProvider.ActionTitle? {
guard let title = title else { return nil }
return UIAlertActionProvider.ActionTitle(rawValue: title)
}
}
Sample test demonstrating usage:
func testDeleteHandlerActionSideEffectTakesPlace() throws {
let alertActionProvider = MockUIAlertActionProvider()
let sut = MyViewController(alertActionProvider: alertActionProvider)
// Do whatever you need to do to get alert presented, then retrieve action and handler
let action = try XCTUnwrap(alertActionProvider.actions[.proceed])
let handler = try XCTUnwrap(alertActionProvider.handlers[.proceed])
handler(action)
// Assert whatever side effects are triggered in your code by triggering handler
}
I used Luke's guidance above to create a subclass of UIAlertAction that saves its completion block so it can be called during tests:
class BSAlertAction: UIAlertAction {
var completionHandler: ((UIAlertAction) -> Swift.Void)?
class func handlerSavingAlertAction(title: String?,
style: UIAlertActionStyle,
completionHandler: #escaping ((UIAlertAction) -> Swift.Void)) -> BSAlertAction {
let alertAction = self.init(title: title, style: style, handler: completionHandler)
alertAction.completionHandler = completionHandler
return alertAction
}
}
You could customize this to save more information (like the title and the style) if you like. Here's an example of an XCTest that then uses this implementation:
func testThatMyMethodGetsCalled() {
if let alert = self.viewController?.presentedViewController as? UIAlertController,
let action = alert.actions[0] as? BSAlertAction,
let handler = action.completionHandler {
handler(action)
let calledMyMethod = self.presenter?.callTrace.contains(.myMethod) ?? false
XCTAssertTrue(calledMyMethod)
} else {
XCTFail("Got wrong kind of alert when verifying that my method got called“)
}
}
Related
Is it possible, with a UIAlert, to return what the user selected to a delegate method? If so, how?
I'd prefer not to change the delegate method or stop using UIAlert, if such a solution exists. Though all help and ideas are appreciated.
Delegate Protocol
protocol RouteManagerDelegate {
//behaves like textFieldShouldEndEditing
func routeShouldUpdateUnfinished() -> Bool
}
My Current Attempt at Implementation
extension MyController : RouteManagerDelegate {
func routeShouldUpdateUnfinished() -> Bool {
var response = false
//make Alert
let confirmationAlert = UIAlertController(title: "Current Route is Unfinished", message: "Do you want to continue?", preferredStyle: .alert)
//add Yes or No options
confirmationAlert.addAction(UIAlertAction(title: "Yes", style: .destructive) { _ in
response = true
})
confirmationAlert.addAction(UIAlertAction(title: "No", style: .cancel)
self.present(confirmationAlert, animated: true)
return response
}
}
This doesn't work because UIAlert behaves asynchronously. The function simply returns false every time.
But you can clearly see the intent:
If the user selects 'Yes' (continue), then routeShouldUpdateUnfinished should return true.
For me the provided code looks like the delegate method is being understood wrong. To help you out a little, here a little diagram:
The delegate pattern is used to communicate from child to parent.
In your solution this could look like:
Parent implements delegate pattern with function
func routeShouldUpdateUnfinished(result: bool) { ... }
When parent sets child up the parent calls
child.delegate = self
The alert inside child calls
delegate.routeShouldUpdateUnfinished(true)
The parent handles the code
Just in case someone finds themselves in this esoteric position:
I wanted to do as Ramden suggested, but also didn't want to expose a function that other classes should never use (except in this scenario).
I ended up using a default implementation for my delegate method (defined in an extension of my protocol) and a static fileprivate method to handle the result of my delegate method returning true.
Now the fileprivate 'handler' method isn't exposed but I can also use a UIAlert. This has flaws but works. Implementation below, if ever it's useful to someone.
protocol RouteManagerDelegate {
func routeShouldUpdateUnfinished() -> Bool
}
extension RouteManagerDelegate { //default implementations of delegate protocol
func routeShouldUpdateUnfinished() -> Bool {
if let delegate = self as? UIViewController {
let alert = UIAlertController(title: "Wait!", message: "Do you want to continue?", preferredStyle: .alert)
confirmationAlert.addAction(UIAlertAction(title: "Yes", style: .destructive) { _ in
RouteManager.handleRouteChangedUnfinished() //the 'handler' function
})
confirmationAlert.addAction(UIAlertAction(title: "No", style: .cancel))
delegate.present(confirmationAlert, animated: true)
}
return false
}
}
struct RouteManager {
func myFunc() {
...
if delegate?.routeShouldUpdateUnfinished() ?? true {
RouteManager.handleRouteChangedUnfinished() //the 'handler' function
}
...
}
static fileprivate func handleRouteChangedUnfinished() { //don't want to expose this
//Notify the Database
}
}
I am creating an app with one of the tab being a guestbook where users can write something. I use an UIAlertController to pop up a window with a textfield for entering name, and a textview for entering the message. I want to make the "Post" button disabled by default, then enable it after the name field has at least 2 characters and the message has at least 3 characters. I have achieved this by
#1: declare the "Post" UIAlertAction at the top
let saveAction = UIAlertAction(title:"Post", style: .default, handler: { (action) -> Void in print(data) })
The above line gives the error (Cannot use instance member 'data' within property initializer; property initializers run before self is available.)
#2: add this "Post" button in alert and making it disabled
alert.addAction(saveAction)
saveAction.isEnabled = false
#3 add two functions to detect how many words are in the textfield and textview, and if they meet the requirement, enable the "Post" button
func textViewDidChange(_ textView: UITextView) { //Handle the text changes here
GuestbookContentWordCount = textView.text.count
data["content"] = textView.text
enableSave()
}
#objc func textFieldDidChange(_ textField: UITextField) {
GuestbookNameWordCount = textField.text?.count ?? 0
data["name"] = textField.text
enableSave()
}
func enableSave () {
if GuestbookContentWordCount >= 3 && addGuestbookNameWordCount >= 2 {
saveAction.isEnabled = true
} else {
saveAction.isEnabled = false
}
}
The ideal situation is when the requirements are met and the user clicks on the "Post" button, I will get the data["name"] and data["content"] and insert it into a database. Right now I have gotten it to work to the point that the "Post" button is enabled after the requirements are met, but when trying to get the data it gives the error "Cannot use instance member 'data' within property initializer; property initializers run before self is available.
Can you please advise how to solve this problem? Thank you.
So this is what I would do
Change your UIAlertAction reference to this outside the viewDidLoad()
var saveAction: UIAlertAction? = nil
Inside the viewDidLoad() you can instantiate it like:
saveAction = UIAlertAction(title:"Post", style: .default, handler: { [weak self] (action) -> Void in
guard let `self` = self else { return }
print(self.data) }
)
[weak self] is used so that you don't end up having retain cycles after your UIViewController is deinitialised.
I am trying to call upon func to bring up an alert and it is inside another static func though it's not working
inside my fetch user func when actual longitude return nil i want to call the alert func though its not working as i thought it would
it says error: extra argument in call "message
func alertTheUser(title: String , message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: nil);
alert.addAction(ok);
present(alert, animated: true, completion: nil);
}
static func firestorefetchUserWithUID(uid: String, completion:#escaping (User2) -> ()) {
//code taken out for this example
//fetch user from database
//dictionary = snapshot etc
guard let latitude = dictionary["Actual Latitude"] as? String else {
alertTheUser(title:"title" , message:"message")
return
}
//code taken out for this example
}
A static func cannot call a non-static func directly. A non-static func is an instance method, and in a static func there is no instance — it is static, meaning it belongs to the type, not to an instance of the type.
Thus, static firestorefetchUserWithUID cannot call alertTheUser because alertTheUser is an instance method and you have no instance to send it to. And if alertTheUser were static, you'd have the same problem again, because then it could not call present for the same reason, as present is an instance method.
It looks to me like making the static func static was just a mistake to begin with; make it an instance method, if you know you'll always have an instance to send it to. (And I presume you do have an instance, because your use of present suggests that this code must be in a UIViewController subclass.)
I'm trying to create a dynamic menu where I send the text and the action of a button from one controller to the next controller and then I want to generate the button that runs the action, but I'm having trouble with the syntax.
So as an example in the first controller i have:
let vc = storyboard.instantiateViewController(withIdentifier:
"CompetitiveKPIChart") as! CompetitiveKPIChartViewController
vc.Menu = [["Menu1":ClickBack()],["Menu2":ClickBack()],["Menu3":ClickBack()]]
func ClickBack() {
self.dismiss(animated: true, completion: {});
}
and in the second controller:
var Menu : [Dictionary<String,()>] = []
override func viewDidLoad() {
let gesture2 = UITapGestureRecognizer(target: self,action: Menu["Menu1"])
btnBack.addGestureRecognizer(gesture2)
}
How can I call the ClickBack() from the first controller in the second controller GestureRecognizer?
The syntax of your Menu declaration is wrong - it should be () -> () for a void function, not ().
This works in Playground, your syntax does not...
var menu : [Dictionary<String,() -> ()>] = [] // Don't use capitals for variables...
func aFunc() {
print("Hello aFunc")
}
menu = [["aFunc": aFunc]]
menu.first!["aFunc"]!() // Prints "Hello aFunc"
However, I think there is a second problem with your code. Your line
UITapGestureRecognizer(target: self,action: Menu["Menu1"])
cannot compile, as Menu is an array, not a dictionary. I think you probably meant
var menu : Dictionary<String,() -> ()> = [:]
in which case your set-up code should say
vc.menu = ["Menu1":ClickBack, "Menu2":ClickBack, "Menu3":ClickBack]
However, there is a final, and probably insurmountable problem, which is that your code
override func viewDidLoad() {
let gesture2 = UITapGestureRecognizer(target: self, action: menu["Menu1"])
btnBack.addGestureRecognizer(gesture2)
}
will still give an error, since action: needs to be a Selector, which is an #objc message identifier, not a Swift function.
I'm afraid I can't see a good way to make this (rather clever) idea work. Time to refactor?
I want to pass a function to call inside the completion block if I need to, but I do not know how to set a default parameter for a function.
func showAlert(controllerTitle: String, message: String, actionTitle: String, preferredStyle: UIAlertControllerStyle = .alert, actionStyle: UIAlertActionStyle = .default, funcToCall: () -> ()){
let alert = UIAlertController(title: controllerTitle, message: message, preferredStyle: preferredStyle)
alert.addAction(UIAlertAction(title: actionTitle, style: actionStyle, handler: {(action) -> Void in
funcToCall()
}))
self.present(alert, animated: true, completion: {() -> Void in })
}
First of all, a question should provide a code block as specific as possible.
The question
So, you have a function foo which accepts a parameter of type closure and you want to provide a default value right?
The solution
Here's the code
func foo(completion: ()->() = { _ in print("Default completion") }) {
completion()
}
Testing
Now you can call foo passing your own closure
foo { print("Hello world") } // Hello world
Or using using the default param
foo() // Default completion
You can pass a default parameter to a function like so
func functionToCall(message: String = "You message") {
print(message)
}
I have removed all parameters from showAlert but the funcToCall just to show the way how you can provide default parameter for a function:
extension UIViewController {
private static func defaultFuncToCall() {
//
}
func showAlert(funcToCall: #escaping () -> () = UIViewController.defaultFuncToCall) {
//
}
}