Asynchronous call to API - swift

I have a three year old barcode scanning ios app that has been making a synchronous call to an api with no problems until I was forced to use a slighly slower api. The latest version of XCode offer this advice: Synchronous URL loading of https://get-thumb.herokuapp.com/getThumb.php?objectid=20019 should not occur on this application's main thread as it may lead to UI unresponsiveness. Please switch to an asynchronous networking API such as URLSession.
The code below asychronusly loads an ArtObject with string data including a path to another api. How can I get the data from the second, getThumb api asynchronously and load it into the UI on the main thread?
func downLoadJson (forObject: String) {
let myURLStr = urlObj + forObject
print (myURLStr)
guard let downLoadURL = URL(string: myURLStr) else {return}
URLSession.shared.dataTask(with: downLoadURL) { (data, urlResponse, error) in
guard let data = data, error == nil, urlResponse != nil else {
print ("something is wrong in URLSessions call")
return
}
do {
let decoder = JSONDecoder()
let anObject = try decoder.decode(ArtObject.self, from: data)
/*
print("object Name is \(anObject.ObjectName)")
print("Creators is \(anObject.Creators)")
print("Medium is \(anObject.Medium)")
print("Titles is \(anObject.Titles)")
print("LabelUUID is \(anObject.LabelUUID)")
*/
gArtObject = anObject // this populates the global gArtObject with the local anObject so that the vals are avial in other funcs
var displayText = "ObjName: " + anObject.ObjectName
displayText += "\nCreator(s): " + anObject.Creators
displayText += "\nMedium: " + anObject.Medium
displayText += "\nTitle(s): " + anObject.Titles
displayText += "\nObjectNumber: " + String(anObject.ObjectNumber)
displayText += "\nComponentNumber: " + anObject.compNumber
//--- UI update must happen on main queue THIS IS THE PROBLEM
DispatchQueue.main.async {
self.objectDetails.text = displayText
self.imageVW.setImageFromURl(stringImageUrl: anObject.imageSmall)
}
} catch {
print("something wrong after download step")
print(error.localizedDescription)
}
}.resume()
extension UIImageView{
func setImageFromURl(stringImageUrl url: String){
if let url = NSURL(string: url) {
if let data = NSData(contentsOf: url as URL) {
self.image = UIImage(data: data as Data)
}
}
}
}

You can achieve this the same way you are loading your JSON.
In the setImageFromURl function remove the synchronous call to Data and use:
if let url = URL(string: url) {
URLSession.shared.dataTask(with: url) { (data, urlResponse, error) in
if let data = data {
DispatchQueue.main.async{
self.image = UIImage(data: data)
}
}
}.resume()
}

Related

Better alternative to DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) for data refresh

I am looking for a better alternative to DispatchQueue.main.asyncAfter(deadline: .now() + 0.5 to refresh my data.
My data is dynamic and changes based on user action, it's important that the data the user sees is always up to date.
I am using Firebase Realtime Database, I am wondering whether I can alter my service file to refetch data any time something changes. here is my service file:
class Service {
static let shared = Service()
let BASE_URL = "https://firebaseurl.com/jsondata.json"
let calendar = Calendar.current
func fetchClient(completion: #escaping ([Calls]) -> ()) {
guard let url = URL(string: BASE_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
if let error = error {
print("Failed to fetch data with error: ", error)
return
}
guard let data = data else {return}
do {
let myDecoder = JSONDecoder()
myDecoder.dateDecodingStrategy = .secondsSince1970
let calls = try myDecoder.decode([Calls?].self, from: data).filter({
self.calendar.isDateInToday($0?.dateTime ?? Date(timeIntervalSinceReferenceDate: -123456789.0))
})
completion(calls.filter{$0?.callmade != true}.compactMap{ $0 })
} catch let error {
print("Failed to create JSON with error: ", error)
}
}.resume()
}
}
Currently in my Main Controller I am using:
func fetchClient() {
Service.shared.fetchClient { (client) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.client = client
self.collectionView.reloadData()
self.fetchClient()
}
}
}
You can use Realtime Database to observer changes in data.
https://firebase.google.com/docs/database/ios/read-and-write#listen_for_value_events

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

Null Object When Serialising JSON Data [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 3 years ago.
I am relatively new to Swift so please excuse any rookie errors. I am retrieving some data from a web service and serialising the data into an object. However, when I return this object from the enclosing function, it is always null. If I run this code all in the ViewController however, it works fine. It only seems to fail when I split my code out into separate classes/methods (I am trying to implement better practises). I should also add that no error is printed from the print(error) statement.
Thanks in advance for any help!
func getLocationData(lat: String, long: String) -> Location {
locationUrl += lat + "&lon=" + long
print("Location query: " + locationUrl)
let request = NSMutableURLRequest(url: NSURL(string: locationUrl)! as URL)
request.httpMethod = "GET"
var location: Location?
_ = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:
{(data, response, error) -> Void in
if (error != nil) {
print("Error making requets to web service")
} else {
do {
location = try JSONDecoder().decode(Location.self, from: data!)
} catch let error as NSError {
print(error)
}
}
}).resume()
return location!
}
Your code is asynchronous so location var is nil when you force-unwrap it as the response isn't yet returned , you need a completion
func getLocationData(lat: String, long: String,completion:#escaping (Location?) -> ()) {
locationUrl += lat + "&lon=" + long
print("Location query: " + locationUrl)
var request = URLRequest(url: URL(string: locationUrl)!)
request.httpMethod = "GET"
_ = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:
{(data, response, error) -> Void in
if (error != nil) {
print("Error making requets to web service")
} else {
do {
let location = try JSONDecoder().decode(Location.self, from: data!)
completion(location)
} catch {
print(error)
completion(nil)
}
}
}).resume()
}
Call
getLocationData(lat:////,long:////) { loc in
print(loc)
}

URLSession in loop not working

I am trying to downlod multiple JSON files with a URLSession and when I run the funtion one time it works. But the moment I call the getSMAPrices function from a loop it does not work and I can not find out why.
Here is the working download function that works if i call it.
func getSMAPrices(symbol: String) {
let urlString = "https://www.alphavantage.co/query?function=SMA&symbol=\(symbol)&interval=daily&time_period=9&series_type=close&apikey=KPLI12AW8JDXM77Y"
guard let url = URL(string: urlString) else {
return
}
dataTask = defaultSession.dataTask(with: url, completionHandler: { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}
guard let data = data else {
return
}
//Implement JSON decoding and parsing
do {
//Decode retrived data with JSONDecoder and assing type of Article object
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let stockData = try decoder.decode(SimpelMovingAvarage.self, from: data)
//Get back to the main queue
DispatchQueue.main.async {
print(stockData)
}
} catch let jsonError {
print(jsonError)
}
})
dataTask?.resume()
}
And here is my very simple loop that replaces a part in the URL every run cycle. But nothing happens.
public func scanSymbols() {
for symbol in self.symbols {
progress += 1
progresBar.maxValue = Double(symbols.count)
progresBar.doubleValue = progress
//This does not work
getSMAPrices(symbol: symbol.key)
}
}
It's because your dataTask variable appears to be an instance property and is getting overwritten every time you call this method.

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.