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

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

Related

In Swift, how do you loop through a list and hand one item at a time to a function with completion closure?

I'm trying to process a folder with audio files through speech to text recognition on MacOS.
If I just process one file, it works, but if I feed multiple files, only one file works and throws an error for rest.
I thought I could use DispatchGroup, but it still feeds everything at once instead of waiting for each item to be completed.
Could someone help me to understand what I'm doing wrong?
let recognizer = SFSpeechRecognizer()
recognizer?.supportsOnDeviceRecognition = true
let group = DispatchGroup()
let fd = FileManager.default
fd.enumerator(at: url, includingPropertiesForKeys: nil)?.forEach({ (e) in
if let url = e as? URL, url.pathExtension == "wav" || url.pathExtension == "aiff" {
let request = SFSpeechURLRecognitionRequest(url: url)
group.enter()
let task = recognizer?.recognitionTask(with: request) { (result, error) in
print("Transcribing \(url.lastPathComponent)")
guard let result = result else {
print("\(url.lastPathComponent): No message")
group.leave()
return
}
while result.isFinal == false {
sleep(1)
}
print("\(url.lastPathComponent): \(result.bestTranscription.formattedString)")
group.leave()
}
group.wait()
}
}
group.notify(queue: .main) {
print("Done")
}
Update: I tried DispatchQueue, but it transcribes only one file and hangs.
let recognizer = SFSpeechRecognizer()
recognizer?.supportsOnDeviceRecognition = true
let fd = FileManager.default
let q = DispatchQueue(label: "serial q")
fd.enumerator(at: url, includingPropertiesForKeys: nil)?.forEach({ (e) in
if let url = e as? URL, url.pathExtension == "wav" {
let request = SFSpeechURLRecognitionRequest(url: url)
q.sync {
let task = recognizer?.recognitionTask(with: request) { (result, error) in
guard let result = result else {
print("\(url.lastPathComponent): No message")
return
}
if result.isFinal {
print("\(url.lastPathComponent): \(result.bestTranscription.formattedString)")
}
}
}
}
})
print("Done")
This is a async/await solution with a Continuation. It runs sequentially.
let recognizer = SFSpeechRecognizer()
recognizer?.supportsOnDeviceRecognition = true
let fd = FileManager.default
let enumerator = fd.enumerator(at: url, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)!
Task {
for case let fileURL as URL in enumerator where ["wav", "aiff"].contains(fileURL.pathExtension) {
do {
try await recognizeText(at: fileURL)
} catch {
print(error)
}
}
}
func recognizeText(at url: URL) async throws {
return try await withCheckedThrowingContinuation { (continuation : CheckedContinuation<Void, Error>) in
let request = SFSpeechURLRecognitionRequest(url: url)
let task = recognizer?.recognitionTask(with: request) { (result, error) in
print("Transcribing \(url.lastPathComponent)")
if let error = error {
continuation.resume(throwing: error)
print("\(url.lastPathComponent): No message")
} else {
print("\(url.lastPathComponent): \(result!.bestTranscription.formattedString)")
if result!.isFinal {
continuation.resume(returning: ())
}
}
}
}
}
If you want your dispatch group to wait for each task to complete before submitting the next, you need to add a `group.wait() inside the loop, after submitting each task.
// Your setup code is unchanged...
fd.enumerator(at: url, includingPropertiesForKeys: nil)?.forEach({ (e) in
if let url = e as? URL, url.pathExtension == "wav" || url.pathExtension == "aiff" {
let request = SFSpeechURLRecognitionRequest(url: url)
group.enter()
let task = recognizer?.recognitionTask(with: request) { (result, error) in
print("Transcribing \(url.lastPathComponent)")
guard let result = result else {
print("\(url.lastPathComponent): No message")
group.leave()
return
}
while result.isFinal == false {
sleep(1)
}
print("\(url.lastPathComponent): \(result.bestTranscription.formattedString)")
group.leave()
}
group.wait() // <---- Add this
}
That should do it.
Note that doing it this way will block the main thread. You should really wrap the code that submits jobs and waits for the last one to finish in a call to a background dispatch queue.
Something like this:
DispatchQueue.global().async {
// Code to loop through and submit tasks, including dispatchGroup logic above.
}

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.

a function does internet fetching and return a value

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
}
}
}
}

Syncronize async functions

I try to create my first project with swift 3.
I try to get data from my API. This works pretty good if I manually start the function. I need to synchronize the async request.
I need to trigger my function 3 times and wait for the others to complete.
makeGetCall(URLstring: "api1")
wait to complete
makeGetCall(URLstring: "api2")
wait to complete
makeGetCall(URLstring: "api3")
Set this to background and trigger every 5 seconds.
func makeGetCall(URLstring: String, update: Bool) {
let completeURL = "http://myapi/" + URLstring
// Set up the URL request
guard let url = URL(string: completeURL) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error as Any)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as XML
if URLstring == "devicelist.cgi" {
self.readDevice(XMLData: responseData)
}
if URLstring == "statelist.cgi" {
self.readDeviceData(XMLData: responseData, update: update)
}
if URLstring == "functionlist.cgi" {
self.readGewerke(XMLData: responseData)
}
}
task.resume()
}
Can somebody help please.
Hagen
This is what I tried with completion handler:
override func viewDidLoad() {
super.viewDidLoad()
makeGetCall(input: "statelist.cgi") {
(result: Bool) in
print("finished statelist")
}
makeGetCall(input: "devicelist.cgi") {
(result: Bool) in
print("finished devicelist")
}
makeGetCall(input: "functionlist.cgi") {
(result: Bool) in
print("finished functionlist")
}
}
func makeGetCall(input: String, completion: #escaping (_ result: Bool) -> Void) {
let completeURL = "http://192.168.0.25/addons/xmlapi/" + input
// Set up the URL request
guard let url = URL(string: completeURL) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error as Any)
return
}
// make sure we got data
guard data != nil else {
print("Error: did not receive data")
return
}
completion(true)
}
task.resume()
}
If I put the 3 calls together it worked as it should.
I thing it should also work with GCD but most of examples for swift 2.
makeGetCall(input: "devicelist.cgi") {
(result: Bool) in
print("finished devicelist")
self.makeGetCall(input: "functionlist.cgi") {
(result: Bool) in
print("finished functionlist")
self.makeGetCall(input: "statelist.cgi") {
(result: Bool) in
print("finished statelist")
}
}
}
Maybe now somebody can help.
Thanks Hagen
Use a DispatchGroup to get notified when something happens.
override func viewDidLoad() {
super.viewDidLoad()
let stateListGroup = DispatchGroup()
stateListGroup.enter()
makeGetCall(input: "statelist.cgi") {
(result: Bool) in
print("finished statelist")
stateListGroup.leave()
}
let deviceListGroup = DispatchGroup()
deviceListGroup.enter()
// the notify closure is called when the (stateList-) groups enter and leave counts are balanced.
stateListGroup.notify(queue: DispatchQueue.main) {
self.makeGetCall(input: "devicelist.cgi") {
(result: Bool) in
print("finished devicelist")
deviceListGroup.leave()
}
}
let functionListGroup = DispatchGroup()
functionListGroup.enter()
deviceListGroup.notify(queue: DispatchQueue.main) {
self.makeGetCall(input: "functionList") {
(result: Bool) in
print("finished functionlist")
functionListGroup.leave()
}
}
functionListGroup.notify(queue: DispatchQueue.main) {
print("update ui here")
}
}
Prints:
statelist.cgi
finished statelist
devicelist.cgi
finished devicelist
functionList
finished functionlist
update ui here
Also keep in mind that the completion handler of session.dataTask() is called on a background queue, so I recommend to dispatch completion(true) on the main queue to avoid unexpected behaviour:
DispatchQueue.main.async {
completion(true)
}