viewing response from URLSession - swift3

I have an endpoint which takes in a phone number and sends a code to the number, but also returns that same message to the data section of the session that called it.
All of that works, but the problem I'm having is that, after the session makes the call, I'm segueing to the next screen and i'm passing that code into the next controller. But i think the api is responding too slow, so by time the segue (and prep for segue) has happened the code has not been returned yet. How can i fix this?
let scriptURL = "https://---------------/api/verify/sms?"
let urlWithParams = scriptURL + "number=\(phone.text!)"
let myUrl = NSURL(string: urlWithParams)
let request = NSMutableURLRequest(url: myUrl! as URL)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
//print(error?.localizedDescription)
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as AnyObject
self.currentCode = json["code"]!! as! String //-> This is the code the is returned from the api call
}catch{
print("error with serializing JSON: \(error)")
}
}
task.resume()
self.performSegue(withIdentifier: "toVerifyCode", sender: (Any?).self)
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if segue.identifier == "toVerifyCode"{
let newController = segue.destination as! verifyCodeController
newController.code = self.currentCode
}
}

The problem is that you placed self.performSegue(withIdentifier: "toVerifyCode", sender: (Any?).self) not in the closure.
So, you have to place it like this:
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
//print(error?.localizedDescription)
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as AnyObject
//on main thread
DispatchQueue.main.async {
self.currentCode = json["code"]!! as! String //-> This is the code the is returned from the api call
self.performSegue(withIdentifier: "toVerifyCode", sender: (Any?).self)
}
}catch{
print("error with serializing JSON: \(error)")
}
}
task.resume()
Also, please note that your closure is executed asynchronously, so I wrapped the call to be executed on main thread by using GCD.

Related

URLSession HTTP Error not updating back in view unless the action is initiated again

I have a view with a button that calls an API, the API either returns an HTTP code 200 or 400 based on a particular scenario.
The button works just fine and everything works smoothly if code 200 is returned, however if code 400 is returned, the view is not updated that the user have to click on the button once again to get the updated message.
I added the http code property as a published variable in the VM's class and the http is an observable, but it doesn't get updated in the view on the first API call, I'm not sure what I'm missing.
I made a lot of changes to the shared code just to help in demonstrating the actual problem.
Update: Also I think another part of the problem, is that the url function returns the value before the url session returns the data, I don't know why this is happening, that when I execute it a second time it uses the values from the previous execution.
HTTPError Class
class HTTPError : Codable, ObservableObject {
var statusCode: Int?
var message: [String]?
var error: String?
init(statusCode: Int? = nil, message: [String]? = [], error: String? = nil){
self.statusCode = statusCode
self.message = message ?? []
self.error = error
}
convenience required init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.statusCode = try container.decodeIfPresent(Int.self, forKey: .statusCode)
do {
self.message = try container.decodeIfPresent([String].self, forKey: .message)
} catch {
guard let value = try container.decodeIfPresent(String.self, forKey:
.message) else {return}
self.message = []
self.message?.append(value)
}
self.error = try container.decodeIfPresent(String.self, forKey: .error)
}
VM Class
class VM: ObservableObject {
#Published var isLoading = true
#Published var httpError = HTTPError()
func checkDriverIn(_ record: DriverQRParam) async -> (Bool) {
...
var request = URLRequest(url: url)
...
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
...
let task = URLSession.shared.dataTask(with: request) { (data, response,
error) in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
self.httpError = try! JSONDecoder().decode(HTTPError.self, from: data)
//gets updated just fine in this class//
}
task.resume()
}catch {
print("Couldn't encode data \(String(describing: error))")
}
if httpError.statusCode != nil && httpError.statusCode == 400 {
return (false)
} else {
return (true)
}
}
View.Swift
struct xyz: View {
#State private var VM = VM()
Button("click") {
Task {
await VM.checkDriverIn(driverParam)
}
}
}

timing issue after completion of URLRequest

i have a behavior i cant resolve.
I have a controller (controller 1) where i check some defaults value. if not present (first time use of app: check login and pwd) i present (modal) a settings vc:
IN CONTROLLER 1
override func viewDidAppear(_ animated: Bool) {
if(!isKeyPresentInUserDefaults(key: "username")) {
NSLog("username not present")
let vc = self.storyboard?.instantiateViewController(withIdentifier: "settings") as! SettingsViewController
self.present(vc, animated: true, completion: nil)
}
}
in this vc (controller 2) (also in the main storyboard) i have a button done. When pressed, it is associated to the:
IN CONTROLLER 2: SettingsVc -> ID : settings
#IBAction func doneSettings(sender: AnyObject) {
if(isInternetAvailable()) {
NSLog("internet available")
login { (result) in
switch result
{
case .Success(let result):
//print(result)
self.dismissSelf()
break
case .Failure(let error):
print(error)
break
}
}
}
else {
NSLog("internet not available")
}
}
the dismissSelf func is defined in the Settingsvc as:
func dismissSelf() {
NSLog("dismissSettingsVC")
self.dismiss(animated: false, completion: nil)
}
the login func is defined in another class, dealing with networking stuff and is as is:
func login(completion: #escaping (AsyncResult<[CustomUserObject]>)->())
{
let myUrl = URL(string: "http://www.xxxx.com/api/api.php");
var request = URLRequest(url:myUrl!)
request.httpMethod = "POST"// Compose a query string
let postString = "u=login&s=password&cmd=login";
request.httpBody = postString.data(using: String.Encoding.utf8);
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest){
(data, response, error) -> Void in
if let error = error
{
completion(AsyncResult.Failure(error as NSError?))
} else {
let result: [CustomUserObject] = []//deserialization json data into array of [CustomUserObject]
let responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("*********response data = \(responseString)")
completion(AsyncResult.Success(result))
}
}
task.resume()
}
So, I launch the app for the first time (no login, no pwd in defaults), then the Settingvc is presented. I press the done button with param hardcoded. The login is correctly called, I receive the answer correctly, and on completion i should dismiss the Settingvc.
thing is, i see the NSLOG for dismiss, but the dismiss appears seconds after the network func completion (from 10 secs to up to a minute), which I can't understand. Why such a delay? any idea?
2017-11-27 21:54:33.262892 app[998:211517] username not present
2017-11-27 21:54:36.119754 app[998:211517] internet available
*********response data =
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-27 21:54:38.472306 app[998:211542] dismissSettingsVC
2017-11-27 21:54:48.048095 app[998:211517] username not present
in this case, it took 10 sec to dismiss the Settingsvc after receiving the login results.
another one:
2017-11-27 22:04:20.364097 app[998:211517] internet available
*********response data =
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-27 22:04:22.495642 app[998:212974] dismissSettingsVC
2017-11-27 22:05:00.049177 app[998:211517] username not present
in this other case, it took 38 sec to dismiss the Settingsvc after receiving the login results.
EDITED
I tried not using a vc presented. Instead in controller 1, i added a view that i first set as visible if username in defaults does not exist and then that I will hide after the login completion. in this view, i added a button to call the loginAction.
#IBAction func loginAction(sender: AnyObject) {
if(isInternetAvailable()) {
NSLog("internet available")
login { (result) in
switch result
{
case .Success(let users):
print(users)
self.loginView.isHidden = true
NSLog("login ok: hiding view")
break
case .Failure(let error):
print(error ?? "ERROR")
break
}
}
}
else {
NSLog("internet not available")
}
}
Same result:
I see the completion and the received data:
2017-11-28 18:17:34.314706 cellar[1270:311710] username not present
2017-11-28 18:17:35.066333 cellar[1270:311710] internet available
2017-11-28 18:17:35.076930 cellar[1270:311710] done login
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-28 18:17:37.655829 cellar[1270:311763] login ok: hiding view
the view should be hidden before the NSLOG "login ok: hiding view". Instead, the UI is updated seconds after (about a min, but variable)
What would avoid the UI to be updated for so long as I wait the completion of the network stuff to perform the UI update?
UPDATE:
weird situation:
as soon as I get the network completion result, by changing the orientation, the dismiss appears right away:
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-28 22:28:30.620408 cellar[1461:360470] dismiss
2017-11-28 22:28:31.537588 cellar[1461:360413] username not present
2017-11-28 22:28:32.126759 cellar[1461:360413] [App] if we're in the
real pre-commit handler we can't actually add any new fences due to CA
restriction
your help is much that appreciated. Thanks
Not sure if this is the reason for your problem but it looks like you aren't running the dismiss() call on the main thread. You should call all UI code on the main thread. Wrap it as follows
case .Success(let result):
//print(result)
DispatchQueue.main.async {
self.dismissSelf()
}
break

unable to save the data in api using POST method in swift

I am not able to see the changes as per required in API, when I am using the Post method to do so can I get the correct solution. Here is the Code that I have written
#IBAction func savebutton(_ sender: Any) {
if let id1 = UserDefaults.standard.string(forKey: "Userid"),let usertype1 = UserDefaults.standard.string(forKey: "Usertype")
{
var request = URLRequest(url: URL(string: "http://www.shreetechnosolution.com/funded/donorprofile.php")!)
request.httpMethod = "POST"
let postString = "Name=\(textname.text!)&Gender=\(textGender.text!)&Email=\(textemail.text!)&MobileNo=\(textmb.text!)&Country=\(textcountry.text!)&donorid=\(id1)&usertype=\(usertype1)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(String(describing: error))")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(String(describing: response))")
}
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! NSDictionary
let msg = json.value(forKey: "message") as! NSString!
let alert : UIAlertView = UIAlertView(title: "Alert box!", message: "\(msg!).",delegate: nil, cancelButtonTitle: "OK")
alert.show()
}
}
}
Can anyone help me, please?

Swift 3 submit form - UITextField changes only after focusing field again

I am working on a login view and trying to change the border color of a UITextField in Xcode/swift3 when validation of the textfield fails. The UITextField should get a red border color.
The problem is that if enter an email, then a password and then press the submit button, i have to focus email text field again before it gets a red border.
This is my LoginViewController.swift so far:
import Foundation
import UIKit
class LoginViewController : UIViewController, UITextFieldDelegate {
#IBOutlet weak var userEmailTextField: UITextField!
#IBOutlet weak var userPasswordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
// login button action
#IBAction func loginButtonTabbed(_ sender: Any) {
// getting values from text fields
let userEmail = userEmailTextField.text;
let userPassword = userPasswordTextField.text;
// set enpoind data
let requestURL = NSURL(string: Constants.apiUrl)
//creating a task to send the post request
var request = URLRequest(url: requestURL as! URL)
request.httpMethod = "POST"
let postString = "cmd=addUser&email="+userEmail!+"&password="+userPassword!
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
do {
let json = try? JSONSerialization.jsonObject(with: data, options: [])
// store json response to dictionary
if let dictionary = json as? [String: Any] {
// check if we got validation errors
if let nestedDictionary = dictionary["validation"] as? [String: Any] {
// display validation messages on device
if let emailMsg = nestedDictionary["Email"] as? String { // change color of textfield
self.userEmailTextField.errorField()
}
}
}
} catch let error as NSError {
print(error)
}
}
//executing the task
task.resume()
}
}
and the UITextField extension UITextField.swift:
import Foundation
import UIKit
extension UITextField {
func errorField(){
self.layer.borderColor = UIColor(red: 255/255.0, green: 59/255.0, blue: 48/255.0, alpha: 1.0).cgColor
self.layer.borderWidth = 1.0;
}
}
When you're doing a network call, it always happens in the background...so in order to do any kind of UI updates you need to be on the main queue. Just put the self.userEmailTextField.errorField() inside DispatchQueue.main.async {...} so it would be done immediately.
Also haven't really tested your code very well. Why?
Even in your current code the border would still turn red, but it turns red after almost like 6-7 seconds (it could take less or more for you)...because it's being ran from background thread.
What I don't understand is why clicking on the textField again brings the red border right away!? Here's what I'm guessing happens:
From the background thread you update the model ie change the textField color which queues the UI/view to be updated...but since we're on a background queue, that UI updated could take a few seconds to happen
But then you tapped on the textField right away and forced a super quick read of the textField and all its properties which includes the border—from main thread (actual user touches are always handled through main thread)...which even though are not yet red on the screen, but since it's red on the model it will read from it and change color to red immediately.

Swift 3 and NSURLSession issue

Thanks to Apple my iOS 9 Project 'Swift 2.3' is completely unusable with iOS 10's 'Swift 3'...
I fixed almost everything except that I am having issue with using NSURLSession, Xcode is telling me that it has been renamed to URLSession, if I rename it Xcode will tell me:
use of undeclared type URLSession
Foundation is imported.
What is the issue?!
For example I am using it this way...
lazy var defaultSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "reCoded.BGDownload")
configuration.sessionSendsLaunchEvents = true
configuration.isDiscretionary = true
let session = URLSession(configuration: configuration, delegate: self, delegateQueue, queue: nil)
return session
}()
and even with the delegate methods the same issue.
Try using Foundation.URLSession where ever you use URLSession.
/Got it to work/ In some cases try to copy your code somewhere else then remove everything in your class that uses URLSession then type the session methods again and put back your copied code you should be fine.
Update your URLSessin functions with;
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.data.append(data as Data)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
print("Failed to download data")
}else {
print("Data downloaded")
self.parseJSON()
}
}
I can explain how but by playing around with the code I got this to work in SWIFT 3 after two days of frustration. I guess SWIFT 3 removed a lot of unnecessary words.
let task = Foundation.URLSession.shared.dataTask(with: <#T##URL#>, completionHandler: <#T##(Data?, URLResponse?, Error?) -> Void#>)
Here's where I am right now. It's not perfect but works maybe half of the time.
First, in the class where my URLsession is defined:
import Foundation
class Central: NSObject, URLSessionDataDelegate, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate {
I don't think all of that is necessary, but there it is. Then here is the function that is called by my background fetch:
func getWebData() {
var defaults: UserDefaults = UserDefaults.standard
let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: "myBGconfig")
let backgroundSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil)
urlString = "https://www.powersmartpricing.org/psp/servlet?type=dayslider"
if let url = URL(string: urlString) {
let rateTask = backgroundSession.downloadTask(with: URL(string: urlString)!)
rateTask.taskDescription = "rate"
rateTask.resume()
}
When the task comes back:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL ) {
if downloadTask.taskDescription == "rate" { // I run 2 web tasks during the session
if let data = NSData(contentsOf: location) {
var return1 = String(data: data as! Data, encoding: String.Encoding.utf8)!
DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + 0.2){
var defaults: UserDefaults = UserDefaults.standard
defaults.set(myNumber, forKey: "electricRate") // myNumber is an extract of the text in returned web data
defaults.set(Date(), forKey: "rateUpdate")
defaults.synchronize()
self.calcSetting() //Calls another function defined in the same class. That function sends the user a notification.
let notificationName = Notification.Name("GotWebData")
NotificationCenter.default.post(name: notificationName, object: nil)
} // Closes the Dispatch
}
if session.configuration.identifier == "myBGconfig" {
print("about to invalidate the session")
session.invalidateAndCancel()
}
}
I haven't figured out yet how to kill the session when BOTH tasks have completed, so right now I kill it when either one is complete, with invalidateAndCancel as above.
And finally, to catch errors:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didCompleteWithError: Error?) {
if downloadTask.taskDescription == "rate" {
print("rate download failed with error \(didCompleteWithError)")
}
if downloadTask.taskDescription == "other" {
print("other download failed with error \(didCompleteWithError)")
}
downloadTask.resume() // I'm hoping this retries if a task fails?
}
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
if let error = error as? NSError {
print("invalidate, error %# / %d", error.domain, error.code)
} else {
print("invalidate, no error")
}
}