Execute GET request synchronously - ios

I have a function that sends a GET request to an API and I need to wait for the response before I can continue with the rest of my code.
I have tried various different threads but haven't got it to work. I've been trying to solve this by myself but I am quite confused by threads and so fourth.
These are my functions:
func checkDomains() {
let endings = [".com"]
for ending in endings {
let domainName = names[randomNameIndx] + ending
let urlString = "https://domainr.p.mashape.com/v2/status?mashape-key={my-key}" + domainName
let url = URL(string: urlString)
var request = URLRequest(url: url!)
request.setValue("{my-key}", forHTTPHeaderField: "X-Mashape-Key")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpMethod = "GET"
var ourBool = false
DispatchQueue.main.sync {
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, er) in
do {
print("hey")
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSDictionary {
ourBool = String(describing: json).contains("inactive")
if ourBool {
self.domainStatuses.append("available")
} else {
self.domainStatuses.append("taken")
}
}
} catch {
}
})
task.resume()
}
}
}
#objc func btnTapped(sender: UIButton) {
self.prepareForNewName()
sender.shake(direction: "horizontal", swings: 1)
self.checkDomains() // This needs to finish before calling the next functions
self.setupForNewName()
self.animateForNewName()
}

My suggestion is in adding callback param into your async function. Example below:
func checkDomains(_ onResult: #escaping (Error?) -> Void)
Then you can call onResult inside your function in places where server return success result or error.
func checkDomains(_ onResult: #escaping (Error?) -> Void) {
...
let task = URLSession.shared.dataTask ... {
do {
//Parse response
DispatchQueue.main.async { onResult(nil) }
} catch {
DispatchQueue.main.async { onResult(error) }
}
...
}
}
The last thing you need pass callback param in place where you calling checkDomains function
#objc func btnTapped(sender: UIButton) {
self.prepareForNewName()
sender.shake(direction: "horizontal", swings: 1)
self.checkDomains { [unowned self] error in
if let error = error {
// handle error
return
}
self.setupForNewName()
self.animateForNewName()
}
}

Thank you for your answers. I just came home and realized there is a simpler way of doing this.
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
do {
...
if response != nil {
DispatchQueue.main.async {
self.setupForNewName()
self.animateForNewName()
}
}
}
} catch {
print(error)
}
})
task.resume()
Of course this is dependent on the response of the API but I am confident enough the API will always respond.

Lately it is a good practice to do network call asynchronously. There some technics to make code simpler, like https://cocoapods.org/pods/ResultPromises
TBC...

Related

Swift combine recursive retry

I would like to perform a recursive once retry with Swift Combine when the server responds with a certain message (in the example a 401 error). The data in that response alters a model, which allows for a single retry.
I wrote a small extension for the result type that was used pre iOS 13
extension URLSession {
typealias HTTPResponse = (response: HTTPURLResponse, data: Data)
typealias DataTaskResult = ((Result<HTTPResponse, Error>) -> Void)
func dataTask(with request: URLRequest, completionHandler: #escaping DataTaskResult) -> URLSessionDataTask {
self.dataTask(with: request) { (data, response, error) in
if let error = error {
completionHandler(.failure(error))
}
completionHandler(.success((response as! HTTPURLResponse, data!)))
}
}
}
I used this extension to do the following
class Account {
enum CommunicationError: Swift.Error {
case counterOutOfSync
}
var counter: Int = 0
func send(isRetry: Bool = false, completionBlock: #escaping URLSession.DataTaskResult) {
var request = URLRequest(url: URL(string: "https://myserver.com/fetch/")!)
request.setValue("\(counter)", forHTTPHeaderField: "MESSAGE-COUNTER")
request.httpMethod = "POST"
URLSession.shared.dataTask(with: request) { [weak self] taskResult in
do {
let taskResponse = try taskResult.get()
if taskResponse.response.statusCode == 401 {
if isRetry { throw CommunicationError.counterOutOfSync }
// Counter is resynced based on taskResponse.data
self?.send(isRetry: true, completionBlock: completionBlock)
} else {
completionBlock(.success(taskResponse))
}
} catch {
completionBlock(.failure(error))
}
}.resume()
}
}
You can see the recursive call in the function. I would like to do the same with Combine, but I don't know how to. This is as far as I get
func combine(isRetry: Bool = false) -> AnyPublisher<Data, Error> {
var request = URLRequest(url: URL(string: "https://myserver.com/fetch/")!)
request.setValue("\(counter)", forHTTPHeaderField: "MESSAGE-COUNTER")
request.httpMethod = "POST"
return URLSession.shared.dataTaskPublisher(for: request).tryMap {
let response = $0.response as! HTTPURLResponse
if response.statusCode == 401 {
if isRetry { throw CommunicationError.counterOutOfSync }
// Counter is resynced based on $0.data
return self.combine(isRetry: true)
} else {
return $0.data
}
}.eraseToAnyPublisher()
}
Any help is appreciated
If you have the original send(isRetry:completionBlock:), you can use Future to convert it to a publisher:
func send() -> AnyPublisher<URLSession.HTTPResponse, Error> {
Future { [weak self] promise in
self?.send(isRetry: false) { result in promise(result) }
}
.eraseToAnyPublisher()
}
Alternatively, Combine already has a .retry operator, so the entire thing could be made purely in Combine:
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response in
let response = response as! HTTPURLResponse
if response.statusCode == 401 {
throw CommunicationError.counterOutOfSync
} else {
return (response: response, data: data)
}
}
.retry(1)
.eraseToAnyPublisher()
This will retry once whenever there's any error (not just 401) from upstream. You can play around more to only retry under some conditions (e.g. see this answer)

URLSession does not call API. Though in Playground it works

So this code works for me in playground but for some reason URLSession.shared.dataTask(... doesnt call my flask api that im currently locally running. Any idea on what's wrong? So far I'm only concerned on why it does not enter the do{in my project but it works properly in playground.
func getWords() -> [Word]{
var words = [Word]()
let url = URL(string: self.url)
let request = URLRequest(url: url!)
let group = DispatchGroup()
print("XD")
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
do {
print("A")
if let data = data{
print("B")
if let decodedResponse = try? JSONDecoder().decode([Word].self, from: data){
group.enter()
DispatchQueue.main.async(){
words = decodedResponse
print("C")
print(words)
group.leave()
}
}
}
print("DD")
} catch {
print("Words.swift Error in try catch")
}
group.enter()
}).resume()
group.leave()
group.notify(queue: DispatchQueue.main, execute: {
print(words)
})
print("ASDASD WORDS: \(words)")
for _ in 1 ... 4 {
// - to make sure there aren't duplicates -
var wordId:Int = Int.random(in: 0..<words.count)
while randomIds.contains(wordId){
wordId = Int.random(in: 0..<words.count)
}
randomIds.append(wordId)
}
//returns 4 words
return words
}
You aren't using DispatchGroup correctly; You should call enter before you start the asynchronous work and leave once it is complete. You can then use notify to perform some operation.
However, you don't really need a DispatchGroup in this situation; You have that because you are trying to turn an asynchronous operation into a synchronous one;
The correct approach is to accept that the operation is asynchronous and it isn't possible for this function to return [Word]. You will need to refactor the function to accept a completion handler closure and invoke that with the result.
Something like this:
func getWords(completionHandler:#escaping (Result<[Word], Error>) -> Void) -> Void{
var words = [Word]()
let url = URL(string: self.url)
let request = URLRequest(url: url!) // Note you should use a guard and call the completion handler with an error if url is `nil`
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if let error = error {
completionHandler(.failure(error))
} else {
do {
if let data = data {
let words = try JSONDecoder().decode([Word].self, from: data)
completionHandler(.success(words))
} else {
// TODO call completionHander with a .failure(SomeError)
}
} catch {
completionHandler(.failure(error))
}
}
}).resume()
}
Then you can call it:
getWords() { result in
switch result {
case .success(let words):
print(words)
case .failure(let error):
print(error.localizedDescription)
}
}

Cancel DispatchWorkItem outside loop - Swift

I've been using DispatchWorkItem and DispatchQueue to make async requests in my app.
However, I've ran into trouble when trying to abort one of the requests.
I successfully use the workItem.cancel() to change the flag and I then check it where I want to abort. Like this:
for stop in self.userSettingsController.history {
stop.PassingInfo?.removeAll()
if workItem?.isCancelled ?? false {
print("CANCELED")
workItem = nil
break
}
...
However there's one case where I have no loop in which I can keep checking if the cancelled flag changes, so I cannot abort the request using the process above. Here's the code:
let tripQueue = DispatchQueue(label: "tripQueue")
var tripWorkItem: DispatchWorkItem? = nil
tripWorkItem = DispatchWorkItem {
self.soapServices.GetPathsByLineAndDirection(lineCode: self.lineCode!, direction: passingInfo.Direction!) { response in
DispatchQueue.main.async {
self.linePaths = response?.filter({$0.Places.contains(where: {$0.Code == self.singleStopSelected?.Code})})
if realTime {
//Get estimated trip
self.showingRealTime = true
if self.linePaths?.count ?? 0 > 0 {
self.getEstimatedTrip(lineCode: self.lineCode ?? "", direction: passingInfo.Direction ?? 0, stopCode: self.singleStopSelected?.Code ?? "", path: (self.linePaths?.first)!) { updateTripTimes in
//Does not work, as is expected. Just showing what I would like to achieve
if tripWorkItem?.isCancelled ?? false {
tripWorkItem = nil
return
}
if updateTripTimes {
DispatchQueue.main.async {
self.updateTripTimes = true
}
}
}
}
} else {
//Get trip
self.showingRealTime = false
self.getTrip(tripId: passingInfo.Id!)
}
}
}
tripWorkItem = nil
}
self.currentTripWorkItem = tripWorkItem
tripQueue.async(execute: tripWorkItem ?? DispatchWorkItem {})
Is there any way to do this?
Thanks in advance.
p.s: I'm sorry if this is duplicated, but I searched before and I couldn't find the question. I might be using the wrong terms.
Rather than putting your code in a DispatchWorkItem, consider wrapping it in a Operation subclass. You get the same isCancelled Boolean pattern:
class ComputeOperation: Operation {
override func main() {
while ... {
if isCancelled { break }
// do iteration of the calculation
}
// all done
}
}
For your network request, wrap it in an custom AsynchronousOperation subclass (e.g. this implementation), and implement cancel which will cancel the network request. For example:
enum NetworkOperationError: Error {
case unknownError(Data?, URLResponse?)
}
class NetworkOperation: AsynchronousOperation {
var task: URLSessionTask!
init(url: URL, completion: #escaping (Result<Data, Error>) -> Void) {
super.init()
self.task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode,
error == nil
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkOperationError.unknownError(data, response)))
self.finish()
}
return
}
DispatchQueue.main.async {
completion(.success(responseData))
self.finish()
}
}
}
override func main() {
task.resume()
}
override func cancel() {
super.cancel()
task.cancel()
}
}
Don't get lost in the details of the above example. Just note that
It subclassed AsynchronousOperation;
In the completion handler, it calls finish after calling the completion handler; and
The cancel implementation cancels the asynchronous task.
Then you can define your queue:
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4 // use whatever you think is reasonable
And add your operations to it:
let operation = NetworkOperation(url: url) { response in
switch response {
case .failure(let error):
// do something with `error`
case .success(let data):
// do something with `data`
}
}
queue.addOperation(operation)
Now, the issue in your GetPathsByLineAndDirection and getEstimatedTrip is that you're not following “cancelable” patterns, namely you don't appear to be returning anything that could be used to cancel the request.
So, let's look at an example. Imagine you had some trivial method like:
func startNetworkRequest(with url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
completion(data, response, error)
}
task.resume()
}
What you'd do is change it to return something that can be canceled, the URLSessionTask in this example:
#discardableResult
func startNetworkRequest(with url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionTask {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
completion(data, response, error)
}
task.resume()
return task
}
Now that’s an asynchronous task that is cancelable (and you can wrap it with the above AsynchronousOperation pattern).

How to make a synchronous GET request

I have a method for GET request in my code:
func makeHTTPGetRequest(path: String, parameters: [String: AnyObject], completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionTask {
let parameterString = parameters.stringFromHttpParameters()
let requestURL = NSURL(string:"\(path)?\(parameterString)")!
let request = NSMutableURLRequest(URL: requestURL)
request.HTTPMethod = "GET"
request.setValue("Bearer " + userInfoDefaults.stringForKey("accessToken")!, forHTTPHeaderField: "Authorization")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler:completionHandler)
task.resume()
return task
}
That is called by an another method that populates a picker view on a specific scene:
func getAffiliateds() -> [String]? {
var affiliateds:[String] = []
makeHTTPGetRequest(baseURL + "affiliateds", parameters: [:], completionHandler: { (data, response, error) in
do {
affiliateds = try NSJSONSerialization.JSONObjectWithData(data!, options:[]) as! [String]
print (affiliateds)
}
catch { print("Error: \(error)") }
})
return affiliateds
}
I need to get all affiliateds from my webservice and then list it on the picker view. But when I debugged the code I noticed that affiliateds are first returned as a null array and then it is returned with the correct information. I need to return the array from getAffiliateds only when it has already received the data from the webservice. How can I make this?
You can't. Your getAffiliateds() cannot return a value dependent on the asynchronous code that it will run. That is the nature of asynchronous code. Instead, perform a callback of some sort in the completion handler when it is called:
makeHTTPGetRequest(baseURL + "affiliateds", parameters: [:], completionHandler: { (data, response, error) in
do {
affiliateds = try NSJSONSerialization.JSONObjectWithData(data!, options:[]) as! [String]
print (affiliateds)
// DO SOMETHING HERE
}
}
A frequent strategy is for the caller to provide another completion handler which this completion handler will call.
You have a routine:
func getAffiliateds() -> [String]? {
var affiliateds:[String] = []
makeHTTPGetRequest(baseURL + "affiliateds", parameters: [:], completionHandler: { (data, response, error) in
do {
affiliateds = try NSJSONSerialization.JSONObjectWithData(data!, options:[]) as! [String]
print (affiliateds)
}
catch { print("Error: \(error)") }
})
return affiliateds
}
And you presumably have some code that does something like:
func populatePicklist() {
let affiliateds = getAffiliateds()
// populate picklist here
}
You should change this to:
func getAffiliatedsWithCompletionHandler(completionHandler: ([String]?) -> ()) {
makeHTTPGetRequest(baseURL + "affiliateds", parameters: [:]) { data, response, error in
do {
let affiliateds = try NSJSONSerialization.JSONObjectWithData(data!, options:[]) as? [String] // two notes here: first, define local var here, not up above; second, use `as?` to gracefully handle problems where result was not `[String]`
print (affiliateds)
completionHandler(affiliateds)
}
catch {
print("Error: \(error)")
completionHandler(nil)
}
}
}
func populatePicklist() {
getAffiliatedsWithCompletionHandler { affiliateds in
// populate picklist here
}
// but not here
}

Include a return handler in async call in Swift

I'm experimenting with async calls but I'm a little lost. The print(json) in the viewDidLoad outputs an empty dictionary, but the one within the function prints correctly. This is unsurprising; it gets to that print before the async is completed. I can't figure out how to fix it; I tried putting the return within the completion handler, but I got an error that Unexpected non-void return value in void function. I tried changing the completion handler to expect a return value, but either that's not the right approach or I was doing it wrong.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let json = getJson("https://maps.googleapis.com/maps/api/geocode/json?address=WashingtonDC&sensor=false")
print(json)
}
func getJson(url: String) -> AnyObject {
var json:AnyObject = [:]
let urlPath = NSURL(string: url)
let urlRequest = NSURLRequest(URL: urlPath!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
if error != nil {
print("Error")
} else {
do {
json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)
print(json)
} catch {
print("json error")
}
}
})
task.resume()
return json
}
}
You will need to have a completion handler based interface to your async API.
func getJson(url: String, completion : (success: Bool, json: AnyObject? ) ->Void ) -> Void {
var json:AnyObject = [:]
let urlPath = NSURL(string: url)
let urlRequest = NSURLRequest(URL: urlPath!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
if error != nil {
print("Error")
} else {
do {
json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)
print(json)
//Call the completion handler here:
completion(success : true, json :json )
} catch {
print("json error")
completion(success : false, json :nil )
}
}
})
task.resume()
}
}
Now you call call this API as follows-
override func viewDidLoad() {
super.viewDidLoad()
getJson("https://maps.googleapis.com/maps/api/geocode/json?address=WashingtonDC&sensor=false") { (success, json) -> Void in
if success {
if let json = json {
print(json)
}
}
}
}

Resources