Home > front end >  Receiving async data on an ObservedObject
Receiving async data on an ObservedObject

Time:01-11

I am trying to build a Singleton ApiManager class that can build and send all of my API requests for me and send the response data to awaiting request object. In SwiftUI when I see, say a user successfully login, I would like to switch the views to maybe a dashboard. The problem is that I am using an ObservableObject to publish the "isLoggedIn" Bool and I make all of my backend requests on a background thread. I am getting this error

[SwiftUI] 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.

Here is how I perform API requests

func performRequest(with request: some AbstractRequest) {
    var link = host   request.endpoint
    appendParameters(to: &link, with: request.params)
    
    guard let url = URL(string: link) else {
        print("Failed to generate url from \(link)")
        return
    }
    
    let task = URLSession.shared.dataTask(with: url){ data, response, error in
        guard let data = data, error == nil else{
            print("\(error?.localizedDescription ?? "")")
            return
        }
        request.completion(data: data)
    }
    task.resume()
}

Here is an example completion handler (Bonus point if you can tell me why I get this warning

Conditional cast from '[LoginResponse]' to '[LoginResponse]' always succeeds

on the cast line but if I remove the cast it complains.)

func completion(data: Data)
{
    do{
        guard let response = try JSONDecoder().decode([LoginResponse].self, from: data) as? [LoginResponse] else { return }
        delegate.responseReceived(response: response)
    }catch let error{
        print("\(error.localizedDescription)")
    }
}

Here is what my LoginManager looks like

class LoginManager: ObservableObject, ApiRequestDelegate{
    static var shared = LoginManager()
    
    var token = ""
    @Published var isLoggedIn: Bool = false
    
    func responseReceived(response: Any) {
        guard let response = response as? LoginResponse, response.token != -1 else { return }
        token = String(response.token)
        isLoggedIn = true
    }
}

And finally my ContentView

struct ContentView: View {
    @ObservedObject var loginManager = LoginManager.shared
    var body: some View {
        if !loginManager.isLoggedIn {
            LoginView()
        }else {
            OpenLinkButton().ignoresSafeArea()
        }
    }
}

CodePudding user response:

Your completion handler inside URLSession.shared.dataTask is called on a background thread. So everything that follows is on a background thread.

The most simple solution would be to dispatch UI changes to the main Thread. e.g.:

DispatchQueue.main.async{
    isLoggedIn = true
}

another solution would be to decorate the function interacting with the UI with @MainActor atribute. e.g.:

@MainActor
func responseReceived(response: Any) {
  • Related