How to call dataTask method several times with a counter? - swift

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

Related

unable to have a result while calling a rest API with Swift

I try to make a small command line program in swift 5 to call a Rest Web service
func callRest() {
URLCache.shared = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
let todoEndpoint: String = "http://myURL"
print (todoEndpoint)
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
exit(1)
}
// private let apiKey = "my API Key"
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"Accept": "application/json",
"API-Key": "my API Key"
]
var session = URLSession.shared
session = URLSession(configuration: config)
let urlRequest = URLRequest(url: url)
let task = session.dataTask(with: urlRequest) { (data, response, error) in
print ("task")
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// check the status code
guard let httpResponse = response as? HTTPURLResponse else {
print("Error: It's not a HTTP URL response")
return
}
print (httpResponse)
}
task.resume()
// Response status
}
print ("debut")
callRest()
print("fin")
And I never see a result...
the line print ("task") is never display
Any help appreciate.
I think I don't really understand the task.resume usage...
(same code work in a playground)

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

Data tasks outside ViewController

I'm gonna start with I'm currently learning swift + iOS so I'm by no means an experienced developer or one for that matter.
My goal is to separate any network calls that are currently done in my view controller to a dedicated class outside of it.
In this view controller i have a IBAction with the following code inside of it:
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else {return}
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
} catch let decodeError as NSError {
print("Decoder error: \(decodeError.localizedDescription)\n")
return
}
}.resume()
What I want, or I think I want to achieve is something like this:
let apiData = "somehow get it from outside"
Then when apiData has info stored in it, execute this next bit of code:
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
How would I achieve this? Thank you.
You can try
class API {
static func userLoginWith(email:String,password:String,completion:#escaping(_ token:String?) -> ()) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else { completion(nil) ; return }
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
completion(apiData.data?.token)
} catch {
print("Decoder error: ",error")
completion(nil)
}
}.resume()
}
}
Inside the VC
API.userLoginWith(email:<##>,password:<##>) { (token) in
if let token = token {
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
}
}

Swift 3 - Function Inside DispatchQueue

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.

Mulitple calls to meetup api swift

I am trying to get a list of events from the meetup's api. Its crashes when request mulitple times as it states can only request once and is limited to 200 requests. I am getting groups then using the "urlName" api call to get the event coming up for that group. The result would be an array of events from a bunch of meetup groups. Here is my code.
func getEventsFromMeetup(complete: (groups: [Meetup], succes: Bool) -> Void) {
var currentUserInterests = [String]()
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let recommendedBaseUrl = "https://api.meetup.com/recommended/groups?key=\(meetupAPIKey)"
let url = NSURL(string: recommendedBaseUrl)
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "GET"
session.dataTaskWithRequest(request) { (data, response, error) in
guard error == nil else {
print(error)
complete(groups: [Meetup](), succes: false)
return
}
guard let data = data else {
print("Error with data")
complete(groups: [Meetup](), succes: false)
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments) as! [NSDictionary]
var groups = [Meetup]()
CurrentUserFM.sharedInstance.getCurrentUserInterests { (interests) in
currentUserInterests = interests
var ints = [String]()
print(json.count)
for j in json {
let m = Meetup(data: j)
ints.append(m.name!)
if let i = m.interestName {
if currentUserInterests.contains(i) {
groups.append(m)
}
}
print("ints: \(ints.count)")
print("json: \(json.count)")
if Int(ints.count) >= Int(json.count) {
dispatch_async(dispatch_get_main_queue(), {
complete(groups: groups, succes: true)
return
})
}
}
}
}catch {
print(error)
}
}.resume()
}
func getEventsForGroups(completionHandler: (meetupEvents: [MeetupEvent]) -> ()) {
self.getEventsFromMeetup { (groups, success) in
var meetupEvents1 = [MeetupEvent]()
var ints = [String]()
for group in groups {
let eventBaseUrl = "https://api.meetup.com/\(group.urlname!)?key=\(meetupAPIKey)"
let url = NSURL(string: eventBaseUrl)
let request = NSMutableURLRequest(URL: url!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
session.dataTaskWithRequest(request, completionHandler: { (data, reponse, error) in
guard error == nil else {
print(error)
return
}
guard let data = data else {
print("Error with data")
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)
let m = MeetupEvent()
if let name = json["name"] as? String {
m.name = name
}
if let link = json["link"] as? String {
m.link = link
}
if let interestName = group.name {
m.interestName = interestName
}
if let image = json["key_photo"]??["thumb_link"] as? String {
m.image = image
}
meetupEvents1.append(m)
ints.append("1")
if Int(ints.count) >= Int(groups.count) {
dispatch_async(dispatch_get_main_queue(), {
completionHandler(meetupEvents: meetupEvents1)
return
})
}
}catch {
print(error)
}
}).resume()
}
}
}
Any suggestions on how to get the events from the groups so that the app wont crash from meetup calls?