agmcoder
agmcoder

Reputation: 619

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:))

I am building an app with Swift and SwiftUI. In MainViewModel I have a function who call Api for fetching JSON from url and deserialize it. this is made under async/await protocol. the problem is the next, I have received from xcode the next comment : "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." in this part of de code :

func getCountries() async throws{
    
        countries = try await MainViewModel.countriesApi.fetchCountries() ?? []
}

who calls this one:

func fetchCountries() async throws -> [Country]? {

    guard let url = URL(string: CountryUrl.countriesJSON.rawValue ) else {
        print("Invalid URL")
        return nil
    }
    let urlRequest = URLRequest(url: url)
    do {
        let (json, _) = try await URLSession.shared.data(for: urlRequest)

        if let decodedResponse = try? JSONDecoder().decode([Country].self, from: json) {
            debugPrint("return decodeResponse")
            return decodedResponse
        }
    } catch {
            debugPrint("error data")
        
    }
    return nil

}

I would like to know if somebody knows how I can fix it

Upvotes: 26

Views: 26875

Answers (3)

Hemang
Hemang

Reputation: 27050

@MainActor
func getCountries() async throws {
        countries = try await MainViewModel.countriesApi.fetchCountries() ?? [] }

This should fix this issue.

Just need to add MainActor right before the function starts.

Upvotes: 3

Joakim Danielson
Joakim Danielson

Reputation: 51920

First fetch the data asynchronously and then assign the result to the property on the main thread

func getCountries() async throws{    
    let fetchedData = try await MainViewModel.countriesApi.fetchCountries()
    await MainActor.run {
        countries = fetchedData ?? []
    }
}

Off topic perhaps but I would change fetchCountries() to return an empty array rather than nil on an error or even better to actually throw the errors since it is declared as throwing.

Something like

func fetchCountries() async throws -> [Country] {
    guard let url = URL(string: CountryUrl.countriesJSON.rawValue ) else {
        return [] // or throw custom error
    }
    let urlRequest = URLRequest(url: url)
    let (json, _) = try await URLSession.shared.data(for: urlRequest)

    return try JSONDecoder().decode([Country].self, from: json)
}

Upvotes: 28

user20384561
user20384561

Reputation:

There are two ways to fix this. One, you can add the @MainActor attribute to your functions - this ensures they will run on the main thread. Docs: https://developer.apple.com/documentation/swift/mainactor. However, this could cause delays and freezing as the entire block will run on the main thread. You could also set the variables using DispatchQueue.main.async{} - see this article from Hacking With Swift. Examples here:

@MainActor func getCountries() async throws{
   ///Set above - this will prevent the error
   ///This can also cause a lag
   countries = try await MainViewModel.countriesApi.fetchCountries() ?? []
}

Second option:

func getCountries() async throws{
  DispatchQueue.main.async{
   countries = try await MainViewModel.countriesApi.fetchCountries() ?? []
  }
}

Upvotes: 19

Related Questions