a function does internet fetching and return a value - swift

I know the following piece of code is wrong, but I want to show my intent.
I want to write a method that will be called by multiple times. and this fetching method will tell me if it is successfully reached.
func fetch(url: String) -> Bool? {
let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
let url = URL(string: url)
var bool: Bool? = nil
if let url = url {
defaultSession.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
print(error)
return
}
DispatchQueue.main.async {
if let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let data = data {
// handle the data.
bool = true
} else {
print("something really wrong")
bool = false
}
}
}).resume()
}
return bool
}
if let bool = fetch(url: "https://www.google.com.hk/webhp?hl=en&sa=X&ved=0ahUKEwimubK7r-HVAhVFmZQKHazMAMMQPAgD"), bool == true {
// if it is true, I can go for next step.
}

Making the UI wait on completion of some API call is not recommended. The app will have no control over how long that API call will take. Situations with bad network connectivity can take several seconds to respond.
You can handle a situation like this is to use a completion handler.
func fetch(url: String, completion: #escaping (_ success: Bool) -> Void) {
let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
let url = URL(string: url)
if let url = url {
defaultSession.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
print(error)
return
}
if let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let data = data {
// handle the data.
completion(true)
} else {
print("something really wrong")
completion(false)
}
}).resume()
}
}
func testFetch () {
fetch(url: "https://www.google.com.hk/webhp?hl=en&sa=X&ved=0ahUKEwimubK7r-HVAhVFmZQKHazMAMMQPAgD") { (success) in
// if it is true, I can go for next step.
DispatchQueue.main.async {
if success {
// it was good
}
else {
// not good
}
}
}
}

Related

Does Swift task run first or print() first when I tap my UIButton?

I am trying to understand what is going on in my code here.
I have a simple API call to open weahter API and that whenever the user taps the UIButton, it should call the api and get the data back from open weather.
Everything works as intended however, when I have my UIButton pressed, the print statement executed first before the Task closure. I'm trying to understand the race condition here
This is my code in viewController:
#IBAction func callAPIButton(_ sender: UIButton) {
Task {
let weatherData = await weatherManager.fetchWeather(cityName: "Seattle")
}
}
Here's the code for fetching the API:
struct WeatherManager{
let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=someAPIKeyHere"
func fetchWeather(cityName: String) -> WeatherModel? {
let urlString = "\(weatherURL)&q=\(cityName)"
let requestResult = performRequest(urlString: urlString)
return requestResult
}
func performRequest(urlString: String) -> WeatherModel? {
var weatherResult : WeatherModel? = nil
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error != nil {
return
}
if let safeData = data {
weatherResult = parseJSON(weatherData: safeData)
}
})
task.resume()
}
return weatherResult
}
func parseJSON(weatherData: Data) -> WeatherModel?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(decodedData)")
let temp = decodedData.main.temp
let name = decodedData.name
let weather = WeatherModel(conditionId:300, cityName: name, temperature: temp)
return weather
} catch {
print("Something is wrong here: " + error.localizedDescription)
}
return nil
}
}
Here's my Model:
struct WeatherModel{
let conditionId: Int
let cityName: String
let temperature: Double
var temperatureString: String{
return String(format: "%.1f", temperature)
}
var conditionName: String {
switch conditionId {
case 200...232:
return "cloud.bolt"
case 300...321:
return "cloud.drizzle"
case 500...531:
return "cloud.rain"
case 600...622:
return "cloud.snow"
case 701...781:
return "cloud.fog"
case 800:
return "sun.max"
case 801...804:
return "cloud.bolt"
default:
return "cloud"
}
}
}
Desired result:
This is in weatherData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e+09, sunset: 1.6713243e+09))
This is what I am getting instead:
This is in weatherData: nil
this is in decodedData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e+09, sunset: 1.6713243e+09))
Thank you in advance
Everything works as intended
No, it doesn't. I don't know why you claim such a thing; your code isn't working at all.
The problem is that you are trying to return weatherResult from performRequest. But performRequest gets its weatherResult value asynchronously, so this attempt is doomed to failure; you will always be returning nil, because the return weatherResult happens before session.dataTask ever even starts to find out what weatherResult is.
You cannot just synchronously return the results of an asynchronous request. You have two basic options for asynchronous requests.
Use the older “completion handler” pattern with Result types:
struct WeatherManager {
let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
let appId = "someAPIKeyHere"
func fetchWeather(
cityName: String,
completion: #escaping (Result<WeatherModel, Error>) -> Void
) {
guard var components = URLComponents(string: weatherURL) else {
completion(.failure(URLError(.badURL)))
return
}
components.queryItems = [
URLQueryItem(name: "appid", value: appId),
URLQueryItem(name: "q", value: cityName)
]
guard let url = components.url else {
completion(.failure(URLError(.badURL)))
return
}
performRequest(url: url, completion: completion)
}
func performRequest(
url: URL,
queue: DispatchQueue = .main,
completion: #escaping (Result<WeatherModel, Error>) -> Void
) {
let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
let task = session.dataTask(with: url) { data, response, error in
guard
error == nil,
let data = data,
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
queue.async { completion(.failure(error ?? URLError(.badServerResponse))) }
return
}
do {
let weatherResult = try parseJSON(weatherData: data)
queue.async { completion(.success(weatherResult)) }
} catch {
queue.async { completion(.failure(error)) }
}
}
task.resume()
}
func parseJSON(weatherData: Data) throws -> WeatherModel {
let decoder = JSONDecoder()
let response = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(response)")
return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
}
}
Then, rather than:
let weather = weatherManager.fetchWeather(cityName: …)
You would
weatherManager.fetchWeather(cityName: …) { result in
switch result {
case .failure(let error):
print(error)
case .success(let weather):
// do something with the `weather` object here
}
}
// note, do not do anything with `weather` here, because the above
// runs asynchronously (i.e., later).
Use the newer async-await pattern of Swift concurrency:
struct WeatherManager {
let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
let appId = "someAPIKeyHere"
func fetchWeather(cityName: String) async throws -> WeatherModel {
guard var components = URLComponents(string: weatherURL) else {
throw URLError(.badURL)
}
components.queryItems = [
URLQueryItem(name: "appid", value: appId),
URLQueryItem(name: "q", value: cityName)
]
guard let url = components.url else {
throw URLError(.badURL)
}
return try await performRequest(url: url)
}
func performRequest(url: URL) async throws -> WeatherModel {
let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
let (data, response) = try await session.data(from: url)
guard
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
throw URLError(.badServerResponse)
}
return try parseJSON(weatherData: data)
}
func parseJSON(weatherData: Data) throws -> WeatherModel {
let decoder = JSONDecoder()
do {
let response = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(response)")
return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
} catch {
print("Something is wrong here: " + error.localizedDescription)
throw error
}
}
}
And then you can do things like:
Task {
do {
let weather = try await weatherManager.fetchWeather(cityName: …)
// do something with `weather` here
} catch {
print(error)
}
}
Note, a few changes in the above unrelated to the asynchronous nature of your request:
Avoid creating URLSession instances. If you do, you need to remember to invalidate them. Instead, it is much easier to use URLSession.shared, eliminating this annoyance.
Avoid building URLs with string interpolation. Use URLComponents to build safe URLs (e.g., ones that can handle city names like “San Francisco”, with spaces in their names).

Firebase getIDToken and how to use it in an API call

I have an API call that grabs json, but requires token authentication. Token auth works great, but when I try and pass the token along to the API function, it's coming back nil. I believe it's because Auth.auth().currentUser!.getIDToken(...) hasn't actually completed yet. Relevant code below... How do I modify this to
class SessionData : ObservableObject {
...
func token() -> String? {
var result: String? = nil
Auth.auth().currentUser!.getIDToken(completion: { (res, err) in
if err != nil {
print("*** TOKEN() ERROR: \(err!)")
} else {
print("*** TOKEN() SUCCESS: \(err!)")
result = res!
}
})
return result
}
...
}
class FetchPosts: ObservableObject {
#Published var posts = [Post]()
func load(api: Bool, session: SessionData) {
if api {
let url = URL(string: MyAPI.getAddress(token: session.token()!))!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let postsData = data {
// 3.
let decodedData = try JSONDecoder().decode(Response.self, from: postsData)
DispatchQueue.main.async {
self.posts = decodedData.result
if decodedData.error != nil {
print("ERROR: \(decodedData.error!)")
session.json_error(error: decodedData.error!)
}
}
} else {
print("No data. Connection error.")
DispatchQueue.main.async {
session.json_error(error: "Could not connect to server, please try again!")
}
}
} catch {
print("* Error: \(error)")
}
}.resume()
} else {
let url = Bundle.main.url(forResource: "test", withExtension: "json")!
let data = try! Data(contentsOf: url)
let decoder = JSONDecoder()
if let products = try? decoder.decode([Post].self, from: data) {
self.posts = products
}
}
}
}
And this is how the .load function is called:
UserViewer(fetch: posts)
.transition(AnyTransition.slide)
.animation(.default)
.onAppear {
withAnimation{
posts.load(api: true, session: session)
}
}
.environmentObject(session)
Because getIDToken executes and returns asynchronously, you can't return directly from it. Instead, you'll need to use a callback function.
Here's a modification of your function:
func token(_ completion: #escaping (String?) -> ()) {
guard let user = Auth.auth().currentUser else {
//handle error
return
}
user.getIDToken(completion: { (res, err) in
if err != nil {
print("*** TOKEN() ERROR: \(err!)")
//handle error
} else {
print("*** TOKEN() SUCCESS: \(err!)")
completion(res)
}
})
}
Then, you can use it later on:
.onAppear {
session.token { token in
guard let token = token else {
//handle nil
return
}
withAnimation{
posts.load(api: true, session: session, token: token)
}
}
}
Modify your load to take a token parameter:
func load(api: Bool, session: SessionData, token: String) {
if api {
guard let url = URL(string: MyAPI.getAddress(token: token)) else {
//handle bad URL
return
}
Also, as you can see I'm doing in my code samples, I would try to get out of the habit of using ! to force unwrap optionals. If the optional is nil and you use !, your program will crash. Instead, familiarize yourself with guard let and if let and learn to handle optionals in a way that won't lead to a crash -- it's one of the great benefits of Swift.

How to execute a synchronous api call after an asynchronous api call

I have two services that are working perfectly independently one is a synchronous call to get shopping-lists and another is an asynchronous call to add shopping-lists. The problem comes when i try to get a shopping-lists just after the add-Shopping-lists call has successfully completed.
The function to get shopping-lists never returns it just hangs after i call it in the closure of the add-Shopping-lists function. What is the best way to make these two calls without promises.
Create ShoppingList
func createURLRequest(with endpoint: String, data: ShoppingList? = nil, httpMethod method: String) -> URLRequest {
guard let accessToken = UserSessionInfo.accessToken else {
fatalError("Nil access token")
}
let urlString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
guard let requestUrl = URLComponents(string: urlString!)?.url else {
fatalError("Nil url")
}
var request = URLRequest(url:requestUrl)
request.httpMethod = method
request.httpBody = try! data?.jsonString()?.data(using: .utf8)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
return request
}
func createShoppingList(with shoppingList: ShoppingList, completion: #escaping (Bool, Error?) -> Void) {
let serviceURL = environment + Endpoint.createList.rawValue
let request = createURLRequest(with: serviceURL, data: shoppingList, httpMethod: HttpBody.post.rawValue)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in
guard let _ = data,
let response = response as? HTTPURLResponse,
(200 ..< 300) ~= response.statusCode,
error == nil else {
completion(false, error)
return
}
completion(true, nil)
})
task.resume()
}
Get shoppingLists
func fetchShoppingLists(with customerId: String) throws -> [ShoppingList]? {
var serviceResponse: [ShoppingList]?
var serviceError: Error?
let serviceURL = environment + Endpoint.getLists.rawValue + customerId
let request = createURLRequest(with: serviceURL, httpMethod: HttpBody.get.rawValue)
let semaphore = DispatchSemaphore(value: 0)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in
defer { semaphore.signal() }
guard let data = data, // is there data
let response = response as? HTTPURLResponse, // is there HTTP response
(200 ..< 300) ~= response.statusCode, // is statusCode 2XX
error == nil else { // was there no error, otherwise ...
serviceError = error
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let shoppingList = try decoder.decode([ShoppingList].self, from: data)
serviceResponse = shoppingList
} catch let error {
serviceError = error
}
})
task.resume()
semaphore.wait()
if let error = serviceError {
throw error
}
return serviceResponse
}
Usage of function
func addShoppingList(customerId: String, shoppingList: ShoppingList, completion: #escaping (Bool, Error?) -> Void) {
shoppingListService.createShoppingList(with: shoppingList, completion: { (success, error) in
if success {
self.shoppingListCache.clearCache()
let serviceResponse = try? self.fetchShoppingLists(with: customerId)
if let _ = serviceResponse {
completion(true, nil)
} else {
let fetchListError = NSError().error(description: "Unable to fetch shoppingLists")
completion(false, fetchListError)
}
} else {
completion(false, error)
}
})
}
I would like to call the fetchShoppingLists which is a synchronous call and get new data then call the completion block with success.
This question is predicated on a flawed assumption, that you need this synchronous request.
You suggested that you needed this for testing. This is not true: One uses “expectations” to test asynchronous processes; we don’t suboptimize code for testing purposes.
You also suggested that you want to “stop all processes” until the request is done. Again, this is not true and offers horrible UX and subjects your app to possibly be killed by watchdog process if you do this at the wrong time while on slow network. If, in fact, the UI needs to be blocked while the request is in progress, we usually just throw up a UIActivityIndicatorView (a.k.a. a “spinner”), perhaps on top of a dimming/blurring view over the whole UI to prevent users from interacting with the visible controls, if any.
But, bottom line, I know that synchronous requests feel so intuitive and logical, but it’s invariably the wrong approach.
Anyway, I’d make fetchShoppingLists asynchronous:
func fetchShoppingLists(with customerId: String, completion: #escaping (Result<[ShoppingList], Error>) -> Void) {
var serviceResponse: [ShoppingList]?
let serviceURL = environment + Endpoint.getLists.rawValue + customerId
let request = createURLRequest(with: serviceURL, httpMethod: .get)
let session = URLSession.shared
let task = session.dataTask(with: request) { data, response, error in
guard let data = data, // is there data
let response = response as? HTTPURLResponse, // is there HTTP response
200 ..< 300 ~= response.statusCode, // is statusCode 2XX
error == nil else { // was there no error, otherwise ...
completion(.failure(error ?? ShoppingError.unknownError))
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let shoppingList = try decoder.decode([ShoppingList].self, from: data)
completion(.success(shoppingList))
} catch let jsonError {
completion(.failure(jsonError))
}
}
task.resume()
}
And then you just adopt this asynchronous pattern. Note, while I’d use the Result pattern for my completion handler, I left yours as it was to minimize integration issues:
func addShoppingList(customerId: String, shoppingList: ShoppingList, completion: #escaping (Bool, Error?) -> Void) {
shoppingListService.createShoppingList(with: shoppingList) { success, error in
if success {
self.shoppingListCache.clearCache()
self.fetchShoppingLists(with: customerId) { result in
switch result {
case .failure(let error):
completion(false, error)
case .success:
completion(true, nil)
}
}
} else {
completion(false, error)
}
}
}
Now, for example, you suggested you wanted to make fetchShoppingLists synchronous to facilitate testing. You can easily test asynchronous methods with “expectations”:
class MyAppTests: XCTestCase {
func testFetch() {
let exp = expectation(description: "Fetching ShoppingLists")
let customerId = ...
fetchShoppingLists(with: customerId) { result in
if case .failure(_) = result {
XCTFail("Fetch failed")
}
exp.fulfill()
}
waitForExpectations(timeout: 10)
}
}
FWIW, it’s debatable that you should be unit testing the server request/response at all. Often instead mock the network service, or use URLProtocol to mock it behind the scenes.
For more information about asynchronous tests, see Asynchronous Tests and Expectations.
FYI, the above uses a refactored createURLRequest, that uses the enumeration for that last parameter, not a String. The whole idea of enumerations is to make it impossible to pass invalid parameters, so let’s do the rawValue conversion here, rather than in the calling point:
enum HttpMethod: String {
case post = "POST"
case get = "GET"
}
func createURLRequest(with endpoint: String, data: ShoppingList? = nil, httpMethod method: HttpMethod) -> URLRequest {
guard let accessToken = UserSessionInfo.accessToken else {
fatalError("Nil access token")
}
guard
let urlString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let requestUrl = URLComponents(string: urlString)?.url
else {
fatalError("Nil url")
}
var request = URLRequest(url: requestUrl)
request.httpMethod = method.rawValue
request.httpBody = try! data?.jsonString()?.data(using: .utf8)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
return request
}
I am sure it could be alot better, but this is my 5 minute version.
import Foundation
import UIKit
struct Todo: Codable {
let userId: Int
let id: Int
let title: String
let completed: Bool
}
enum TodoError: String, Error {
case networkError
case invalidUrl
case noData
case other
case serializationError
}
class TodoRequest {
let todoUrl = URL(string: "https://jsonplaceholder.typicode.com/todos")
var todos: [Todo] = []
var responseError: TodoError?
func loadTodos() {
var responseData: Data?
guard let url = todoUrl else { return }
let group = DispatchGroup()
let task = URLSession.shared.dataTask(with: url) { [weak self](data, response, error) in
responseData = data
self?.responseError = error != nil ? .noData : nil
group.leave()
}
group.enter()
task.resume()
group.wait()
guard responseError == nil else { return }
guard let data = responseData else { return }
do {
todos = try JSONDecoder().decode([Todo].self, from: data)
} catch {
responseError = .serializationError
}
}
func retrieveTodo(with id: Int, completion: #escaping (_ todo: Todo? , _ error: TodoError?) -> Void) {
guard var url = todoUrl else { return }
url.appendPathComponent("\(id)")
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let todoData = data else { return completion(nil, .noData) }
do {
let todo = try JSONDecoder().decode(Todo.self, from: todoData)
completion(todo, nil)
} catch {
completion(nil, .serializationError)
}
}
task.resume()
}
}
class TodoViewController: UIViewController {
let request = TodoRequest()
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global(qos: .background).async { [weak self] in
self?.request.loadTodos()
self?.request.retrieveTodo(with: 1, completion: { [weak self](todoData, error) in
guard let strongSelf = self else { return }
if let todoError = error {
return debugPrint(todoError.localizedDescription)
}
guard let todo = todoData else {
return debugPrint("No todo")
}
debugPrint(strongSelf.request.todos)
debugPrint(todo)
})
}
}
}

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.

Swift 4: How to asynchronously use URLSessionDataTask but have the requests be in a timed queue?

Basically I have some JSON data that I wish to retrieve from a bunch of URL's (all from the same host), however I can only request this data roughly every 2 seconds at minimum and only one at a time or I'll be "time banned" from the server. As you'll see below; while URLSession is very quick it also gets me time banned almost instantly when I have around 700 urls to get through.
How would I go about creating a queue in URLSession (if its functionality supports it) and while having it work asynchronously to my main thread; have it work serially on its own thread and only attempt each item in the queue after 2 seconds have past since it finished the previous request?
for url in urls {
get(url: url)
}
func get(url: URL) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
DispatchQueue.main.async {
print(error.localizedDescription)
}
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
DispatchQueue.main.async {
print("Server Error")
}
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
DispatchQueue.main.async {
self.itemsCount.append(count)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
Recursion solves this best
import Foundation
import PlaygroundSupport
// Let asynchronous code run
PlaygroundPage.current.needsIndefiniteExecution = true
func fetch(urls: [URL]) {
guard urls.count > 0 else {
print("Queue finished")
return
}
var pendingURLs = urls
let currentUrl = pendingURLs.removeFirst()
print("\(pendingURLs.count)")
let session = URLSession.shared
let task = session.dataTask(with: currentUrl, completionHandler: { (data, response, error) in
print("task completed")
if let _ = error {
print("error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("server error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
if response.mimeType == "application/json" {
print("json data parsed")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}else {
print("unknown data")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}
})
//start execution after two seconds
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
print("resume called")
task.resume()
}
}
var urls = [URL]()
for _ in 0..<100 {
if let url = URL(string: "https://google.com") {
urls.append(url)
}
}
fetch(urls:urls)
The easiest way is to perform recursive call:
Imagine you have array with your urls.
In place where you initially perform for loop with, replace it with single call get(url:).
self.get(urls[0])
Then add this line at the and of response closure right after self.itemsCount.append(count):
self.urls.removeFirst()
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (_) in
self.get(url: urls[0])
}
Make DispatchQueue to run your code on threads. You don't need to do this work on Main Thread. So,
// make serial queue
let queue = DispatchQueue(label: "getData")
// for delay
func wait(seconds: Double, completion: #escaping () -> Void) {
queue.asyncAfter(deadline: .now() + seconds) { completion() }
}
// usage
for url in urls {
wait(seconds: 2.0) {
self.get(url: url) { (itemCount) in
// update UI related to itemCount
}
}
}
By the way, Your get(url: url) function is not that great.
func get(url: URL, completionHandler: #escaping ([Int]) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
print(error.localizedDescription)
/* Don't need to use main thread
DispatchQueue.main.async {
print(error.localizedDescription)
}
*/
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("Server Error")
/* Don't need to use main thread
DispatchQueue.main.async {
print("Server Error")
}
*/
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
self.itemsCount.append(count)
// append all data that you need and pass it to completion closure
DispatchQueue.main.async {
completionHandler(self.itemsCount)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
I would recommend you to learn concept of GCD(for thread) and escaping closure(for completion handler).
GCD: https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1
Escaping Closure: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID546