How to call a protocol from an extension before changing screens - swift

I need to get a value from an extension before i click on a button that goes to another screen, how can i do that?
This is the IBAction inside viewController. When i click it makes a request to an API then send a value to global variable on the second screen:
#IBAction func enter(_ sender: UIButton) {
if let login = loginTextfield.text, let password = passwordTextfield.text {
loginManager.performLoginRequest(login, password)
resultsViewController.receivedToken = token
navigationController?.pushViewController(resultsViewController, animated: true)
}
}
extension LoginViewController: LoginManagerDelegate {
func didUpdateLogin(with login: LoginModel) -> (Bool, String) {
success = login.success
token = login.token
return (success, token)
}
}
Manager:
import Foundation
protocol LoginManagerDelegate {
func didUpdateLogin(with login: LoginModel) -> (Bool, String)
}
struct LoginManager {
var delegate: LoginManagerDelegate?
func performLoginRequest(_ login: String, _ password: String) {
let url = URL(string: "https://private-anon-1a0df64d9c-ibmfc.apiary-mock.com/login")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = """
{\n "username": "\(login)",\n "password": "\(password)"\n}
""".data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let response = response {
print(response)
if let data = data, let body = String(data: data, encoding: .utf8) {
print(body)
if let login = self.parseJSON(loginData: data) {
self.delegate?.didUpdateLogin(with: login)
}
}
} else {
print(error ?? "Unknown error")
}
}
task.resume()
}
func parseJSON(loginData: Data) -> LoginModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(LoginData.self, from: loginData)
let success = decodedData.success
let token = decodedData.data.token
let login = LoginModel(success: success, token: token)
return login
} catch {
print(error.localizedDescription)
return nil
}
}
}
My problem is, this extension is just being called after i click the button. This way resultsViewController.receivedToken is not getting the value from token.
So, how can i call didUpdateLogin (to pass it values to success and token) before clicking on the IBAction?

THe reason for this behaviour is the background thread you are using:
(1) You call loginManager.performLoginRequest(login, password) which then starts a background thread to actually work on that request.
(2) In the meantime your code continues to run, executing resultsViewController.receivedToken = token.
(3) Since (1) is not done yet, your token is still nil (or an old token).
One of many possible solutions:
Add a block to the parameters of performLoginRequest in which you call
resultsViewController.receivedToken = token
navigationController?.pushViewController(resultsViewController, animated: true)
This way you make sure that code is only called after(!) the login was successful because you wait for it. In the meantime you could show a loading spinner or something similar. Login is a task where a user simply has to wait, there is usually (depending on the app) no way around it.
The code could look something like this in the end:
loginManager.performLoginRequest(login, password) {
resultsViewController.receivedToken = token
navigationController?.pushViewController(resultsViewController, animated: true)
}
whereas your LoginManager would have a method like
func performLoginRequest(_ login: String,
_ password: String,
completion: #escaping () -> Void)
which is then used later in your Dispatch:
DispatchQueue.main.async {
let loginVC = LoginViewController()
loginVC.didUpdateLogin(login: login)
completion()
}

Related

Authentication error/ purple warning when trying to segue after validating data from API

I have created a POST request which validates if the username and password are correct through a StatusCode: 0 that comes from the response of the POST request if the data are correct, At the signInViewController class I have created the button signInSegueToDashboard which when pressed must validate the data and if the data are valid then the user will be logged in without any problem.
The button sender at signInViewController:
#IBAction func signInSegueToDashboard(_ sender: Any) {
APICallerPOST.shared.signInToAccount(username: emailTextField.text!, password: passwordTextField.text!) { (result, error) in
if let result = result {
if result.StatusCode == 0 {
guard let mainTabBarController = self.storyboard?.instantiateViewController(withIdentifier: "mainTabBarController") else {
return
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) {_ in
mainTabBarController.modalPresentationStyle = .custom
self.present(mainTabBarController, animated: true, completion: nil)
}
}else if result.StatusCode == 5 {
print("error: \(error!.localizedDescription)")
}
}
}
}
When i press the button after typing the correct data it just does nothing and just shows a purple warning that is saying to put it on Main thread, When i did put on main thread the segue part then it doesn't validate the data at all instead it just logs you in without any validation.
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()
}
Confused a lot.
The dataTask is asynchronous, and so is the code it runs in the completion handler. However all updates to UI need to be performed on the main thread, and so the parts of the completion handler that update the UI need to be pushed back onto the main thread.
In your case you could do it like this:
if result.StatusCode == 0 {
DispatchQueue.main.async {
guard let mainTabBarController = self.storyboard?.instantiateViewController(withIdentifier: "mainTabBarController")
else {
return
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) {_ in
mainTabBarController.modalPresentationStyle = .custom
self.present(mainTabBarController, animated: true, completion: nil)
}
}
// ...
However it seems you are trying to use the Timer to delay presentation of the viewController, and there is a better way of doing this than using the Timer. You can use a delayed execution with DispatchQueue's asyncAfter(deadline:qos:flags:execute:) method:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// do your UI work
}

HTTP post request and save response in app

I'm totally new to swift and iOS programming so I'm a little lost on how to do this and even in what files I should be doing this too.
I'm trying to do a http post request to get calendar events and save them in the app to later use and display.
I made a model class with this code.
import UIKit
class Event {
var id: Int
var init_date: String
var end_date: String
var title: String
var description: String
var color_code: String
var all_day: Int
init?(id: Int, init_date: String, end_date: String, title: String, description: String, color_code: String, all_day: Int) {
//Initialization should fail if these are false
if id < 0 || init_date.isEmpty || end_date.isEmpty || title.isEmpty {
return nil
}
//Initialize stored properties
self.id = id
self.init_date = init_date
self.end_date = end_date
self.title = title
self.description = description
self.color_code = color_code
self.all_day = all_day
}
}
But now I don't know what the next step would be. I need this to be downloaded immediately once the app is opened for the first time and not when it's not being opened for the first time. Do I create a new method in the ViewController.swift for the download?
Right now I haven't added anything to the ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
What should I do next?
At this point you need to create a function that handles the POST request you are making.
Once completed, place this function inside your appDelegate main function didFinishLaunchingWithOptions. This is the function that executes on appStart
On a successful function call save the data (presumably json) into a Global Variable or whatever you need for you app.
TIP:
On you class
class Event: Codable {
}
make sure to add Codable like above
Below is an example of what your post request will look like
func myPostRequest(completionHandler: #escaping (Bool?, String?) -> Void){
guard let url = URL(string:"") else { return }
let parameters = ["": ""]
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("", forHTTPHeaderField: "Authorization")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
error == nil
else {
print(error as Any)
return
}
if let httpResponse = response as? HTTPURLResponse {
if (httpResponse.statusCode == 200) {
if let data = data {
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]]
//print("^^^^^^^^^^^^^^",json)
for x in json ?? [] {
//here is where you will parse your data from the post request
}
completionHandler(true, nil)
return
}
} else {
completionHandler(false, "No Response From Server")
print("Failure response: STATUS CODE != 200")
}
} else {
completionHandler(false, "Database Connection Error")
print("Error \(error!)")
}
}
task.resume()
} catch let error {
completionHandler(false, "failure")
print("POSTERROR: \(error.localizedDescription)")
}
}
I use Alamofire, you can add it to your project via:
Pods
Swift Package Manager
When you add the framework you can use it:
import Alamofire
Then you need to make your class with the protocol Codable to pass the data to your class.
class Event: Codable { }
Then you need to call the url and store the response in a variable:
override func viewDidAppear(_ animated: Bool) {
AF.request("your API rest url").responseData { (resData) in
guard let data = resData.data else { return }//Check if the data is valid
do {
let decoder = JSONDecoder()//Initialize a Json decoder variable
let decodedData = try decoder.decode(Event.self, from: data)//Decode the response data to your decodable class
//Print the values
print(decodedData.headers)
print(decodedData.id)
print(decodedData.init_date)
print(decodedData.end_date)
} catch {
print(error.localizedDescription)
}
}
}

Where do Dispatch Group commands go in code?

I am trying to run a function X times in a for in loop but when all the functions have returned I want to run another function.
Currently I have it working by delaying the final function 1 second but I would really like to get Dispatch Group working.
I've been through various online examples and other questions but nothing I try seems to work, The code I have at the moment I know won't work as it is running dispatchGroup.leave() each time the for in functions are sent rather than when they return.
I've tried puting the DispatchGroup code in the function (which is in another file) but I'm stumped, however I think I am close to a solution.
I've also looked at semaphores and using count and incrementing a value each time the loop runs but I keep coming back to DispatchGroups.
My last resort is to ask a question!
ViewController code
#IBAction func removeDeviceBtn(_ sender: Any) {
let dispatchGroup = DispatchGroup()
for owner in arrOwnerList {
dispatchGroup.enter()
self.removeDevice(device: self.device, account: owner as! String, completion: self.completed)
dispatchGroup.leave()
}
dispatchGroup.notify(queue: DispatchQueue.main, execute: {
self.removeDeviceFromServer(device: self.device)
self.sendEmail(to:"gordon#example.co.uk", subject:self.device+" has been removed", text:self.device+" has been removed from the server, please check the sim for bar and termination")
})
Function code in other file as an extension
func completed(isSuccess: Bool) {
}
func removeDevice(device: String, account: String, completion: #escaping (Bool) -> Void) {
let dictHeader : [String:String] = ["username":Username,"password":Password]
let dictArray = [device]
WebHelper.requestPUTAPIRemoveDevice(BaseURL+"rootaccount/removedevices/"+account+"?server=MUIR", header: dictHeader, dictArray: dictArray, controllerView: self, success: { (response) in
if response.count == 0 {
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.ServerError, on: self)
}
}
else {
if response.count != 0 {
let isSuccess = true
completion(isSuccess)
}
else{
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.NoDataFound, on: self)
}
}
}
}) { (error) in
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: error?.localizedDescription ?? Messages.ServerError, on: self)
}
}
}
Code from WebHelper file
class func requestPUTAPIRemoveDevice(_ strURL: String,header: Dictionary<String,String>,dictArray: Array<Any>, controllerView viewController: UIViewController, success: #escaping (_ response: [AnyHashable: Any]) -> Void, failure: #escaping (_ error: Error?) -> Void) {
if GlobalConstant.isReachable() {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.showHUD()
}
let loginString = String(format: "%#:%#", header["username"]!, header["password"]!)
let loginData: Data = loginString.data(using: String.Encoding.utf8)!
let base64LoginString = loginData.base64EncodedString(options: NSData.Base64EncodingOptions())
let headers = ["Authorization": "Basic "+base64LoginString, "Referer": "http://www.example.com"]
let postData = try? JSONSerialization.data(withJSONObject: dictArray, options: [])
let request = NSMutableURLRequest(url: NSURL(string: strURL)! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "PUT"
request.allHTTPHeaderFields = headers
request.httpBody = postData
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
}
failure(error)
} else {
if let httpResponse = response as? HTTPURLResponse {
print("Server code \(httpResponse.statusCode)")
if httpResponse.statusCode == 200 || httpResponse.statusCode == 208 {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
}
let jsonResult = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers)
if (jsonResult is NSDictionary) {
success(jsonResult as! [AnyHashable : Any])
}
else if (jsonResult is NSArray) {
success(["response":jsonResult as! NSArray])
}
else{
success(["response":httpResponse.statusCode])
DispatchQueue.main.async {
}
}
}
else{
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
}
failure(error)
}
}
}
})
dataTask.resume()
}
else {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
GlobalConstant.showAlertMessage(withOkButtonAndTitle: "", andMessage: "Internet not connected", on: viewController)
}
}
}
The final solution (apart from tidying up the various other issues) was to add success(["response":httpResponse.statusCode]) to the WebHelper file, corrected code above
Put the leave inside the completion handler:
for owner in arrOwnerList {
dispatchGroup.enter()
removeDevice(device: device, account: owner as! String) { [weak self] success in
self?.completed(isSuccess: success)
dispatchGroup.leave()
}
}
Or given that you’re not really doing anything in completed function, I’d just remove that:
for owner in arrOwnerList {
dispatchGroup.enter()
removeDevice(device: device, account: owner as! String) { _ in
dispatchGroup.leave()
}
}
I notice that you have paths of execution in removeDevice that aren’t calling the completion handler. Make sure every path of execution calls the completion handler or else your dispatch group will never get resolved.
func removeDevice(device: String, account: String, completion: #escaping (Bool) -> Void) {
let dictHeader = ["username": Username, "password": Password]
let dictArray = [device]
WebHelper.requestPUTAPIRemoveDevice(BaseURL+"rootaccount/removedevices/"+account+"?server=MUIR", header: dictHeader, dictArray: dictArray, controllerView: self, success: { response in
DispatchQueue.main.async {
if response.count == 0 {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.ServerError, on: self)
completion(false)
} else {
completion(true)
}
}
}, failure: { error in
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: error?.localizedDescription ?? Messages.ServerError, on: self)
completion(false)
}
})
}
By the way, I don’t know the name of the “failure” closure, so I assumed it was failure, but adjust as required by your requestPUTAPIRemoveDevice method. We generally avoid the multiple closure pattern in Swift, but if you’re going to do that, I’d avoid the trailing closure syntax. It makes the functional intent of this second closure a bit more explicit.
Or, this all begs the question as to why requestPUTAPIRemoveDevice is initiating UI updates at all. I’d probably put that in the view controller method. So requestPUTAPIRemoveDevice should just pass back enough information so the removeDeviceBtn routines knows what error to present. And this idea of presenting a separate error message for each failure is probably suspect, too. (E.g. if you have lost internet connection and are trying to remove a dozen devices, do you really want to show a dozen separate error messages?) But this is beyond the scope of this question.

How to create proper completion handler for server login in 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
}
}

Get user authentication before next UI View will appear

I am working on user authentication process but i stuck in the moment when reciving data from rest with token. Whenever i create the new task it does not enter on the first time into the function but after creating it skipping doing smth else which is showing a next hooked up UIViewController to segue.
My rest service with post method hashing user password, creating json, URL request and at the end creating URLSession. How could i wait for finish of this task ? To not let to do anything else before it is not complited ?
EDIT
I've added OpeartionQueue to liquidate nil's from next view.
func postLogin(name:String, pass:String, completion: #escaping (Bool) -> () ) {
let md5Data = self.MD5(string:pass)
let hashPass = md5Data!.map { String(format: "%02hhx", $0) }.joined()
let json: [String: Any] = ["username": name,
"passwordHash": hashPass ]
let jsonData = try? JSONSerialization.data(withJSONObject: json)
// create post request
let url = URL(string: LOGIN_URL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
// insert json data to the request
request.httpBody = jsonData
request.setValue("application/json;charest=utf-8", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "No data")
return
}
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseJSON = responseJSON as? [String: Any] {
print(responseJSON)
let message:String = responseJSON["message"] as! String
if !(message.range(of: "ERROR") != nil){
SessionMenager.Instance.token = message
completion(true)
}
} else{
print(error.debugDescription)
}
}
task.resume()
}
Then simply in my LoginViewController action with button :
#IBAction func LoginButton(_ sender: Any) {
let username = usernameTextField.text
let password = passwordTextField.text
if username == "" {
AlertWindow(title: "Username", message: "Wrong username")
} else if password == "" {
AlertWindow(title: "Password", message: "Wrong password")
} else {
let usernameToUpper = username!.uppercased()
RestService.Instance.postLogin(name: usernameToUpper, pass: password!, completion: { sth in
if sth {
OperationQueue.main.addOperation {
[weak self] in
self?.performSegue(withIdentifier: "mapSegue", sender: self)
}
} else {
return
}
})
}
}
The segue was hooked up into LoginButton which took me instantly to the next page. I've changed it into hooking up all view controllerr.
Thanks!
Because your segue is hooked up into LoginButton, it will automatically show the next viewController once you press the button.
Just hoop up the segue to the whole viewController and it should work.