How do I resubmit URLRequest when unsuccessful? - swift

I am unsure how to resubmit a URLRequest when it's unsuccessful.
I have a function that calls several API's. On one of the API requests, about half the time it is unsuccessful in the first attempt and I have to push the button on my app again to have it run the function again until it goes through. I've traced my code to the exact spot where it is getting caught up, but am unsure how I can handle it to keep trying the request until successful. Below is my code.
let request = URLRequest(url: URL(string: urlNew)!)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
// other logic happens here
})
Most of the time it passes through just fine and everything works as expected. How do I make it keep trying the request however in the case it's not successful? It makes it up to "let task ..." just fine, but that's where it gets caught up. For reference:
urlNew = "MyAPI String value is here"

You can call the function recursively when the request is unsuccessful
func callAPI(urlNew:String) {
let request = URLRequest(url: URL(string: urlNew)!)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
// other logic happens here
guard let data = data, error == nil else {
self.callAPI(urlNew:urlNew)
return
}
})
task.resume()
}

Related

URLTask does not send any UrlRequest

I am new to swift and doing a project in swift 4.0 to acquire data form Fitbit API and got a Strange problem, my url task does not send any urlrequest any more but skip all the code until task.resume, and do not give anything back. Can anyone helps me plz. The code is shown below
import UIKit
class FitbitAPI{
static let sharedInstance : FitbitAPI = FitbitAPI()
var parsedJson : [Any]? = nil
func authorize(with token: String){
let accessToken = token
let baseURL = URL(string: "https://api.fitbit.com/1/user/-/activities/steps/date/today/1m.json")
let request = NSMutableURLRequest(url:baseURL!)
let bodydata = "access_token=\(String(describing: accessToken))"
request.httpMethod = "GET"
request.setValue("Bearer \(String(describing: accessToken))", forHTTPHeaderField: "Authorization")
request.httpBody = bodydata.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: {[weak self] (data, response, error) in
if let error = error {
print(error)
}
if let data = data, error == nil{
do {
self?.parsedJson = (try JSONSerialization.jsonObject(with: data, options: []) as? [Any] )
print(String(describing: self?.parsedJson))
}catch _{
print("Received not-well-formatted JSON")
}
}
if let response = response {
let httpResponse = response as! HTTPURLResponse
print("response code = \(httpResponse.statusCode)")
}
})
task.resume()
}
}
As #Larme implied in his comment, all of that code between the let task = line and the task.resume() line is a callback. Meaning it won't get called until the task completes. Put breakpoints inside of that callback (like on your if let error = error line), and see if they get hit.
ALso, your URL task is a local variable in this method. That means it's entirely possible that its getting released from memory right at the end of this method, before the callback can even be executed. You'll need a reference to the task outside of the method if you want to guarantee that it stays alive in memory long enough to hit the completion callback.

Swift: OperationQueue - wait for asynchronous calls to finish before starting next (have maximum of 2 URLRequests running at a time)

tl;dr I have an OperationQueue and I want to have two operations running at the time. Those operations download something asynchronously hence they all get triggered at once instead of running one after another.
I fill a table of very large images by doing doing the following for each of the images:
public func imageFromUrl(_ urlString: String) {
if let url = NSURL(string: urlString) {
let request = NSURLRequest(url: url as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
if let imageData = data as Data? {
DispatchQueue.main.async {
self.setImageData(imageData)
}
}
});
task.resume()
}
calling it like imageView.imageFromUrl(...).
On slower internet connections, the calls stack and it starts loading every image at once. The user then has to wait for the downloads to "fight" each other and is staring at a blank screen for a while before the images all appear at once (more or less). It would be a much better experience for the user if one image appeared after another.
I thought about queuing up the items, downloading the first of the list, drop it from the list and call the function recursively like this:
func doImageQueue(){
let queueItem = imageQueue[0]
if let url = NSURL(string: (queueItem.url)) {
print("if let url")
let request = NSURLRequest(url: url as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
print("in")
if let imageData = data as Data? {
print("if let data")
DispatchQueue.main.async {
queueItem.imageView.setImageData(imageData)
self.imageQueue.remove(at: 0)
if(self.imageQueue.count>0) {
self.doImageQueue()
}
}
}
});
task.resume()
}
}
This does load the images one after another, by I think it's a waste of time not to have at least 2 requests running at a time. Making my current implementation handle 2 images at the same time would result in big spaghetti code so I've looked into Swift's OperationQueue.
I would do
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
for (all images) {
queue.addOperation {
imageView.imageFromUrl(imageURL
}
}
But this also triggers all the calls at once, probably due to the fact that the requests run asynchronously and the method call ends before waiting for the image to be downloaded. How can I deal with that? The app will also run on watchOS, maybe there is a library for this already but I don't think this should be too hard to achieve without a library. Caching isn't a concern.
Your original code with the operation queue and your original iamgeFromUrl method are all you need if you make one small change to imageFromUrl. You need to add a couple lines of code to ensure that imageFromUrl doesn't return until the download is complete.
This can be done using a semaphore.
public func imageFromUrl(_ urlString: String) {
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let semaphore = DispatchSemaphore(value: 0)
let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
semaphore.signal()
if let imageData = data as Data? {
DispatchQueue.main.async {
self.setImageData(imageData)
}
}
});
task.resume()
semaphore.wait()
}
}
As written now, the imageFromUrl will only return once the download completes. This now allows the operation queue to properly run the 2 desired concurrent operations.
Also note the code is modified to avoid using NSURL and NSURLRequest.

How can we wait for HTTP requests to finish?

Using several answers on SO, we have managed to write and execute a basic HTTP request:
import Foundation
let url:URL = URL(string: "http://jsonplaceholder.typicode.com/posts")!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
let paramString = "data=Hello"
request.httpBody = paramString.data(using: String.Encoding.utf8)
let task = session.dataTask(with: request as URLRequest) {
(data, response, error) in
guard let data = data, let _:URLResponse = response, error == nil else {
print("error")
return
}
let dataString: String = String(data: data, encoding: String.Encoding.utf8)!
print("here")
print("Data: \(dataString)")
print("Response: \(response!)")
}
task.resume()
while task.response == nil {}
print("Done")
You'll note that we already busy-wait until task.response is set. However, neither data nor response are printed, only here.
After endless trials with wrapping things this or that way we determine that we have a Heisenbug here: changing nothing in the code, sometimes here is printed, sometimes nothing, and very, very rarely dataString (let alone response).
So we insert sleep(3) before print("Done") and, wonder of wonders, we get all prints.
Then we yelled a little bit (I may actually have thrown something), thought briefly about abandoning Swift altogether, but then calmed down enough to facepalm like sirs and post here.
Apparently, the main thread terminates whether or not any asynchronous tasks (threads?) are still running or not, killing all its spawn. How can we prevent that from happening, that is "join" the threads?
Bonus question: Does Alamofire deal with this behind the covers?
Using CwUtils by Matt Gallagher, I implemented a simple CountdownLatch which does the job:
import Foundation
import CwlUtils
<...>
let task = session.dataTask(with: request as URLRequest) {
(data, response, error) in
<...>
latch.countDown()
}
task.resume()
latch.await()
The most straight-forward (and built-in) way is probably to use a DispatchSemaphore:
<...>
let sem = DispatchSemaphore(value: 0)
let task = session.dataTask(with: request as URLRequest) {
(data, response, error) in
<...>
sem.signal()
}
task.resume()
sem.wait()
Active waiting seems to be the only way on the GCD. Using standard library material, this is what works:
import Foundation
<...>
var done = false
let task = session.dataTask(with: request as URLRequest) {
(data, response, error) in
<...>
done = true
}
task.resume()
repeat {
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1))
} while !done

datataskwithURL() completion block not being called. Xcode swift

I am trying to write a basic function to make an HTTP Get request and parse the xml data that comes back. I already have completed the section for parsing XML from a local file, but i can't seem to get any data from the server. I tested this code below, but the completion block does not even run, for information to be passed back from the server. Any suggestions please.
func getDatafromURL (url: String) {
guard let urlforRequest = NSURL(string: url) else {
print("Error: cannot create URL")
return
}
let request = NSURLRequest(URL: urlforRequest)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
print(response)
print(error)
}
task.resume()
}

Swift - proper way to wait for login request to complete before resuming execution

I have the following code which performs a login to a web server, retrieves an API token, and then allows the app to continue. After this code executes, the the calling function uses the login_result to report to the user whether they successfully logged in or need to try again.
Right now, I just have the app sleep for 3 seconds to make sure I have response before moving on. Otherwise, the login results handler starts executing without an answer (because the server hasn't had time to respond).
Obviously, this is a horrible way to do things. What is the correct / swift idiomatic way to handle this?
func remote_login (email: String, password: String) -> Bool {
login_result = false
var jsonResults: NSDictionary = [:]
let account = Account()
let url = NSURL(string: "\(base_api_url())/login?email=\(email)&password=\(password)")
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
do {
jsonResults = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSDictionary
account.setCredentials(jsonResults) // we store an API token returned from a successful login; we don't store the users credentials locally
self.login_result = true
}
catch {
print("\n\n\n Login Error \n\n\n")
}
}) // task
task.resume()
sleep(3) //need time for the response to come back...s/b far more elegant way to do this.
return self.login_result
}
Use a closure as your function argument, like so:
func remote_login(email: String, password: String, completion((Bool) -> Void)) {
Then, call the closure when finished (after JSON parsing) like this:
self.login_result = true
completion(login_result)
So, when you use the function, it will look like this:
remote_login("email", "password", {
success in
//Logged in successfully
}
The closure will be called when the HTTP request finishes.
As best I can see:
func remote_login (email: String, password: String, myFunctionCompletionHandler: ()->()) -> Bool {
login_result = false
var jsonResults: NSDictionary = [:]
let account = Account()
let url = NSURL(string: "\(base_api_url())/login?email=\(email)&password=\(password)")
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
do {
jsonResults = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSDictionary
// we store an API token returned from a successful login; we don't store the users credentials locally
account.setCredentials(jsonResults)
myFunctionCompletionHandler()
// is this needed as it's handled by the handler?
self.login_result = true
}
catch {
myFunctionCompletionHandler()
print("\n\n\n Login Error \n\n\n")
}
}) // task
task.resume()
return self.login_result
}
You might have to refactor things to handle the completionHandler a little better as I've just literally "plonked it in" but that should do the trick :)..
Just handle and pass the "failed and passed" correctly across to it :)