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

Time:11-05

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

CodePudding user response:

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

CodePudding user response:

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() ?? []
  }
}
  • Related