Home > Blockchain >  How do I refactor the code for making the API call using async await in swift
How do I refactor the code for making the API call using async await in swift

Time:09-27

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
}

enter image description here

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.

  • Related