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
Related
I need to fetch large amounts of data from an endpoint in an async way. The API endpoint serves a predefined amount of data at a time. After the first request I must check to see if I get a "next" url from the response and visit that link in order to continue the download. This recursive behaviour continues until all available data has been served, in other words paging functionality (HAL links). At this point I have implemented a func that download recursively, however: problem is that the final completion handler does not seem to get called.
Demo code: The ThingsApi is a class that encapsulates the actual API call. The important thing is that this class has an initial url and during recursion will get specific url's to visit asynchronously. I call the downloadThings() func and need to get notified when it is finished. It works if I leave recursion out of the equation. But when recursion is in play then nothing!
I have created a simplified version of the code that illustrate the logic and can be pasted directly into the Playground. The currentPage and pages var's are just there to demo the flow. The last print() statement does not get called. Leave the currentPage += 1 to experience the problem and set currentPage += 6 to avoid recursion. Clearly I am missing out of some fundamental concept here. Anyone?
import UIKit
let pages = 5
var currentPage = 0
class ThingsApi {
var url: URL?
var next: URL?
init(from url: URL) {
self.url = url
}
init() {
self.url = URL(string: "https://whatever.org")
}
func get(completion: #escaping (Data?, HTTPURLResponse?, Error?) -> Void) {
// *** Greatly simplified
// Essentially: use URLSession.shared.dataTask and download data async.
// When done, call the completion handler.
// Simulate that the download will take 1 second.
sleep(1)
completion(nil, nil, nil)
}
}
func downloadThings(url: URL? = nil, completion: #escaping (Bool, Error?, String?) -> Void) {
var thingsApi: ThingsApi
if let url = url {
// The ThingsApi will use the next url (retrieved from previous call).
thingsApi = ThingsApi(from: url)
} else {
// The ThingsApi will use the default url.
thingsApi = ThingsApi()
}
thingsApi.get(completion: { (data, response, error) in
if let error = error {
completion(false, error, "We have nothing")
} else {
// *** Greatly simplified
// Parse the data and save to db.
// Simulate that the thingsApi.next will have a value 5 times.
currentPage += 1
if currentPage <= pages {
thingsApi.next = URL(string: "https://whatever.org?page=\(currentPage)")
}
if let next = thingsApi.next {
// Continue downloading things recursivly.
downloadThings(url: next) { (success, error, feedback) in
guard success else {
completion(false, error, "failed")
return
}
}
} else {
print("We are done")
completion(true, nil, "done")
print("I am sure of it")
}
}
})
}
downloadThings { (success, error, feedback) in
guard success else {
print("downloadThings() failed")
return
}
// THIS DOES NOT GET EXECUTED!
print("All your things have been downloaded")
}
It seems like this is simply a case of "you forgot to call it yourself" :)
In this if statement right here:
if let next = thingsApi.next {
// Continue downloading things recursivly.
downloadThings(url: next) { (success, error, feedback) in
guard success else {
completion(false, error, "failed")
return
}
}
} else {
print("We are done")
completion(true, nil, "done")
print("I am sure of it")
}
Think about what happens on the outermost call to downloadThings, and execution goes into the if branch, and the download is successful. completion is never called!
You should call completion after the guard statement!
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!
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)
}
What I am trying to accomplish is posting a notification through NSNotificationCenter's default center. This is being done within a closure block after making a network call using Alamofire. The problem I am having is that a class that should be responding to a posted notification isn't receiving such notification.
My ViewController simply creates a First object that get's things moving:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let first = First()
}
}
My First class creates and instance of a Second class and adds itself as an observer to my NSNotificationCenter. This is the class that can't seem to get the notification when the notification is posted.
class First : NSObject {
let second = Second()
override init(){
super.init()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(First.gotDownloadNotification(_:)), name: "test", object: nil)
second.sendRequest()
}
// NOT REACHING THIS CODE
func gotDownloadNotification(notification: NSNotification){
print("Successfully received download notification from Second")
}
}
My Second class is what makes the network call through my NetworkService class and posts a notification in a closure once the request is successful and complete.
class Second : NSObject {
func sendRequest(){
let networkService = NetworkService()
networkService.downloadFile() { statusCode in
if let statusCode = statusCode {
print("Successfully got a status code")
// Post notification
NSNotificationCenter.defaultCenter().postNotificationName("test", object: nil)
}
}
}
}
Finally, my NetworkService class is what makes a network call using Alamofire and returns the status code from the response through a closure.
class NetworkService : NSObject {
func downloadFile(completionHandler: (Int?) -> ()){
Alamofire.download(.GET, "https://www.google.com") { temporaryURL, response in
let fileManager = NSFileManager.defaultManager()
let directoryURL = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
let pathComponent = response.suggestedFilename
return directoryURL.URLByAppendingPathComponent(pathComponent!)
}
.response { (request, response, _, error) in
if let error = error {
print("File download failed with error: \(error.localizedDescription)")
completionHandler(nil)
} else if let response = response{
print("File downloaded successfully")
// Pass status code through completionHandler to Second
completionHandler(response.statusCode)
}
}
}
}
The output after execution is:
File downloaded successfully
Successfully got a status code
From this output I know the download was successful and Second got the status code from the closure and posted a notification right after.
I believe that I have tried resolving most other suggestions on Stack Overflow related to not receiving notifications such as objects not being instantiated before notification is posted or syntax of either adding an observer or posting a notification.
Does anyone have any idea why the posted notification is not being received in the First class?
Since there is a direct relationship between First and Second the protocol/delegate pattern is the better way to notify. Even better with this pattern and you don't have to take care of unregistering the observer. NSNotificationCenter is supposed to be used only if there is no relationship between sender and receiver.
And basically the thread doesn't matter either.
protocol SecondDelegate {
func gotDownloadNotification()
}
class Second : NSObject {
var delegate : SecondDelegate?
init(delegate : SecondDelegate?) {
self.delegate = delegate
}
func sendRequest(){
let networkService = NetworkService()
networkService.downloadFile() { statusCode in
if let statusCode = statusCode {
print("Successfully got a status code")
// Post notification
self.delegate?.gotDownloadNotification()
}
}
}
}
class First : NSObject, SecondDelegate {
let second : Second
override init(){
super.init()
second = Second(delegate:self)
second.sendRequest()
}
func gotDownloadNotification(){
print("Successfully received download notification from Second")
}
}
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")
}
}