Swift 3 - Function Inside DispatchQueue - swift

I called a function inside DispatchQueue.main.async. Here's my code:
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
for i in 0 ... (Global.selectedIcons.count - 1) {
if self.albumorphoto == 1 {
if i == 0 {
self.detector = 1
self.uploadPhoto() //here
}
else {
self.detector = 2
self.uploadPhoto() //here
}
}
else {
self.uploadPhoto() //here
}
}
group.leave()
}
group.notify(queue: .main) {
print("done")
}
}
func uploadPhoto(){
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let params = param
request.httpBody = params.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("error=\(error!)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response!)")
}
let responseString = String(data: data, encoding: .utf8)
print("responseString = \(responseString!)")
if self.detector == 1 {
self.album = self.parseJsonData(data: data)
}
}
task.resume()
}
func parseJsonData(data: Data) -> [AnyObject] {
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary
let jsonalbum = jsonResult!["data"] as? [AnyObject]
for jsonAlbum in jsonalbum! {
self.folderID = jsonAlbum["id"] as! String
}
} catch {
print(error)
}
return album
}
I wish to make it wait until all the tasks in DispathcQueue finish. It works but the problem is my function uploadPhoto(). It can't wait until uploadPhoto() finish doing its task. Any idea to solve this? Thanks!

Using a DispatchGroup is the right choice here, but you have to enter and leave for each asynchronous task:
let group = DispatchGroup()
photos.forEach { photo in
group.enter()
// create the request for the photo
URLSession.shared.dataTask(with: request) { data, response, error in
group.leave()
// handle the response
}.resume()
}
group.notify(queue: .main) {
print("All photos uploaded.")
}
You don't need a DispatchQueue.async() call because URLSession.shared.dataTask is already asynchronous.
In my code i assumed that you want to model your objects as Photo and replace Global.selectedIcons.count with a photos array:
class Photo {
let isAlbum: Bool
let isDefector: Bool
let imageData: Data
}
I'd recommend you take a look at Alamofire and SwiftyJSON to further improve your code. These are popular libraries that make dealing with network requests a lot easier. With them you can reduce almost the entire uploadPhoto()/parseJsonData() functions to something like this:
Alamofire.upload(photo.imageData, to: url).responseSwiftyJSON { json in
json["data"].array?.compactMap{ $0["id"].string }.forEach {
self.folderID = $0
}
}
This makes your code more stable because it removes all forced unwrapping. Alamofire also provides you with features like upload progress and resuming & cancelling of requests.

Related

How to call dataTask method several times with a counter?

I'm currently developing an application using SwiftUI.
I want to call dataTask method several times with while method, a flag, and a counter.
But my code doesn't work...
How could solve this problem?
Here is my code:
func makeCallWithCounter(){
var counter = 0
var flag = false
// Set up the URL request
let endpoint: String = "https://sample.com/api/info/"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var 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
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.sample = try JSONDecoder().decode([Sample].self, from: responseData)
counter += 1
if counter > 4 {
flag = true
}
}catch{
print("Error: did not decode")
return
}
}
}
while flag == false {
task.resume()
}
}
UPDATED
func makeCallWithCounter(){
var day = 1
var date = "2020-22-\(day)"
var totalTemperature = 0
var counter = 0
var flag = false
// Set up the URL request
let endpoint: String = "https://sample.com/api/info/?date=\(date)"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var 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
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.sample = try JSONDecoder().decode([Sample].self, from: responseData)
day += 1
totalTemperature += self.sample.temperature
if day > 4 {
flag = true
}
}catch{
print("Error: did not decode")
return
}
}
}
while flag == false {
task.resume()
}
print(totalTemperature)
}
Xcode:Version 12.0.1
As I wrote in the comments you need a loop and DispatchGroup. On the other hand you don't need flag and counter and actually not even the URLRequest
I removed the redundant code and there is still a serious error: The line
totalTemperature += sample.temperature
cannot work if sample is an array. The question contains not enough information to be able to fix that.
func makeCallWithCounter() {
var totalTemperature = 0
let group = DispatchGroup()
for day in 1...4 {
// Set up the URL request
let endpoint = "https://sample.com/api/info/?date=2020-22-\(day)"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
continue
}
// make the request
group.enter()
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
defer { group.leave() }
if let error = error { print(error); return }
// parse the result as JSON, since that's what the API provides
do {
let sample = try JSONDecoder().decode([Sample].self, from: data!)
totalTemperature += sample.temperature
} catch {
print(error)
}
}
task.resume()
}
group.notify(queue: .main) {
print(totalTemperature)
}
}

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

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

How to save the result of a #escaping closure function into a variable?

I currently have a class which is below:
class Anton {
//URL to web service (Internal)
let URL_DISPLAY_MENU = "http://192.168.1.100/api/DisplayMenu.php"
func displayMenu(completion: #escaping ([[String:Any]]) ->()) {
let requestURL = URL(string: URL_DISPLAY_MENU)
var request = URLRequest(url: requestURL!)
request.httpMethod = "POST"
var menu: [[String:Any]]?
let task = URLSession.shared.dataTask(with: request) { data,response,error in guard let data = data, error == nil else {
print("error=\(String(describing: error))")
return
}
do {
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(String(describing: response))")
} else {
menu = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] ?? []
var dictionary = [Int:Any]()
for (index,item) in menu!.enumerated() {
let uniqueID = index
dictionary[uniqueID] = item
}
completion(menu!)
}
} catch let error as NSError {
print(error)
}
}
task.resume()
}
}
At the moment I use the class & contained function as follows:
var anton = Anton()
anton.displayMenu { menu in
print(menu)
}
What I want to do is have a way of saving the #escaping result into a global variable. I'm very new to escaping closures so not sure how to go about this.
You can save your value directly in completion.
Example:
var anton = Anton()
anton.displayMenu { menu in
self.myMenu = menu
}

URL request using Swift

I have access the "dictionary" moviedb for
example : https://www.themoviedb.org/search/remote/multi?query=exterminador%20do%20futuro&language=en
How can i catch only the film's name and poster from this page to my project in Swift ?
It's answer :)
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
reload()
}
private func reload() {
let requestUrl = "https://www.themoviedb.org/search/remote/multi?query=exterminador%20do%20futuro&language=en"
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let request = NSURLRequest(URL: NSURL(string: requestUrl)!)
let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
if let error = error {
println("###### error ######")
}
else {
if let JSON = NSJSONSerialization.JSONObjectWithData(data,
options: .AllowFragments,
error: nil) as? [NSDictionary] {
for movie in JSON {
let name = movie["name"] as! String
let posterPath = movie["poster_path"] as! String
println(name) // "Terminator Genisys"
println(posterPath) // "/5JU9ytZJyR3zmClGmVm9q4Geqbd.jpg"
}
}
}
})
task.resume()
}
}
You need to include your api key along with the request. I'd just try something like this to see if it works or not. If it does, then you can go about using the api key in a different way to make it more secure. I wouldn't bother as it's not an api with much sensitive functionality.
let query = "Terminator+second"
let url = NSURL(string: "http://api.themoviedb.org/3/search/keyword?api_key=YOURAPIKEY&query=\(query)&language=‌​en")!
let request = NSMutableURLRequest(URL: url)
request.addValue("application/json", forHTTPHeaderField: "Accept")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { data, response, error in
if let response = response, data = data {
print(response)
//DO THIS
print(String(data: data, encoding: NSUTF8StringEncoding))
//OR THIS
if let o = NSJSONSerialization.JSONObjectWithData(data, options: nil, error:nil) as? NSDictionary {
println(dict)
} else {
println("Could not read JSON dictionary")
}
} else {
print(error)
}
}
task.resume()
The response you'll get will have the full list of properties. You need the poster_path and title (or original_title) property of the returned item.