Returning from threading/ GCD/ completion handler - swift

I have some logic to sign a user in from a login screen. If the login fails, I want to display a message to let the user know. If the user logs in successfully, I trigger a segue. When I test it with invalid credentials, the error alert displays as expected but the segue is still being triggered even though it's nested in an if-else statement. Why is this? How can I return out of it and avoid the else block...? I tried adding 'return' under the DispatchQ/ show alert code and that didn't have any effect.
self.login(username: usernameTextField.text!, password: passwordTextField.text!) { (error) in
if error != nil {
DispatchQueue.main.async {
self.showAlert(msg: error ?? "error")
}
} else {
DispatchQueue.main.async {
// segue code
}
}
}
login:
func login(username: String, password: String, completionHandler: #escaping (_ error: String?) -> ()) {
SessionHelper.shared.logUserIn(withUsername: username, andPassword: password) { (error) in
if let err = error {
completionHandler(err)
}
completionHandler(nil)
}
}

You're running the completion handler either way; if you get an error back, you're calling it, but then you fall through and run it with nil. Try this:
func login(username: String, password: String, completionHandler: #escaping (_ error: String?) -> ()) {
SessionHelper.shared.logUserIn(withUsername: username, andPassword: password) { (error) in
if let err = error {
completionHandler(err)
} else {
completionHandler(nil)
}
}
}

Your login function calls completionHandler twice in the error case. The if falls through to the following statement. You should either put the following statement in an else block, or return from the true block.

Related

How to wait until get the response from component under test that use Alamofire? - Xcode

I have a login view controller that user Almofire library to get the response. I do the unit test on that controller but the test always fail. I think because take time to response.
My test case:
override func setUp() {
super.setUp()
continueAfterFailure = false
let vc = UIStoryboard(name: "Main", bundle: nil)
controllerUnderTest = vc.instantiateViewController(withIdentifier: "LoginVC") as! LoginViewController
controllerUnderTest.loadView()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
controllerUnderTest = nil
super.tearDown()
}
func testLoginWithValidUserInfo() {
controllerUnderTest.email?.text = "raghad"
controllerUnderTest.pass?.text = "1234"
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}
I try to use:
waitForExpectations(timeout: 60, handler: nil)
But I got this error:
caught "NSInternalInconsistencyException"
almofire function in login presenter :
func sendRequest(withParameters parameters: [String : String]) {
Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in
debugPrint("new line : \(response)" )
switch response.result {
case .success(let value):
let userJSON = JSON(value)
self.readResponse(data: userJSON)
case .failure(let error):
print("Error \(String(describing: error))")
self.delegate.showMessage("* Connection issue ")
}
self.delegate.removeLoadingScreen()
//firebase log in
Auth.auth().signIn(withEmail: parameters["email"]!, password: parameters["pass"]!) { [weak self] user, error in
//guard let strongSelf = self else { return }
if(user != nil){
print("login with firebase")
}
else{
print("eroor in somthing")
}
if(error != nil){
print("idon now")
}
// ...
}
}
}
func readResponse(data: JSON) {
switch data["error"].stringValue {
case "true":
self.delegate.showMessage("* Invalid user name or password")
case "false":
if data["state"].stringValue=="0" {
self.delegate.showMessage("logged in successfully")
}else {
self.delegate.showMessage("* Inactive account")
}
default:
self.delegate.showMessage("* Connection issue")
}
}
How can I solve this problem? :(
Hi #Raghad ak, welcome to Stack Overflow 👋.
Your guess about the passage of time preventing the test to succeed is correct.
Networking code is asynchronous. After the test calls .sendActions(for: .touchUpInside) on your login button it moves to the next line, without giving the callback a chance to run.
Like #ajeferson's answer suggests, in the long run I'd recommend placing your Alamofire calls behind a service class or just a protocol, so that you can replace them with a double in the tests.
Unless you are writing integration tests in which you'd be testing the behaviour of your system in the real world, hitting the network can do you more harm than good. This post goes more into details about why that's the case.
Having said all that, here's a quick way to get your test to pass. Basically, you need to find a way to have the test wait for your asynchronous code to complete, and you can do it with a refined asynchronous expectation.
In your test you can do this:
expectation(
for: NSPredicate(
block: { input, _ -> Bool in
guard let label = input as? UILabel else { return false }
return label.text == "logged in successfully"
}
),
evaluatedWith: controllerUnderTest.lblValidationMessage,
handler: .none
)
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
waitForExpectations(timeout: 10, handler: nil)
That expectation will run the NSPredicate on a loop, and fulfill only when the predicate returns true.
You have to somehow signal to your tests that are safe to proceed (i.e. expectation is fulfilled). The ideal approach would be decouple that Alamofire code and mock its behavior when testing. But just to answer your question, you might want to do the following.
In your view controller:
func sendRequest(withParameters parameters: [String : String], completionHandler: (() -> Void)?) {
...
Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in
...
// Put this wherever appropriate inside the responseJSON closure
completionHandler?()
}
}
Then in your tests:
func testLoginWithValidUserInfo() {
controllerUnderTest.email?.text = "raghad"
controllerUnderTest.pass?.text = "1234"
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
let expectation = self.expectation(description: "logged in successfully)
waitForExpectations(timeout: 60, handler: nil)
controllerUnderTest.sendRequest(withParameters: [:]) {
expectation.fulfill()
}
XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}
I know you have some intermediate functions between the button click and calling the sendRequest function, but this is just for you to get the idea. Hope it helps!

Testing text of an animated label that appears and disappears

I am struggling to test the appearance of a label(toastLabel) which I have that animates briefly into view when someone enters the wrong email.
private func registerNewUser(email: String, password: String, confirmationPassword: String) {
if password == confirmationPassword {
firebaseData.createUser(email: email, password: password, completion: { (error, _ ) in
if let error = error {
self.showToast(in: self.view, with: error.localizedDescription)
} else {
self.showToast(in: self.view, with: "Registered succesfully")
self.signInUser(email: email, password: password)
}
})
} else {
//raise password mismatch error
print("password mismatch error")
}
}
func showToast(in toastSuperView: UIView, with text: String) {
let toastLabel = ToastLabel()
toastLabel.text = text
toastSuperView.addSubview(toastLabel)
layoutToastLabel(toastLabel)
animateToastLabel(toastLabel)
}
private func layoutToastLabel(_ toastLabel: ToastLabel) {
toastLabel.centerYToSuperview()
toastLabel.pinToSuperview(edges: [.left, .right])
}
private func animateToastLabel(_ toastLabel: ToastLabel) {
UIView.animate(withDuration: 2.5, delay: 0, options: .curveEaseOut, animations: {
toastLabel.alpha = 0.0
}, completion: { _ in
toastLabel.removeFromSuperview()
})
}
I just want to test that the error text received back from firebase appears after the user enters an email that has already been taken.
func testRegisteringWithUsedEmailDisplaysFirebaseError() {
let email = registeredEmail
let password = "password"
welcomeScreenHelper.register(email: email,
password: password,
confirmationPassword: password,
completion: {
let firebaseErrorMessage = "The email address is already in use by another account."
XCTAssert(self.app.staticTexts[firebaseErrorMessage].exists)
})
}
func register(email: String, password: String, confirmationPassword: String, completion: (() -> Void)? = nil) {
let emailTextField = app.textFields[AccesID.emailTextField]
let passwordTextField = app.secureTextFields[AccesID.passwordTextField]
let confirmPasswordTextField = app.secureTextFields[AccesID.confirmPasswordTextField]
let registerButton = app.buttons[AccesID.registerButton]
emailTextField.tap()
emailTextField.typeText(email)
passwordTextField.tap()
passwordTextField.typeText(password)
registerButton.tap()
confirmPasswordTextField.tap()
confirmPasswordTextField.typeText(confirmationPassword)
registerButton.tap()
completion?()
}
when I use other tools such as expectation and XCTWaiter the tests still don't pass despite the text and label definitely appearing. I have never had to do a test like this so I'm not sure where I may be going wrong, whether I have to do something different to test an animated view or something.
Update1:
So I can see after a bit more playing that when i tap the registerButton the toast appears as it should but the test doesn't continue until it has disappeared again. I find this odd as it's not strictly attached to the registerButton being its own view.
update2:
I have update my test as follows:
func testRegisteringWithUsedEmailDisplaysFirebaseError() {
welcomeScreenHelper.register(email: registeredEmail,
password: password,
confirmationPassword: password,
completion: {
let firebaseErrorMessage = "The email address is already in use by another account."
let text = self.app.staticTexts[firebaseErrorMessage]
let exists = NSPredicate(format: "exists == true")
self.expectation(for: exists, evaluatedWith: text, handler: nil)
self.waitForExpectations(timeout: 10, handler: nil)
XCTAssert(self.app.staticTexts[firebaseErrorMessage].exists)
})
}
with the addition of:
override func setUp() {
app.launch()
UIView.setAnimationsEnabled(false)
super.setUp()
}
override func tearDown() {
if let email = createdUserEmail {
firebaseHelper.removeUser(with: email)
}
UIView.setAnimationsEnabled(true)
super.tearDown()
}
But so far no luck. I can still see that in func register once the register button is tapped the toast shows and the next line isn't called until the toastLabel has finished animating.
There are several things you need to solve in such kind of test:
If the code you are testing is using DispatchQueue.async you should use XCTestCase.expectation
If the code you are testing has UIView.animate in it (I see there is one in your example) do UIView.setAnimationsEnabled(false) before the test and enable it back after the test finishes so expectation won't wait for animation to complete. You can do it in XCTestCase.setUp and XCTestCase.tearDown methods.
If the code you are testing has dependencies like services that are doing async calls (I assume firebaseData is) you should either inject their mocks/stubs that will behave synchronously or use XCTestCase.expectation and pray for API/network be OK while the tests are run.
So using XCTestCase.expectation + UIView.setAnimationsEnabled(false) should work for you. Just XCTestCase.expectation with high enough timeout should work too.
EDIT 1:
Correct way to use expectation:
func test() {
let exp = expectation(description: "completion called")
someAsyncMethodWithCompletion() {
exp.fulfill()
}
waitForExpectations(timeout: 1) { _ in }
// assert here
}
So your test method should be:
func testRegisteringWithUsedEmailDisplaysFirebaseError() {
let exp = expectation(description: "completion called")
welcomeScreenHelper.register(email: registeredEmail,
password: password,
confirmationPassword: password,
completion: { exp.fulfill() })
self.waitForExpectations(timeout: 10, handler: nil)
let firebaseErrorMessage = "The email address is already in use by another account."
XCTAssert(self.app.staticTexts[firebaseErrorMessage].exists)
}

Should perform segue on request

i'm starting with Swift3 and i'm having a recurrent problem due to the asynchronism. But until now, i always find a solution with callback.
I have a textField and a button, when i click on the button, i check on the API if there is a existing user named as in the textField.
Using shouldPerformSegue, i return the value if the users exist or no.
I have a separated class for handling calls on the Api
class Api {
static let urlApi = "https://XXXXXXXXXXXXX"
private let CUSTOMER_ID = "XXXXXXXX"
private let CUSTOMER_SECRET = "XXXXXXXX"
private var access_token : String? = nil
private var userInfo : User?
init() {
self.connect()
}
func connect() {
// Do the connect...
}
func get(user: String, callback: #escaping (_ status: Bool) -> Void) {
Alamofire.request(URL(string: "\(Api.urlApi)/v2/users/\(user)")!,
method: .get,
parameters: nil,
encoding: URLEncoding.default,
headers: ["Authorization": "Bearer \(self.access_token!)"])
.responseJSON(completionHandler: { response in
if response.result.isFailure {
print("ERROR: GET USER", response)
callback(false)
} else {
print("SUCCESS Getting user ", user)
callback(true)
}
})
}
}
And in my shouldPerformSegue
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
var userExist : Bool? = nil
let dispatchQueue = DispatchQueue(label: "getUser")
let semaphore = DispatchSemaphore(value: 1)
dispatchQueue.sync {
self.api?.get(user: self.userTextField.text!, callback: { status in
userExist = status
print("1 USEREXIST", userExist)
})
}
semaphore.wait()
print("2 USEREXIST", userExist)
return userExist ?? false // always false because userExist == nil
}
Sorry for the function mess, i don't really find the right way to do my DispachQueue and my Semaphore .. All googling answer look that i need those
The proper way to handle this scenario would be to make the request when the user taps on the button. If there is an error, you would present some error that says the username already exists. Then they would try again.
If the request is successful and that username has not been taken, then you would call performSegueWithIdentifier. The link below shows a good demonstration of the steps to take after this. Your current implementation isn't necessary.
https://stackoverflow.com/a/37823730/653839

How to call a Function in ViewDidLoad with multiple parameters using Swift

I want to call a function in ViewDidLoad method with multiple parameters. But it shows the error
Missing argument 'didFindNewAccessory'in call
Here's my code:
override func viewDidLoad() {
addaccessory(self)
}
func addaccessory(browser: HMAccessoryBrowser, didFindNewAccessory accessory:HMAccessory){
print("Found new accessory")
home?.addAccessory(accessory, completionHandler: {
error in
if error != nil {
print("Failed to add it to home")
}else{
print("Successfully Added")
self.home?.assignAccessory(accessory, toRoom: self.room!, completionHandler: {
error in
if error != nil{
print("Unable to add to room")
}else{
print("Successfuly added to ropom")
self.findOrCreatedServioceGroup()
}
})
}
})
}
You must provide the argument to your function addaccessory.
In your case you must have a browser: HMAccessoryBrowser and accessory:HMAccessory, that's why your call addaccessory(self) is wrong with invalid argument.
You should have something like this :
addaccessory(yourBrowser, didFindNewAccessory: yourAccessory)

Waiting for Callback Function to Finish : Swift

I want to change the view when the user logs in from the LoginController. When access = true, change controller. However, I cant do it within the user.login because it gives me an error saying I have to process this on the main thread. Now I know that there are solutions for this, but I have been searching for a week and ran around in circles. I have gotten close enough to produce what I have below.
I have 3 pieces of information that I would like to share:
LoginController:
var access = false
user.login(
{(response: Bool) -> () in
access = response
print(access)
},username: emailField.text!, password: passwordField.text!)
if(access){
print("YAY")
let controller = storyboard?.instantiateViewControllerWithIdentifier("NewsFeed") as! NewsFeed
presentViewController(controller, animated: true, completion: nil)
}else{
print("NAY")
}
LoginClass:
func login(completionHandler : (response: Bool) -> (), username: String, password: String){
//Set the calback with the calback in the function parameter
let parameters : [String: AnyObject] = ["tag": "login", "email": username, "password": password]
manager.postDataToServer(
{(response: NSDictionary) -> () in
if(response["success"] as! Int == 1){
// Log user in
}else{
// User not able to login
}
completionHandler(response: false)
}
}, page: "login", params: parameters)
}
APIManager:
func postDataToServer(completionHandler: (response: NSDictionary) -> (), page: String, params: [String: AnyObject]){
// Gets the information and returns the User
// Works completely fine
}
ANSWER : Please go down to look at the Updated Answer
Please look at #Rob's answer. However you may get an error message saying Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[UIKeyboardTaskQueue waitUntilAllTasksAreFinished] may only be called from the main thread.'. It is because you cannot change views unless its on the main thread. Simply wrap it in
NSOperationQueue.mainQueue().addOperationWithBlock {
//Change View
}
Updated
I realized my mistake was that I didn't endEditing in one of my fields before processing the information. I fixed it by doing the following, passwordField.endEditing and also emailField.endEditing (just to be safe)
Instead of waiting for the response, just move the code you want to perform inside the closure:
user.login( { response in
if response {
print("YAY")
let controller = self.storyboard?.instantiateViewControllerWithIdentifier("NewsFeed") as! NewsFeed
self.presentViewController(controller, animated: true, completion: nil)
} else {
print("NAY")
}
}, username: emailField.text!, password: passwordField.text!)
Even better, I'd change the order of those parameters, so that the closure was the last parameter (and you can then use "trailing closure" syntax):
func loginWithUsername(username: String, password: String, completionHandler : (response: Bool) -> ()) {
// your login code here
}
And then:
user.loginWithUsername(emailField.text!, password: passwordField.text!) { response in
if response {
print("YAY")
let controller = self.storyboard?.instantiateViewControllerWithIdentifier("NewsFeed") as! NewsFeed
self.presentViewController(controller, animated: true, completion: nil)
} else {
print("NAY")
}
}