swift, calling network another time in the same task's thread - ios

Sorry for beginner's question.
I have an action that depends on the result of the data returned from the network, and the action may require another network request. Since the first network request is called in datatask already, I want to use that same thread for the 2nd network request, but don't know how to do it. Any tip? tks
let task = URLSession.shared.dataTask(with: request as URLRequest )
{ data, response, error in
if func_1 (data) {
return
}else {
//call another network request here, but don't want to do
//another task = URLSession.shared.... again since we are already on an async thread
}
}

//call another network request here, but don't want to do
//another task = URLSession.shared.... again since we are already on an async thread
This is a misunderstanding. There is no problem creating another network request at this point. All that does is place another request on the appropriate queue. It doesn't matter what thread you're running on when you schedule that.
I highly recommend Apple's Concurrency Programming Guide. You may have some misunderstandings about how iOS concurrency works. A key part of iOS work is understanding GCD queues, and particularly how they differ from threads.

Related

Swift Siesta Framework: do something before sending requests

I am trying the Siesta framework and I want to call a function before sending every API calls.
I saw that decorateRequests(with:) is best suited for what I am looking to do, but as the return value must be a Request, there's an error on the following code:
service.decorateRequests(with: { (res, req) -> Request in
if (res.url == self.tests.url) {
// do things..., then call req.repeated()
} else {
req.repeated()
}
})
However, I have this error:
Missing return in a closure expected to return 'Request'
Any idea how I can make it work? Thanks
The basic Swift syntax error here is that you need to use the return keyword if a value-returning closure contains more than one statement.
If what you need to do is something that either:
can synchronously block the main thread because it is brief (e.g. log the request, flip a test expectation), or
needs to be started when the request is sent, but the request can go ahead and start immediately without waiting for it to finish
…then it needn’t be complicated. Do your brief task while everyone waits, then return the request:
service.decorateRequests { req, res in
if res.url == self.tests.url {
doThatOtherThing() // blocks the main thread, which is fine if it’s brief
}
return req
}
If on the other hand you need to do something that will take an indefinite amount of time while the main thread continues, and then at a later time initiate that request, then Siesta currently doesn’t support that very well. You can do it by writing a custom implementation of the Request protocol, but that's laborious and error prone. A better approach is coming in a future version of Siesta.

Pattern for retrying URLSession dataTask?

I'm fairly new to iOS/Swift development and I'm working on an app that makes several requests to a REST API. Here's a sample of one of those calls which retrieves "messages":
func getMessages() {
let endpoint = "/api/outgoingMessages"
let parameters: [String: Any] = [
"limit" : 100,
"sortOrder" : "ASC"
]
guard let url = createURLWithComponents(endpoint: endpoint, parameters: parameters) else {
print("Failed to create URL!")
return
}
do {
var request = try URLRequest(url: url, method: .get)
let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, response, error) in
if let error = error {
print("Request failed with error: \(error)")
// TODO: retry failed request
} else if let data = data, let response = response as? HTTPURLResponse {
if response.statusCode == 200 {
// process data here
} else {
// TODO: retry failed request
}
}
}
task.resume()
} catch {
print("Failed to construct URL: \(error)")
}
}
Of course, it's possible for this request to fail for a number of different reasons (server is unreachable, request timed out, server returns something other than 200, etc). If my request fails, I'd like to have the ability to retry it, perhaps even with a delay before the next attempt. I didn't see any guidance on this scenario in Apple's documentation but I found a couple of related discussions on SO. Unfortunately, both of those were a few years old and in Objective-C which I've never worked with. Are there any common patterns or implementations for doing something like this in Swift?
This question is airing on the side of opinion-based, and is rather broad, but I bet most are similar, so here goes.
For data updates that trigger UI changes:
(e.g. a table populated with data, or images loading) the general rule of thumb is to notify the user in a non-obstructing way, like so:
And then have a pull-to-refresh control or a refresh button.
For background data updates that don't impact the user's actions or behavior:
You could easily add a retry counter into your request result depending on the code - but I'd be careful with this one and build out some more intelligent logic. For example, given the following status codes, you might want to handle things differently:
5xx: Something is wrong with your server. You may want to delay the retry for 30s or a minute, but if it happens 3 or 4 times, you're going to want to stop hammering your back end.
401: The authenticated user may no longer be authorized to call your API. You're not going to want to retry this at all; instead, you'd probably want to log the user out so the next time they use your app they're prompted to re-authenticate.
Network time-out/lost connection: Retrying is irrelevant until connection is re-established. You could write some logic around your reachability handler to queue background requests for actioning the next time network connectivity is available.
And finally, as we touched on in the comments, you might want to look at notification-driven background app refreshing. This is where instead of polling your server for changes, you can send a notification to tell the app to update itself even when it's not running in the foreground. If you're clever enough, you can have your server repeat notifications to your app until the app has confirmed receipt - this solves for connectivity failures and a myriad of other server response error codes in a consistent way.
I'd categorize three methods for handling retry:
Reachability Retry
Reachability is a fancy way of saying "let me know when network connection has changed". Apple has some snippets for this, but they aren't fun to look at — my recommendation is to use something like Ashley Mill's Reachability replacement.
In addition to Reachability, Apple provides a waitsForConnectivity (iOS 11+) property that you can set on the URLSession configuration. By setting it, you are alerted via the URLSessionDataDelegate when a task is waiting for a network connection. You could use that opportunity to enable an offline mode or display something to the user.
Manual Retry
Let the user decide when to retry the request. I'd say this is most commonly implemented using a "pull to refresh" gesture/UI.
Timed/Auto Retry
Wait for a few second and try again.
Apple's Combine framework provides a convenient way to retry failed network requests. See Processing URL Session Data Task Results with Combine
From Apple Docs: Life Cycle of a URL Session (deprecated)... your app should not retry [a request] immediately, however. Instead, it should use reachability APIs to determine whether the server is reachable, and should make a new request only when it receives a notification that reachability has changed.

Alamofire cancel sometimes does not work

I have a simple request like:
func newRequest() {
println("CANCEL = \(self.getTime())")
self.request_.cancel()
self.request_ = request(method, url)
validate(statusCode: [200])
.validate(contentType: ["application/json"])
.responseJSON { [unowned self] (_, _, json, error) in
if(error?.code == NSURLErrorCancelled ) {
println("CANCELED!")
}
println("DONE LOADING = \(self.getTime())")
// ...
}
}
As shown above, when new request is invoked, I want previous to be canceled.
And it usually works, but sometimes when previous request is about to end (there is a very short amount of time between logs), it does not.
(newRequest) CANCEL = 1436103465.93128
// CANCELED! SHOULD BE HERE
(previousRequest) DONE LOADING = 1436103466.08223
To make it work I added a var isCanceled and check whether it is set to true.
I am not sure if it works as it should (it may be too late to cancel) or it is a small bug.
You expectation here is incorrect. The cancellation is not a synchronous behavior. It is asynchronous and needs to hop several dispatch queues before your responseJSON closure will be called. Since you are repointing the self.request_ reference to a new request, your previous request is actually going out of memory and your responseJSON closure for the previous request will never be executed.
Instead of using a single reference to one request, you could use your self.request_ property to store the latest request, and use a requests set to store all the active requests. Once the responseJSON closure is called, make sure to remove the request from the requests set. This way, you would keep a reference to all requests until they finished properly canceling.
Believe me, Alamofire cancellation works properly. Our giant test suite agrees. 👍🏼

Force AFNetwork POST request to be synchronous

I have a button that connects a view controller to another via a segue and when pressed, sends the text from a UITextField as a parameter to an AFNetwork POST request and must decide whether the segue can continue or not depending on whether the request was successful or not.
So I wrote this:
var proceed = false
let token = tokenTextField.text.trim()
let requestURL = "https://myapi.com/authenticate/"
// Make this a synchronous HTTP POST request so that we only decide whether
// to proceed with the segue or not once we know if the request was successful
manager.POST(requestURL,
parameters: [ "code" : token ],
success: { (operation: AFHTTPRequestOperation!, responseObject: AnyObject!) in
proceed = true
NSLog("Success! Response is \(responseObject.description)")
},
failure: { (operation: AFHTTPRequestOperation!, error: NSError!) in
println("Failure! Error is: \(error.localizedDescription)")
proceed = false
self.displayLoginAttempErrorAlert()
}).waitUntilFinished()
println("what a hard thing")
return proceed
But this code prints "what a hard thing" before anything printed within the success or error callbacks, so it is obviously asynchronous. For the reasons explained above, I want it to be synchronous. How can I force that behavior?
Please bear in mind that there is nothing wrong with the request itself. I just wanna ensure the right order of things.
UPDATE: I'm aware that forcing things to be synchronous may block the UI and that's fine for my purpose. This is like a login screen where nothing should really happen between the issuing of the request and the arrival of the response.
Thanks in advance
You can:
specify the completionQueue of the manager to be something other than the main queue (so that you don't deadlock when you block the main thread, waiting for the completion handler to be called);
dispatch_semaphore_semaphore before you start the request;
dispatch_semaphore_signal the semaphore inside the completion block of the request; and
dispatch_semaphore_wait for the semaphore after the request, but before you return from your function.
But making an asynchronous method behave synchronously is fundamentally the incorrect approach, in my opinion. Not only is it a poor UX, limits your UI, etc., but also you risk having the watchdog process kill your app.
Rather than making an asynchronous process behave synchronously in shouldPerformSegueWithIdentifier, I would instead employ a standard asynchronous pattern:
remove the existing segue you presumably have from the UIKit control to the next scene;
create a new segue between the view controllers (as shown here) that won't be performed automatically, but only programmatically (see below);
have an IBAction that performs the asynchronous request, and have a completion block that
initiates the segue programmatically, as needed.
Why dont you call your viewController in success block... we also have the same situation where user has a login screen and while authentication if failure occurs then we need to show alert else on success redirect it to homeViewController.
If you use queueCompletion block it will be same as handling it in success block if you are executing only one operation in queue.
Still needs a synchronous call then use NSURLConnection class of iOS
-[NSURLConnection sendSynchronousRequest:returningResponse:error]

How return data from an HTTP request in Swift/Objective C

I'm trying to use Coinbase's API to get information about my online bitcoin wallet, and I'm trying to use Swift's NSURLSession object to do so. Perhaps I'm missing something obvious in the Apple docs, but after reading through the information about NSURLSession and NSURLSessionTask I still do not understand how to make an HTTP request and then return the body of the response so that the body can persist throughout the life of my app. As of now I only see the ability to use completion blocks which return void, or delegates which either return void themselves or use completion blocks which also return void. I want to use the data I get from the response later in the app, but because I'm using completion blocks I must handle the response data immediately after the response arrives.
To make it clear, I want to do something along the lines of the pseudocode function below:
func makeHTTPCall(urlString : String) -> String? {
create NSURLSession object
create request with proper headers and using the passed-in urlString
use the session object to send out the request
get the response object, extract the response body as a string, and return it
}
Then later, I could call something like this:
let myObject : MyObject = MyObject()
let respData : String = myObject.makeHTTPCall("https://coinbase.com/api/v1/account/balance")
This data is returning a JSON Object string, which is the String I want to persist beyond the life of the response and its completion block. How can I do this in either Swift or Objective C, since I'll be able to use either in Xcode 6?
EDIT: Two answers have been posted, but they miss the fundamental point of this question. I need to RETURN the data which I receive from the response. Both answers (and all other answers I've seen on SO) simply print the data received. I would like code that doesn't use a void-returning completion handler, but instead returns the data so that it can be used later in the lifecycle of the app. If there is anything unclear about my question, please tell me, though I don't see how this can be made clearer.
In the edit to your question, you say:
I need to RETURN the data which I receive from the response. Both answers (and all other answers I've seen on SO) simply print the data received. I would like code that doesn't use a void-returning completion handler, but instead returns the data so that it can be used later in the lifecycle of the app. If there is anything unclear about my question, please tell me, though I don't see how this can be made clearer.
I understand the appeal of this strategy, because it feels so intuitively logical. The problem is that your networking requests should always run asynchronously (e.g. use that completion handler pattern to which you allude).
While there are techniques making a function "wait" for the asynchronous request to complete (i.e. to make the asynchronous NSURLSession method behave synchronously or use one of the old synchronous network request methods), this is a really bad idea for a number of reasons:
If you do this from the main thread, it results in a horrible user experience (the app will be unresponsive while the request is in progress and the user won't know if the app is busy doing something or whether it's frozen for some unknown reason).
Again, if you do this from the main thread, you also risk having the iOS "watch dog" process kill your app (because if you block the main queue for more than a few seconds at the wrong time, particularly as the app comes to foreground, the OS will unceremoniously terminate your app). See Technical Q&A #1693 for a discussion on the problems of doing synchronous network requests.
We generally prefer the asynchronous network techniques because they offer more features unavailable with synchronous techniques (e.g. making requests cancelable, offer progress updates when using delegate-based network requests, etc.).
You really should use the completion handler pattern that those other questions suggest, and manage the changing state of the app in those handlers. In those situations where you absolutely cannot let the user proceed until some network request is done (e.g. you can't let the user buy something until you confirm their bitcoin balance, and you can't do that until they log in), then change the UI to indicate that such a request is in progress. For example, dim the UI, disable the controls, pop up an activity indicator view (a.k.a., a "spinner"), etc. Only then would you initiate the request. And upon completion of the request, you would restore the UI. I know it seems like a lot, but it's the right way to do it when the user absolutely cannot proceed until the prior request is done.
I'd also think long and hard as to whether it's truly the case that you absolutely have to force the user to wait for the prior network request to complete. Sometimes there are situations where you can let the user do/review something else while the network request is in progress. Yes, sometimes that isn't possible, but if you can find those sorts of opportunities in your app, you'll end up with a more elegant UX.
I know that problem and use this code for synchronous requests:
func synchronousRequest() -> NSDictionary {
//creating the request
let url: NSURL! = NSURL(string: "exampledomain/...")
var request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
var error: NSError?
var response: NSURLResponse?
let urlData = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)
error = nil
let resultDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(urlData!, options: NSJSONReadingOptions.MutableContainers, error: &error) as! NSDictionary
return resultDictionary
}
What you are asking for is a synchronous network request. There are many ways to do this, such as...
NSData's init(contentsOfURL aURL: NSURL!)
NSURLConnection's synchronous request method
...etc.
These methods will block the current thread until they complete - which can be a potentially long time. Network requests can have very high timeouts, it may be several minutes before the device gives up. NSData's init with contents of URL will return NSData, not void, and does not execute asynchronously. It will block until it is complete, which is why it's recommended to not do these types of requests from the main thread. The UI will be frozen until it completes.
In general the use of synchronous networking methods is discouraged. Asynchronous network requests are greatly preferred for a number of reasons. Using an asynchronous method that takes a completion block as a parameter will not prevent you from using the returned data elsewhere in your application. The block is executed when the network request has finished (wether it succeeds or fails) and it is passed the data, response metadata, and error. You are free to do what you want with that data - nothing prevents you from persisting it, passing it off to another object, etc. Based on your comments it sounds like you want to take the data that was the result of the network request and set it as the value of a property somewhere - that is entirely doable using an asynchronous method that uses a block as a completion handler.
In objective-C you can use __block and get the data when the operation finishes:
__block NSData *myData;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:urlString]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
myData = data;
}] resume];

Resources