I want to refactor the API call that is made using async and await but I am getting the error as the publishing needs to be done on the main thread.
The below is the code that I wrote in the file named LogIn View:-
@State private var quotes = [Quote]()
var body: some View {
NavigationView {
List(quotes, id:\.quote_id) { quote in
VStack(alignment: .leading) {
Text(quote.author)
.font(.headline)
Text(quote.quote)
.font(.body)
}
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Log out") {
authentication.updateValidation(success: false)
}
}
}
.navigationTitle("Dashboard Screen ")
}
.task {
await fetchData()
}
}
func fetchData() async {
//create url
guard let url = URL(string: "https://breakingbadapi.com/api/quotes") else {
print("URL does not work")
return
}
//fetch data from url
do {
let (data, _) = try await URLSession.shared.data(from: url)
//decode that data
if let decodeResponse = try? JSONDecoder().decode([Quote].self, from: data) {
quotes = decodeResponse
}
} catch {
print("Data not valid")
}
}
I want to write the function fetchData() in a separate file and use it here in LogIn View but upon trying to do so I am getting the error mentioned above. Can anyone Please help me with this.
PS:- all the variables are defined inside another file named variables. The code for that is as follows:-
import Foundation
struct Quote: Codable {
var quote_id: Int
var quote: String
var author: String
var series: String
}
CodePudding user response:
A nice place is in an extension of NSURLSession
, e.g.
extension NSURLSession {
func fetchQuotes() async throws -> [Quote] {
//create url
guard let url = URL(string: "https://breakingbadapi.com/api/quotes") else {
print("URL does not work")
return
}
//fetch data from url
let (data, _) = try await data(from: url)
//decode that data
return try JSONDecoder().decode([Quote].self, from: data)
}
}
Then you can simply do:
.task {
do {
quotes = try await URLSession.shared.fetchQuotes()
} catch {
errorMessage = error.description
}
}
This has the advantage you can use it with a different kind of URLSession
, e.g. for API requests we usually use an ephemeral session. Another good place would be a static async func in the Quote
struct.
CodePudding user response:
to put your func fetchData()
in a model and avoid the error, try this approach:
class QuotesModel: ObservableObject {
@Published var quotes = [Quote]()
@MainActor // <-- here
func fetchData() async {
guard let url = URL(string: "https://breakingbadapi.com/api/quotes") else {
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
quotes = try JSONDecoder().decode([Quote].self, from: data)
} catch {
print(error)
}
}
}
struct ContentView: View {
@StateObject var model = QuotesModel()
var body: some View {
NavigationView {
List(model.quotes, id: \.quote_id) { quote in
VStack(alignment: .leading) {
Text(quote.author)
.font(.headline)
Text(quote.quote)
.font(.body)
}
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Log out") {
// authentication.updateValidation(success: false)
}
}
}
.navigationTitle("Dashboard Screen ")
}
.task {
await model.fetchData()
}
}
}
struct Quote: Codable {
var quote_id: Int
var quote: String
var author: String
var series: String
}
CodePudding user response:
Make @MainActor method with @Published in ObservableObject class. and for model use codable.
struct QuoteView: View {
@State var quotes: [Quote] = []
@ObservedObject var quoteStore = QuoteStore()
var body: some View {
NavigationView {
List(quotes, id:\.quote_id) { quote in
VStack(alignment: .leading) {
Text(quote.author)
.font(.headline)
Text(quote.quote)
.font(.body)
}
}
.navigationTitle("Quotes")
}
.task {
quotes = try! await quoteStore.fetchData()
}
}
}
struct Quote: Codable {
let quote_id = UUID()
let quote: String
let author: String
}
class QuoteStore: ObservableObject {
@Published var quotes: [Quote] = []
@MainActor
func fetchData() async throws -> [Quote] {
guard var url = URL(string: "https://breakingbadapi.com/api/quotes") else { throw AppError.invalidURL }
let (data, _) = try await URLSession.shared.data(from: url)
let repos = try JSONDecoder().decode([Quote].self, from: data)
return repos
}
}
enum AppError: Error {
case invalidURL
}
CodePudding user response:
depending on your code it may just be as simple as putting the code giving you the error inside await MainActor.run { ... }
That said, as a general rule async code is easier to manage when it returns values to use, rather than setting variables from inside functions.
struct SomeFetcher {
func fetchData() async -> [Quotes] {
...
if let decodeResponse = try? JSONDecoder().decode([Quote].self, from: data) {
return decodeResponse
}
...
}
}
struct TheView: View {
var dataGetter = SomeFetcher()
@State private var quotes = [Quote]()
var body: some View {
NavigationView {
}
.task {
quotes = await dataGetter.fetchData()
}
}
}
Editied: Sorry I wrote this code while sleepy. It does not need to be observed in this case when the view is calling the fetch. Other answers have pointed out that the downloaded info is frequently managed by the class that downloads it and published by it.