How to create proper completion handler for server login in swift? - swift

I have an api manager class in my swift application and it has a server login with username and password.
I want to know how to create a completion handler for it that when the server responses with 200 status code, the function handles that response and for example performs a segue in the viewcontroller.
I did not find any tutorials for this. Thanks for your help!
EDIT 1:
What i need is: The completion handler is immediately run when the function is called. I want the completion handler run after server responds.
And this is my login function:
public class func Login(username: String, password: String, complitionHandler: #escaping (Int) -> Void) {
let urlS = "http://server.com/" + "login.php"
let url = URL(string: urlS)
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let body = "username=\(username.lowercased())&password=\(password)"
request.httpBody = body.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else {
print(error!)
print("error")
logedIn = 2
return
}
do{
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSDictionary
if let parseJson = json {
let code = parseJson["status"] as! String
if code == "200" {
print("loged inn")
logedIn = 1
}else if code == "400" {
print("uuuser/pass error")
logedIn = 0
}
}
}catch{
print("json error")
logedIn = 2
}
}
task.resume()
DispatchQueue.main.async {
complitionHandler(logedIn)
}
}
And how i call the function in my ViewController:
Manager.Login(username: "1", password: "1") { (i) in
switch i {
case 0:
print("user/pass error")
case 1:
print("loged in")
self.performSegue(withIdentifier: "toMain", sender: self)
case 2:
print("json error")
default:
()
}
}

You have all of the pieces in place. You just need to move your call to the completion handler to the correct place:
}catch{
print("json error")
logedIn = 2
}
DispatchQueue.main.async {
complitionHandler(logedIn)
}
}
task.resume()
Also note that method names should start with lowercase letters so your Login function should be named login.
Now you can use this login method like:
login(username: someUsername, password: somePassword) { (result) in
if result == 1 {
// success - do your segue
} else if result == 0 {
// bad username/password
} else {
// some error
}
}

Related

Authentication with POST request is failing to validate data

So I have a created a POST request which validates the Username and Password when someone tries to login, The problem is when i press the button sender it doesn't validate the data at all even if the Username and Password field are empty it still segues you to the main Dashboard which I find it very weird.
The Button Sender from signInViewController class :
#IBAction func signInSegueToDashboard(_ sender: Any) {
APICallerPOST.shared.signInToAccount(username: emailFieldSignIn.text!, password: passwordFieldSignIn.text!) { [self] (result, error) in
switch result?.StatusCode {
case 0:
DispatchQueue.main.async {
activityLoaderSignIn.startAnimating()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
guard let mainTabBarController = self.storyboard?.instantiateViewController(withIdentifier: "mainTabBarController")
else {
return
}
self.activityLoaderSignIn.stopAnimating()
mainTabBarController.modalPresentationStyle = .custom
self.present(mainTabBarController, animated: true, completion: nil)
}
case 1:
print("error")
case 2:
print("error2")
case 3:
print("error3")
case 4:
print("error4")
case 5:
print("error5")
default:
break
}
}
}
The problem is that even if the emailFieldSignIn.text! and passwordFieldSignIn.text! are empty it still segues you to the mainTabBarController without any validation of the data.
The POST request from APICallerPOST class :
func signInToAccount(username: String, password: String, completion: #escaping (SignInResponse?, Error?) -> Void) {
//declare parameter as a dictionary which contains string as key and value combination.
let parameters = ["User": username, "Password": password]
//create the url with NSURL
let url = URL(string: "https://censoredurl/Signin")!
//create the session object
let session = URLSession.shared
//now create the Request object using the url object
var request = URLRequest(url: url)
request.httpMethod = "POST" //set http method as POST
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted) // pass dictionary to data object and set it as request body
} catch let error {
print(error.localizedDescription)
completion(nil, error)
}
//HTTP Headers
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Accept")
//create dataTask using the session object to send data to the server
let task = session.dataTask(with: request, completionHandler: { data, response, error in
guard error == nil else {
completion(nil, error)
return
}
guard let data = data else {
completion(nil, NSError(domain: "dataNilError", code: -100001, userInfo: nil))
return
}
do {
//create json object from data
let decoder = JSONDecoder()
guard let json = try? decoder.decode(SignInResponse.self, from: data) else {
completion(nil, NSError(domain: "invalidJSONTypeError", code: -100009, userInfo: nil))
return
}
print(json)
completion(json, nil)
} catch let error {
print(error.localizedDescription)
completion(nil, error)
}
})
task.resume()
}
Please check my answer it will be helpfull for you I have added compulsory checks to validate if UITextfields are empty or not you can also add valid email check.
#IBAction func signInSegueToDashboard(_ sender: Any) {
if emailFieldSignIn.text!.isEmpty || passwordFieldSignIn.text!.isEmpty{
// show some error
return
}
APICallerPOST.shared.signInToAccount(username: emailFieldSignIn.text!, password: passwordFieldSignIn.text!) { [self] (result, error) in
switch result?.StatusCode {
case 0:
DispatchQueue.main.async {
activityLoaderSignIn.startAnimating()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
guard let mainTabBarController = self.storyboard?.instantiateViewController(withIdentifier: "mainTabBarController")
else {
return
}
self.activityLoaderSignIn.stopAnimating()
mainTabBarController.modalPresentationStyle = .custom
self.present(mainTabBarController, animated: true, completion: nil)
}
case 1:
print("error")
case 2:
print("error2")
case 3:
print("error3")
case 4:
print("error4")
case 5:
print("error5")
default:
break
}
}
}

Not getting Firebase token id when calling getIdToken in Swift 5 iOS

I have been trying to make this work for 3 days now, and I can't see where I am going. When trying to get firebase tokenId using the - (void)getIDTokenWithCompletion: (nullable void (^)(NSString *_Nullable __strong, NSError *_Nullable __strong))completion; function provided by firebase I am getting nothing in return.
I have created a separate class to get the Id using a completion handler. Below is the code I am using
import Foundation
import FirebaseAuth
class FirebaseToken {
static var shared = FirebaseToken.init()
func getIdToken(token completion: #escaping(String?,Error?) -> Void){
Auth.auth().currentUser?.getIDToken(completion:{ idToken, error in
guard let error = error else {return }
print(error)
completion(nil, error)
guard let token = idToken else {return}
completion(token, nil)
print(token)
})
}
}
This is the class I am using to call the func getIdToken function to get the Id, which is inside the func makeAPICall<T:Codable>(urlPath: String, apiMethod: HTTPMethod, expectedReturnType: T.Type,user completionHandler: #escaping ([T]?,Error?) function.
import Foundation
import Alamofire
import Firebase
class ApiService {
static var shared = ApiService.init()
let session: Session = {
let manager = ServerTrustManager(allHostsMustBeEvaluated: false,evaluators: ["localhost": DisabledTrustEvaluator()])
let configuration = URLSessionConfiguration.af.default
return Session(configuration: configuration, serverTrustManager: manager)
}()
//MARK:- GET
func makeAPICall<T:Codable>(urlPath: String, apiMethod: HTTPMethod, expectedReturnType: T.Type,user completionHandler: #escaping ([T]?,Error?) -> Void) {
var urlComponent = URLComponents()
urlComponent.scheme = "https"
urlComponent.host = "localhost"
urlComponent.port = 5001
urlComponent.path = "/api/" + urlPath
print(urlComponent.url!)
guard let url = urlComponent.url else {
return
}
var headers: HTTPHeaders?
FirebaseToken.shared.getIdToken(token: {tokenId, error in
guard let errors = error else {return}
print(errors)
guard let tokens = tokenId else {return}
headers = [
.authorization(bearerToken: tokens),
.accept("application/json")
]
})
guard let headerAuth = headers else {
print("not getting firebase token")
return
}
print(headerAuth)
session.request(url, method: apiMethod).validate().responseDecodable(of: [T].self) {(response) in
switch response.result{
case .success:
guard let users = response.value else {
return
}
//print(header)
completionHandler(users, nil)
case .failure(let error):
completionHandler(nil, error)
}
}
}
}
the variable var headers: HTTPHeaders?, which inside the function
guard let headerAuth = headers else {
print("not getting firebase token")
return
}
should be printing out the token yet, for some reason the token isn't being added. Can someone let me know where I am going wrong as I have been stuck for 3 days and I am still very new to firebase?
The printing result should be within print(headerAuth), however; I keep on getting the result within print("not getting firebase token")
The getIdToken method is an asynchronous call. Any code that needs the resulting token, needs to be inside the completion handler.
So something like:
FirebaseToken.shared.getIdToken(token: {tokenId, error in
guard let tokens = tokenId else {return}
headers = [
.authorization(bearerToken: tokens),
.accept("application/json")
]
guard let headerAuth = headers else {
print("not getting firebase token")
return
}
session.request(url, method: apiMethod).validate().responseDecodable(of: [T].self) {(response) in
switch response.result{
case .success:
guard let users = response.value else {
return
}
completionHandler(users, nil)
case .failure(let error):
completionHandler(nil, error)
}
}
})
I also removed this line:
guard let errors = error else {return}
As I'm pretty sure this returns out of the block when there is no error.

Could not cast value of type 'Swift.String' (0x10fef45c0) to 'Swift.Error' (0x10ff2bd10). (lldb)

Below line of code is producing the error,
DispatchQueue.main.async {
completion(.success(jsonData), Error as! Error)
}
When print jsonData This code returns perfect result of array but getting this error,
Could not cast value of type 'Swift.String' (0x10fef45c0) to 'Swift.Error' (0x10ff2bd10). (lldb)
As the error says I understand its a cast exception, but I'm not able to modify the code to make it work. I'm kinda new to Swift, so any help would be appreciated. Below is my
import Foundation
class APIService {
private var dataTask: URLSessionDataTask?
func getPopularPosts(completion: #escaping (Result<Any, Error>, Error) -> Void) {
let popularURL = "URL Here"
guard let url = URL(string: popularURL) else {return}
// Create URL Session - work on the background
dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
// Handle Error
if let error = error {
completion(.failure(error), Error.self as! Error)
print("DataTask error: \(error.localizedDescription)")
return
}
guard let response = response as? HTTPURLResponse else {
// Handle Empty Response
print("Empty Response")
return
}
print("Response status code: \(response.statusCode)")
guard let data = data else {
// Handle Empty Data
print("Empty Data")
return
}
do {
// Parse the data
let decoder = JSONDecoder()
let jsonData = try decoder.decode(APIService.self, from: data)
// print(jsonData)
// Back to the main thread
DispatchQueue.main.async {
completion(.success(jsonData), Error as! Error)
}
} catch let error {
completion(.failure(error),error)
}
}
dataTask?.resume()
}
}
Modify the completion block parameters, you already are returning the error inside the Result's .failure(Error) block so no need to repeat it again as another parameter in the completion parameter. Here's how you fix this:
Declaration:
class APIService {
private var dataTask: URLSessionDataTask?
func getPopularPosts(completion: #escaping (Result<CategoriesNewsData, Error>) -> Void) {
let popularURL = "URL Here"
guard let url = URL(string: popularURL) else {return}
// Create URL Session - work on the background
dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
// Handle Error
if let error = error {
completion(.failure(error))
print("DataTask error: \(error.localizedDescription)")
return
}
guard let response = response as? HTTPURLResponse else {
// Handle Empty Response
print("Empty Response") // Throw a custom error here too.
return
}
print("Response status code: \(response.statusCode)")
guard let data = data else {
// Handle Empty Data
print("Empty Data") // Throw a custom error here too.
return
}
do {
let decoder = JSONDecoder()
let jsonData = try decoder.decode(CategoriesNewsData.self, from: data)
DispatchQueue.main.async {
completion(.success(jsonData))
}
} catch let error {
completion(.failure(error))
}
}
dataTask?.resume()
}
}
Calling:
service.getPopularPosts { result in
switch result {
case .success(let categoriesNewsData):
print(categoriesNewsData)
case .failure(let error):
print(error)
}
}

Swift 4: How to asynchronously use URLSessionDataTask but have the requests be in a timed queue?

Basically I have some JSON data that I wish to retrieve from a bunch of URL's (all from the same host), however I can only request this data roughly every 2 seconds at minimum and only one at a time or I'll be "time banned" from the server. As you'll see below; while URLSession is very quick it also gets me time banned almost instantly when I have around 700 urls to get through.
How would I go about creating a queue in URLSession (if its functionality supports it) and while having it work asynchronously to my main thread; have it work serially on its own thread and only attempt each item in the queue after 2 seconds have past since it finished the previous request?
for url in urls {
get(url: url)
}
func get(url: URL) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
DispatchQueue.main.async {
print(error.localizedDescription)
}
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
DispatchQueue.main.async {
print("Server Error")
}
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
DispatchQueue.main.async {
self.itemsCount.append(count)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
Recursion solves this best
import Foundation
import PlaygroundSupport
// Let asynchronous code run
PlaygroundPage.current.needsIndefiniteExecution = true
func fetch(urls: [URL]) {
guard urls.count > 0 else {
print("Queue finished")
return
}
var pendingURLs = urls
let currentUrl = pendingURLs.removeFirst()
print("\(pendingURLs.count)")
let session = URLSession.shared
let task = session.dataTask(with: currentUrl, completionHandler: { (data, response, error) in
print("task completed")
if let _ = error {
print("error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("server error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
if response.mimeType == "application/json" {
print("json data parsed")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}else {
print("unknown data")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}
})
//start execution after two seconds
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
print("resume called")
task.resume()
}
}
var urls = [URL]()
for _ in 0..<100 {
if let url = URL(string: "https://google.com") {
urls.append(url)
}
}
fetch(urls:urls)
The easiest way is to perform recursive call:
Imagine you have array with your urls.
In place where you initially perform for loop with, replace it with single call get(url:).
self.get(urls[0])
Then add this line at the and of response closure right after self.itemsCount.append(count):
self.urls.removeFirst()
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (_) in
self.get(url: urls[0])
}
Make DispatchQueue to run your code on threads. You don't need to do this work on Main Thread. So,
// make serial queue
let queue = DispatchQueue(label: "getData")
// for delay
func wait(seconds: Double, completion: #escaping () -> Void) {
queue.asyncAfter(deadline: .now() + seconds) { completion() }
}
// usage
for url in urls {
wait(seconds: 2.0) {
self.get(url: url) { (itemCount) in
// update UI related to itemCount
}
}
}
By the way, Your get(url: url) function is not that great.
func get(url: URL, completionHandler: #escaping ([Int]) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
print(error.localizedDescription)
/* Don't need to use main thread
DispatchQueue.main.async {
print(error.localizedDescription)
}
*/
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("Server Error")
/* Don't need to use main thread
DispatchQueue.main.async {
print("Server Error")
}
*/
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
self.itemsCount.append(count)
// append all data that you need and pass it to completion closure
DispatchQueue.main.async {
completionHandler(self.itemsCount)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
I would recommend you to learn concept of GCD(for thread) and escaping closure(for completion handler).
GCD: https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1
Escaping Closure: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID546

a function does internet fetching and return a value

I know the following piece of code is wrong, but I want to show my intent.
I want to write a method that will be called by multiple times. and this fetching method will tell me if it is successfully reached.
func fetch(url: String) -> Bool? {
let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
let url = URL(string: url)
var bool: Bool? = nil
if let url = url {
defaultSession.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
print(error)
return
}
DispatchQueue.main.async {
if let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let data = data {
// handle the data.
bool = true
} else {
print("something really wrong")
bool = false
}
}
}).resume()
}
return bool
}
if let bool = fetch(url: "https://www.google.com.hk/webhp?hl=en&sa=X&ved=0ahUKEwimubK7r-HVAhVFmZQKHazMAMMQPAgD"), bool == true {
// if it is true, I can go for next step.
}
Making the UI wait on completion of some API call is not recommended. The app will have no control over how long that API call will take. Situations with bad network connectivity can take several seconds to respond.
You can handle a situation like this is to use a completion handler.
func fetch(url: String, completion: #escaping (_ success: Bool) -> Void) {
let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
let url = URL(string: url)
if let url = url {
defaultSession.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
print(error)
return
}
if let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let data = data {
// handle the data.
completion(true)
} else {
print("something really wrong")
completion(false)
}
}).resume()
}
}
func testFetch () {
fetch(url: "https://www.google.com.hk/webhp?hl=en&sa=X&ved=0ahUKEwimubK7r-HVAhVFmZQKHazMAMMQPAgD") { (success) in
// if it is true, I can go for next step.
DispatchQueue.main.async {
if success {
// it was good
}
else {
// not good
}
}
}
}