Can't refresh UI after async call - ios

What I want to do: I have a login screen, the user fill the username and password, and then it press the login button. An async call to the server is done to check if user is registered and password is okay, and if yes (async function set a bool to yes) then do a segue to the next view controller. Simple as that, I've tried many ways but with always the same problem, the main thread runs the shouldPerformSegueWithIdentifier method, do the async call and check the global bool var (false by default) before the background thread has updated it, so the segue is not performed because the global variable is set to true AFTER. Only if I use sleep(1) the UI is refreshed but I don't want to use this. Is there a way to do this without sleep?? Every method I run has a completion handler.
I don't know how to sync the main with the background thread. I've read it's posible to update UI from async call so this should be posible. I've been looking questions for a while and tried lot of snippets, and still haven't found a solution for my problem.
This is the code I have so far:
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
let apiCall = webApi()
dispatch_async(dispatch_get_main_queue()) {
apiCall.callCheckIsUserLogged(nil, password : self.passwordField.text, email: self.mailField.text){ (ok) in
}
}
//sleep(1) if I uncomment this, my method works because it will return true
return userIsLogged
}
apiCall.callCheckIsUserLogged() :
typealias successClosure = (success : Bool) -> Void
//Make a call to check if the user exist, server returns json with success or failure
func callCheckIsUserLogged(username: String?, password : String?, email: String?,completed : successClosure){
userApiCallUrl = "http://apiurl.com/users/login"
let call = asyncCallClass()
call.doAsyncCallWithParams(userApiCallUrl, calltype: "POST", username: username, pass: password, mail: email){ (success) in
completed(success: true)
}
}
call.doAsyncCallWithParams() code:
internal typealias completion = (success : Bool) -> Void
private var flagCompletion : Bool = false
//Handle async class with this method
//var callType is aditioned everytime an arg is not passed nil.
//callType == 3 it is a call to check if user is logged
//callType == 2 is a call to register a new user
func doAsyncCallWithParams(url : String, calltype : String, username : String?, pass : String?, mail : String?, completed : completion){
var callType : Int = 0
//Set Async Url
setUrl(url)
//Set Post Params
if let user : String = username{
self.username = "username=\(user)"
callType += 1
}
if let password : String = pass{
self.password = "password=\(password)"
callType += 1
}
if let mail : String = mail{
self.email = "email=\(mail)"
callType += 1
}
//register a new user
if(callType == 3){
paramString = "\(self.username)&\(self.password)&\(self.email)"
}
//check if user is logged, send email and password
if(callType == 2){
paramString = "\(self.email)&\(self.password)"
}
//Do call
callWithCompletionHandler { (success) in
self.flagCompletion = true
completed(success: self.flagCompletion)
}
}
callWithCompletionHandler() code:
private typealias completionAsyncCall = (success : Bool) -> Void
private func callWithCompletionHandler(completed : completionAsyncCall){
asyncJson.removeAllObjects()
//Set async call params
let request = NSMutableURLRequest(URL: NSURL(string: self.url!)!)
request.HTTPMethod = "POST"
let trimmedPostParam : String = self.paramString!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
request.HTTPBody = trimmedPostParam.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard error == nil && data != nil else {
// check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? NSHTTPURLResponse where httpStatus.statusCode != 200 {
// check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
let responseString = NSString(data: data!, encoding: NSUTF8StringEncoding)
let result : AnyObject = responseString!.parseJSONString!
if let nsMutableResult = result as? NSMutableArray{
print("NSMutableArray")
}
if let nsDictResult = result as? NSMutableDictionary{
self.parseMutableDictionary(nsDictResult)
}
self.flag = true // true if download succeed,false otherwise
completed(success: flagAsyncCall!)
}
task.resume()
}

On login button press call :
apiCall.callCheckIsUserLogged(nil, password : self.passwordField.text, email: self.mailField.text){ (ok) in
if ok {
self.performSegueWithIdentifier("Identifier", sender: self)
} else {
print("User not logged in")
}
}
Because your callCheckIsUserLogged method already returns if user logged in or not.
internal typealias completion = (success : Bool) -> Void

Related

Code after guard is called later than expected

Hi I am new in iOS development and I am having hard time to understand the following issue. Basically I am trying to get user's name by passing current user's id to Cloud Firestore. However I am having hard time to understand a bug in the code. I can successfully pass the name of user to name variable, while the function returns default value of name which is "" (empty string). It seems that the block of code inside
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
print("after guard") // this line
}
happens later than
print("name") // this line
return name
Full code:
private func returnCurrentUserName() -> String {
// User is signed in.
var name = ""
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).getDocument { (snapshot, error) in
if error == nil {
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
print("after guard") // this line
}
}
}
print("name") // this line
return name
}else {
return ""
}
}
(Note: the query from Cloud Firestore is successful and I can get users name on the console but "name" is printed after "after guard".)
In addition to the other answer:
If you would like to execute code after your operation is done, you could use a completion block (that's just a closure which gets called upon completion):
private func returnCurrentUserName(completion: #escaping () -> ()) -> String {
// User is signed in.
var name = ""
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).getDocument { (snapshot, error) in
if error == nil {
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
completion()//Here you call the closure
print("after guard") // this line
}
}
}
print("name") // this line
return name
}else {
return ""
}
}
How you would call returnCurrentUserName:
returnCurrentUserName {
print("runs after the operation is done")
}
Simplified example:
func returnCurrentUserName(completion: #escaping () -> ()) -> String {
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
completion() //runs after 4 seconds
}
return "xyz"
}
let test = returnCurrentUserName {
print("runs after the operation is done")
}
print(test)
The reason is your getDocument is an asynchronous operation. It takes a callback, and that callback will be invoked when the operation is done. Because of the asynchronous operation, the program will continue process the next line without waiting for the async operation to be completed. That's why you see your print("name") getting executed before the print("after guard")

how to manage a several asynchronous task before doing some action?

I am beginner in programming. I actually have my own answer of this questions and the app worked as I am expected, but I am not sure if this is the correct way to to this.
This check out action will be triggered after the user click chechoutButton. but before before this chechoutButton.isEnabled , I have to make sure 3 parameters are available (not nil). before doing this check out action, I need 3 parameters :
get user's coordinate from GPS.
get user's location address from Google Place
API
Get current date time from server for verification.
method to get user location address from Google Place API will be triggered only if I get the coordinate from GPS, and as we know, fetching data from the internet (to take date and time) also takes time, it should be done asynchronously.
how do I manage this checkoutButton only enabled if those 3 parameters are not nil ? Is there a better way according to apple guideline to do this
the simplified code are below
class CheckoutTVC: UITableViewController {
#IBOutlet weak var checkOutButton: DesignableButton!
var checkinAndCheckoutData : [String:Any]? // from MainMenuVC
var dateTimeNowFromServer : String?
var userLocationAddress : String?
let locationManager = LocationManager()
var coordinateUser : Coordinate? {
didSet {
getLocationAddress()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// initial state
checkOutButton.alpha = 0.4
checkOutButton.isEnabled = false
getDateTimeFromServer()
getCoordinate()
}
#IBAction func CheckoutButtonDidPressed(_ sender: Any) {
}
}
extension CheckoutTVC {
func getDateTimeFromServer() {
activityIndicator.startAnimating()
NetworkingService.getDateTimeFromServer { (result) in
switch result {
case .failure(let error) :
self.activityIndicator.stopAnimating()
// show alert
case .success(let timeFromServer) :
let stringDateTimeServer = timeFromServer as! String
self.dateTimeNowFromServer = stringDateTimeServer
self.activityIndicator.stopAnimating()
}
}
}
func getCoordinate() {
locationManager.getPermission()
locationManager.didGetLocation = { [weak self] userCoordinate in
self?.coordinateUser = userCoordinate
self?.activateCheckOutButton()
}
}
func getLocationAddress() {
guard let coordinateTheUser = coordinateUser else {return}
let latlng = "\(coordinateTheUser.latitude),\(coordinateTheUser.longitude)"
let request = URLRequest(url: url!)
Alamofire.request(request).responseJSON { (response) in
switch response.result {
case .failure(let error) :// show alert
case .success(let value) :
let json = JSON(value)
let locationOfUser = json["results"][0]["formatted_address"].string
self.userLocationAddress = locationOfUser
self.locationAddressLabel.text = locationOfUser
self.activateNextStepButton()
}
}
}
func activateCheckoutButton() {
if dateTimeNowFromServer != nil && userLocationAddress != nil {
checkOutButton.alpha = 1
checkOutButton.isEnabled = true
}
}
}
I manage this by using this method, but I don't know if this is the correct way or not
func activateCheckoutButton() {
if dateTimeNowFromServer != nil && userLocationAddress != nil {
checkOutButton.alpha = 1
checkOutButton.isEnabled = true
}
}
You can use DispatchGroup to know when all of your asynchronous calls are complete.
func notifyMeAfter3Calls() {
let dispatch = DispatchGroup()
dispatch.enter()
API.call1() { (data1)
API.call2(data1) { (data2)
//DO SOMETHING WITH RESPONSE
dispatch.leave()
}
}
dispatch.enter()
API.call3() { (data)
//DO SOMETHING WITH RESPONSE
dispatch.leave()
}
dispatch.notify(queue: DispatchQueue.main) {
finished?(dispatchSuccess)
}
}
You must have an equal amount of enter() and leave() calls. Once all of the leave() calls are made, the code in DispatchGroupd.notify will be called.

Completion handler with POST request

I have simple login method which returns bool, depends on success of user login. I have problem with order of the responses and execution of the code. I've read about completion handlers, which I think are a solution to my problem but I'm not sure. Here is my method:
//perform user login in, setting nsuserdefaults and returning the bool result
func login(username: String, password:String) -> (Bool) {
var success:Bool = false
//sending inputs to server and receiving info from server
let postRequest = postDataToURL()
postRequest.link = "http://pnc.hr/rfid/login.php"
postRequest.postVariables = "username=" + username + "&password=" + pass
word
postRequest.forData("POST") { jsonString in
// getting the result from the asinhronys task
let result = convertStringToDictionary(jsonString as String)
if let loggedIn = result?["loggedIn"] as? Bool where loggedIn == true {
let userType = result?["userType"] as? String
let token = result?["token"] as? String
//if user is logged - setting parameters in Key Chains and redirecting them to the menu view
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(loggedIn, forKey: "loggedIn")
defaults.setObject(username, forKey: "username")
defaults.setObject(userType, forKey: "userType")
defaults.setObject(token, forKey: "token")
success = true
}
else {
success = false
}
print ("class - " + String(jsonString))
print ("classIN - " + String(success))
}
print ("classOUT - " + String(success))
return success
}
I would like to make return of success variable inside if statement which checks variable loggedIn is equal to true. But in that case I get error.
Then I have made this method. The problem is that method returns the variable success quicker than the POST request has been done. So it will be false in every case. I have printed variables to see the order of the code execution and method first prints the "classOUT", returns the variable, and then sets up variable value and print "classIN".
How can I wait until the code which logs user gets executed so I can get the right value of the variable success?
Perform user login in, setting nsuserdefaults and returning the bool result
completionBlock: is that block which will get executed when you call it like any block but you get to choose when and what all to pass through that block.
func login(username: String, password:String,completionBlock : ((success : Bool)->Void)){
//sending inputs to server and receiving info from server
let postRequest = postDataToURL()
postRequest.link = "http://pnc.hr/rfid/login.php"
postRequest.postVariables = "username=" + username + "&password=" + password
postRequest.forData("POST") { jsonString in
// getting the result from the asinhronys task
let result = convertStringToDictionary(jsonString as String)
if let loggedIn = result?["loggedIn"] as? Bool where loggedIn == true {
let userType = result?["userType"] as? String
let token = result?["token"] as? String
//if user is logged - setting parameters in Key Chains and redirecting them to the menu view
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(loggedIn, forKey: "loggedIn")
defaults.setObject(username, forKey: "username")
defaults.setObject(userType, forKey: "userType")
defaults.setObject(token, forKey: "token")
completionBlock(success:true)
}
else {
completionBlock(success:false)
}
}
}
when you call it would look something like this:-
login(username: String, password:String,completionBlock : { (success) in
print(success)
})
you could do something like this
func login(username: String, password: String, completion: (Bool) -> ()) {
... YOUR USUAL NETWORKING CODE ...
completion(success)
}
and then call it
login(username: anonymous, password: ******) { authStatus in
if authStatus == true {
print("user in")
} else {
print("try one more time")
}
}

closures in swift are doing whatever. My code execution isn't acting as is supposed

In app delegate, after to get the coordinates of the user with core location, I want to make two api calls. One is to my server, to get a slug of the city name in which we are. The call is async so I want to load all the content into a global variable array before make the second call to google maps api, to get the city name from google, also with an async call. And finally after I have loaded all the google data, I want to compare the two arrays, with the city names to find a coincidence. To do that, I need the first two operation to have ended. For this I'm using closures, to ensure all the data is loaded before the next operation start. But when I launch my program, it doesn't find any coincidence between the two arrays and when I set breakpoints, I see the second array (google) is loaded after the comparison is made, which is very frustrating because I've set a lot of closures, and at this stage I'm not able to find the source of my issue. Any help would be appreciated.
this is app delegate:
let locationManager = CLLocationManager()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
//Language detection
let pre = NSLocale.preferredLanguages()[0]
print("language= \(pre)")
//Core Location
// Ask for Authorisation from the User.
self.locationManager.requestAlwaysAuthorization()
//Clore Location
// For use in foreground
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
//Load cities slug via api call
let apiCall : webApi = webApi()
apiCall.loadCitySlugs(){(success) in
//Slug loaded in background
//Call google api to compare the slug
apiCall.loadGoogleContent(){(success) in //this function is called after compareGoogleAndApiSlugs()
apiCall.compareGoogleAndApiSlugs() //this one called before
}
}
}
return true
}
This is my global variables swift file:
import Foundation
import CoreLocation
let weatherApiKey : String = "" //weather api key
var globWeatherTemp : String = ""
var globWeatherIcon : String = ""
var globCity : String = ""
var globCountry : String = ""
let googleMapsApiKey : String = ""
let googlePlacesApiKey : String = ""
var TableData:Array< String > = Array < String >()
var nsDict = []
var locValue : CLLocationCoordinate2D = CLLocationCoordinate2D()
typealias SuccessClosure = (data: String?) -> (Void)
typealias FinishedDownload = () -> ()
typealias complHandlerAsyncCall = (success : Bool) -> Void
typealias complHandlerCitySlug = (success:Bool) -> Void
typealias complHandlerAllShops = (success:Bool) -> Void
typealias googleCompareSlugs = (success:Bool) -> Void
var flagCitySlug : Bool?
var flagAsyncCall : Bool?
var flagAllShops : Bool?
var values : [JsonArrayValues] = []
var citySlug : [SlugArrayValues] = []
var asyncJson : NSMutableArray = []
let googleJson : GoogleApiJson = GoogleApiJson()
this is the first function called in app delegate, which make a call to my server to load the city slug:
func loadCitySlugs(completed: complHandlerCitySlug){
//Configure Url
self.setApiUrlToGetAllSlugs()
//Do Async call
asyncCall(userApiCallUrl){(success)in
//Reset Url Async call and Params
self.resetUrlApi()
//parse json
self.parseSlugJson(asyncJson)
flagCitySlug = true
completed(success: flagCitySlug!)
}
}
This is the second function, it load google content but it's called after compareGoogleAndApiSlugs() and it's supposed to be called before...
/*
Parse a returned Json value from an Async call with google maps api Url
*/
func loadGoogleContent(completed : complHandlerAsyncCall){
//Url api
setGoogleApiUrl()
//Load google content
googleAsyncCall(userApiCallUrl){(success) in
//Reset API URL
self.resetUrlApi()
}
flagAsyncCall = true // true if download succeed,false otherwise
completed(success: flagAsyncCall!)
}
And finally the async calls, there are two but they are almost the same code:
/**
Simple async call.
*/
func asyncCall(url : String, completed : complHandlerAsyncCall)/* -> AnyObject*/{
//Set async call params
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
request.HTTPMethod = "POST"
request.HTTPBody = postParam.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard error == nil && data != nil else {
// check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? NSHTTPURLResponse where httpStatus.statusCode != 200 {
// check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
let responseString = NSString(data: data!, encoding: NSUTF8StringEncoding)
asyncJson = responseString!.parseJSONString! as! NSMutableArray
flagAsyncCall = true // true if download succeed,false otherwise
completed(success: flagAsyncCall!)
}
task.resume()
}
If anyone can see the issue or throw some light it would be very appreciated.
The problem is this function:
func loadGoogleContent(completed : complHandlerAsyncCall){
setGoogleApiUrl()
googleAsyncCall(userApiCallUrl){(success) in
self.resetUrlApi()
}
flagAsyncCall = true
completed(success: flagAsyncCall!) //THIS LINE IS CALLED OUTSIDE THE googleAsyncCall..
}
The above completion block is called outside of the googleAsyncCall block.
The code should be:
func loadGoogleContent(completed : complHandlerAsyncCall){
setGoogleApiUrl()
googleAsyncCall(userApiCallUrl){(success) in
self.resetUrlApi()
flagAsyncCall = true
completed(success: flagAsyncCall!)
}
}
Btw.. your global variables are NOT atomic.. so be careful.

How do I call a function only after an async function has completed?

I have a function Admin that runs asynchronously in the background.
Is there a way to make sure that the function is completed before calling the code after it?
(I am using a flag to check the success of the async operation. If the flag is 0, the user is not an admin and should go to the NormalLogin())
#IBAction func LoginAction(sender: UIButton) {
Admin()
if(bool.flag == 0) {
NormalLogin()
}
}
func Admin() {
let userName1 = UserName.text
let userPassword = Password.text
let findTimeLineData2:PFQuery = PFQuery(className: "Admins")
findTimeLineData2.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if !(error != nil){
for object in objects as! [PFObject] {
let userName2 = object.objectForKey("AdminUserName") as! String
let userPassword2 = object.objectForKey("AdminPassword") as! String
if(userName1 == userName2 && userPassword == userPassword2) {
//hes an admin
bool.flag = 1
self.performSegueWithIdentifier("AdminPage", sender: self)
self.UserName.text = ""
self.Password.text = ""
break;
}
}
}
}
}
You need to look into completion handlers and asynchronous programming. Here's an example of an async function that you can copy into a playground:
defining the function
notice the "completion" parameter is actually a function with a type of (Bool)->(). Meaning that the function takes a boolean and returns nothing.
func getBoolValue(number : Int, completion: (result: Bool)->()) {
if number > 5 {
// when your completion function is called you pass in your boolean
completion(result: true)
} else {
completion(result: false)
}
}
calling the function
here getBoolValue runs first, when the completion handler is called (above code) your closure is run with the result you passed in above.
getBoolValue(8) { (result) -> () in
// do stuff with the result
print(result)
}
applying the concept
You could apply this concept to your code by doing this:
#IBAction func LoginAction(sender: UIButton) {
// admin() calls your code, when it hits your completion handler the
// closure {} runs w/ "result" being populated with either true or false
Admin() { (result) in
print("completion result: \(result)") //<--- add this
if result == false {
NormalLogin()
} else {
// I would recommend managing this here.
self.performSegueWithIdentifier("AdminPage", sender: self)
}
}
}
// in your method, you pass a `(Bool)->()` function in as a parameter
func Admin(completion: (result: Bool)->()) {
let userName1 = UserName.text
let userPassword = Password.text
let findTimeLineData2:PFQuery = PFQuery(className: "Admins")
findTimeLineData2.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if !(error != nil){
for object in objects as! [PFObject] {
let userName2 = object.objectForKey("AdminUserName") as! String
let userPassword2 = object.objectForKey("AdminPassword") as! String
if(userName1 == userName2 && userPassword == userPassword2) {
// you want to move this to your calling function
//self.performSegueWithIdentifier("AdminPage", sender: self)
self.UserName.text = ""
self.Password.text = ""
// when your completion handler is hit, your operation is complete
// and you are returned to your calling closure
completion(result: true) // returns true
} else {
completion(result: false) // returns false
}
}
}
}
}
Of course, I'm not able to compile your code to test it, but I think this will work fine.

Resources