In SwiftUI, how can return a function only when an API request is finished? [duplicate] - swift

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 1 year ago.
I'm trying to learn a bit about making API calls in SwiftUI.
I have a function called loadData which runs via the onAppear modifier.
The goal of that function is to see if I currently have data in CoreData.
If there is no data in CoreData, then I'd like to call another function that makes the API call to get the data, but only return the fetched data.
With the example I have below, the getCurrentSol function returns before the async portion is finished. Resulting in no data being returned. What is the appropriate way for me to return the data?
As you can see, I did try a while(true) "trick". But for whatever reason, my results variable never even updates with the fetched data, even though the decodedData variable does contain the proper results.
}.onAppear(perform: loadData)
}
func loadData() {
print("data: \(storedData) ")
print("data.count: \(storedData.count)")
if(storedData.count == 0){
let fetchedData = getCurrentSol()
let currentSol = fetchedData.sol
print("fetchedData: \(fetchedData)")
print("currentSol: \(currentSol)")
}
}
func getCurrentSol() -> CuriosityRoverModel {
var results = CuriosityRoverModel(sol: 0, low: 0, high: 0, opacity: "Sunny", sunrise: "00:00", sunset: "00:00", month: "Month 0")
let urlString = "https://api.maas2.apollorion.com"
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) {data, response, error in
DispatchQueue.main.async {
if let data = data {
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(CuriosityRoverModel.self, from: data)
//This recieves the proper data, but it doesn't get written to the results var
print("decodedData: \(decodedData)")
results = decodedData
} catch {
print("Error: ", error)
}
}
}
}.resume()
// I thought this would be a way to wait for the data
// but results never gets updated so it ends up running endlessly
while(true){
if(results.sol > 0){
return results
}
}
//return results // This would just return the "empty" results var from above before the data is actually retrieved
}
}

There are many ways to achieve what you want. This is one approach, using a closure:
....
.onAppear(perform: loadData)
}
func loadData() {
print("data: \(storedData) ")
print("data.count: \(storedData.count)")
if (storedData.count == 0) {
getCurrentSol() { results in // <--- here
if let fetchedData = results {
let currentSol = fetchedData.sol
print("fetchedData: \(fetchedData)")
print("currentSol: \(currentSol)")
}
}
}
}
// use a completion closure to "return" your results when done, not before
func getCurrentSol(completion: #escaping (CuriosityRoverModel?) -> Void) {
let urlString = "https://api.maas2.apollorion.com"
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) {data, response, error in
DispatchQueue.main.async {
if let data = data {
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(CuriosityRoverModel.self, from: data)
print("decodedData: \(decodedData)")
completion(decodedData) // <--- here, return the results
} catch {
print("Error: ", error) // need to deal with errors
completion(nil) // <--- here, should return the error
}
}
}
}.resume()
}
}

Related

Why is URLSession not returning data in playground?

I cannot understand why this URLSession is not working on my playground. The URL works fine using curl commands on my terminal, so I know it is active, but I cannot see any list of names printed on my console.
The only print I see is the one "called" after that one, it seems there must be some error, but I have no clue about it, no message. Issue must be around the URLSession but cannot get where.
UPDATE
I added an extension to data found on stack, using right after this code
let (data, response) = try await URLSession.shared.data(from: url)
data.printJson()
The data is printed, but still cannot print anything in the for loop, where it should be.
extension Data {
func printJson() {
do {
let json = try JSONSerialization.jsonObject(with: self, options: [])
let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
guard let jsonString = String(data: data, encoding: .utf8) else {
print("Invalid data")
return
}
print(jsonString)
} catch {
print("Error: \(error.localizedDescription)")
}
}
}
And
import Foundation
//-----------------------------------------------
//MARK: - model
//-----------------------------------------------
struct ResponseRandom: Codable {
let users: [User]
}
struct User: Codable, Identifiable {
let id: Int
let first_name: String
let email: String
// "avatar": "https://robohash.org/rationeetsit.png?size=300x300&set=set1"
let date_of_birth: String//"1958-07-18"
}
//-----------------------------------------------
//MARK: - class
//-----------------------------------------------
class HTTPRequest_randomUsers {
// #Published var users = [UserRandom]()
init() {
Task {
await loadData()
}
}
func loadData() async {
print("called")
let numberOfItems = 50
guard let url = URL(string: "https://random-data-api.com/api/v2/users?size=2&response_type=json") else {
fatalError("URL error")
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse else {
print("not valid response")
return}
guard response.statusCode == 200 else {
print("not 200 status")
return}
let decoded = try JSONDecoder().decode([User].self, from: data)
print("decoded")
await MainActor.run {
// users = decoded.users
for item in decoded {
print(item.first_name)
}
}
} catch {
print("error: \(error)")
}
}
}
//here my call
let c = HTTPRequest_randomUsers()
Network requests run asynchronously (i.e., finish later). But by default, simple Playgrounds will stop when they reach the end of their path of execution. One must tell the Playground to continue execution indefinitely in order for the asynchronous results to be retrieved successfully:
import PlaygroundSupport
and
PlaygroundPage.current.needsIndefiniteExecution = true

Publishing changes from background threads is not allowed; make sure to publish values from the main thread

I'm having an issue with a Swift function. I have my class which manages all the networking, and in there I have two functions: fetchData, which gets an array of IDs, and getPosts, which gets all posts related to those IDs via a DispatchGroup and appends them to a published array, which I will then use in the View.
The issue I'm facing is:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates..
The published variable assignment is already wrapped in a DispatchQueue.main.async block. What am I missing?
import Foundation
class NetworkManager: ObservableObject {
var feedPosts: [Int] = []
#Published var posts = [Post]()
func fetchData(feedType: PostsOrderType) -> Void {
if let url = URL(string: "https://hacker-news.firebaseio.com/v0/\(feedType)stories.json") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if (error == nil){
let decoder = JSONDecoder()
if let result = data {
do {
let decodedRes = try decoder.decode([Int].self, from: result)
self.feedPosts = decodedRes
self.getPosts()
} catch {
print("Decoding error: \(error)")
}
}
} else {
print("Error retrieving data: \(error!)")
}
}
task.resume()
}
}
func getPosts() -> Void {
let dGroup = DispatchGroup()
//Limited to 15 to reduce networking times
let firstTenResults = self.feedPosts[0...15]
for id in firstTenResults{
guard let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")
else {
return
}
dGroup.enter()
URLSession.shared.dataTask(with: url){
data, response, error in
let decoder = JSONDecoder()
if let result = data {
do {
let decodedRes = try decoder.decode(Post.self, from: result)
DispatchQueue.main.async {
self.posts.append(decodedRes)
}
} catch {
print("Error decoding data: \(error)")
}
}
dGroup.leave()
}.resume()
}
}
}

Method to return value retrieved from HTTP request [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 4 years ago.
I have a method which performs an HTTP request and retrieves data from a website, it's working as expected, I'm getting the data correctly. What I haven't been able to do is return the retrieved value when the method is called.
Here is the code...
func myFunction(zipCode: String)->String{
var myData:String = ""
let siteLink = "http://example.com/zip/" + zipCode
let url = URL(string: siteLink)
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print(error!)
return
}
guard let data = data else {
print("Data is empty")
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
guard let jsonArray = json as? [[String: String]] else {
return
}
myData = jsonArray[0]["MyPropertyName"]!
// Here, myData outputs, "Info for zip code 52484 from HTTP request"
}
task.resume()
return myData
}
When I call myFunction I get and empty string...
myFunction(zipCode: "52484")// Outputs an empty string
What I was expecting to see is a return value of "Info for zip code 52484 from HTTP request" since the myData variable was modified inside the let task = before the return call. I tried returning inside the let task = but this cannot be done in Swift.
How can I return the retrieved value when the myFunction is called?
You need a completion as request is asynchronous
func myFunction(zipCode: String,completion:#escaping(_ str:String?) -> () ) {
let siteLink = "http://example.com/zip/" + zipCode
let url = URL(string: siteLink)
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print(error!)
completion(nil)
return
}
guard let data = data else {
print("Data is empty")
completion(nil)
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
guard let jsonArray = json as? [[String: String]] else {
completion(nil)
return
}
let myData = jsonArray[0]["MyPropertyName"]!
completion(myData)
}
task.resume()
}
Call
myFunction(zipCode: "52484") { (str) in
if let st = str {
print(st)
}
}

Is there a way to use my array of type Music, in another scope?

I'm attempting to print/dump and array of type Music outside of a function it's created in. I can successfully dump the musicItems array inside of the getMusicData function but when I set the musicItems array outside of the scope, it won't print anything. What am I doing wrong with the scope here? I have a feeling it's super simple but I just can't figure it out. Thanks in advance for taking the time to read this.
edit: It's giving me "0 elements" in the console when I attempt to dump the musicItems array in the ViewController class. Well, the function is in the same class as well so I guess I don't know what to call the first array. The parent array?
struct MusicResults: Decodable {
let results: [Music]?
}
struct Music: Decodable {
let trackName: String?
let collectionName: String?
let artworkUrl30: String?
}
class ViewController: UITableViewController, UISearchBarDelegate {
var musicItems: [Music] = []
#IBAction func musicButton(_ sender: UIBarButtonItem) {
getMusicData()
dump(musicItems)
}
Here is the function.
func getMusicData() {
var musicItems: [Music] = []
guard let searchTerm = searchString else {return}
let newString = searchTerm.replacingOccurrences(of: " ", with: "+", options: .literal, range: nil)
let jsonUrlString = "https://itunes.apple.com/search?media=music&term=\(newString)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let music = try JSONDecoder().decode(MusicResults.self, from: data)
for results in music.results! {
// print(results.trackName!)
musicItems.append(results)
}
//dump(musicItems)
self.musicItems = musicItems
// DispatchQueue.main.async {
// self.tableView.reloadData()
// }
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
Fixed Code
#IBAction func musicButton(_ sender: UIBarButtonItem) {
getMusicData {
music in
dump(music)
}
function:
func getMusicData(completionHandler: #escaping (_ music: [Music]) -> ()) {
...
let music = try JSONDecoder().decode(MusicResults.self, from: data)
for results in music.results! {
musicItems.append(results)
}
completionHandler(musicItems)
...
Your 'getMusicData' function is asynchronous which means that when it executes, it queues data task in a background queue and proceeds the execution and since there are no more institutions it simply returns control to its calling site - 'musicButton()' action, which in its turn executes the next instruction - prints the 'musicItems' array which might (and most likely, is) still not populated as the network call haven’t yet completed. One of the options that you have here is to pass a completion block to your 'getMusicData' function, that runs it after data task gets the results.
Another option is to use Property Observers
var musicItems: [Music] = [] {
didSet {
dump(self.musicItems)
/// This is where I would do the...
// DispatchQueue.main.async {
// self.tableView.reloadData()
// }
}
}
and then
func getMusicData() {
guard let searchTerm = searchString else { print("no search"); return }
let newString = searchTerm.replacingOccurrences(of: " ", with: "+", options: .literal, range: nil)
let jsonUrlString = "https://itunes.apple.com/search?media=music&term=\(newString)"
guard let url = URL(string: jsonUrlString) else { print("url error"); return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { print(err ?? "unknown"); return }
do {
let music = try JSONDecoder().decode(MusicResults.self, from: data)
if let results = music.results {
self.musicItems.append(contentsOf: results)
}
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}

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.