Home > database >  SwiftUI: Getting data from an API when a view is initialized
SwiftUI: Getting data from an API when a view is initialized

Time:07-04

New to iOS development and I'm using the MVVM pattern in my SwiftUI app.

I have a user repository which hits my endpoint to retrieve use profile data:

...

private var user: User

...

func fetchUser(token: String, userId: String) async -> User {
    guard let url = URL(string: "ENDPOINT_URL") else {
        return self.user
    }
        
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        if error != nil {
            return
        }

        if let response = response as? HTTPURLResponse {
            print("Response:statusCode: \(response.statusCode)")
        }

        let decoder = JSONDecoder()

        do {
            let profileData = try decoder.decode(User.self, from: data!)
            print("fetchUserProfile:profileData: \(profileData)")
            print("fetchUserProfile:user: \(self.user)")
            // TODO: Do something with the data
            // self.user = profileData
        } catch {
            print("Error decoding data: \(error)")
        }
    }
        
    task.resume()

    return self.user
}

In my view model I call a getUser method like so:

func getUser() async -> Void {
    print("Getting user")
    let userRes = await repository.fetchUser(token: String(describing: userToken), userId: String(describing: userSub))

    print("getUser:userRes: \(userRes)")
    
    // Not entirely sure that I need this
    DispatchQueue.main.async {
        self.user = userRes
    }
}

Finally in my view the view model is stored as an ObservableObject and the getUser method is called using a task view modifier:

struct UserDispatchView: View {
    @ObservedObject private var vm = ViewModel()

    var body: some View {
        VStack(spacing: 25) {
            Text(vm.user.first_name)
                .font(.title3)
                .frame(maxWidth: .infinity, alignment: .leading)
    ...
}
.task { await vm.getUser() }

I'm not getting any errors, but my issue is that the user returned to the view is empty despite being an observable object and publishing its value in the view model.

My question is, since it's not updating the view as expected, how should I go about running an async function when the view initializes such that The text view shown here will properly show the user's first_name?

CodePudding user response:

It will be nil to start. You have to have a ProgressView or some message on the screen while the fetching is happening. There is a delay, it might be little but there is one there. Also, when using async await you need @MainActor so View updates happen on the main thread, once you set that up you don't need the DispatchQueue block.

struct UserDispatchView: View {
    //Use StateObject instead of `ObservedObject`
    @StateObject private var vm : ViewModel

    init(){
        self._vm = StateObject(wrappedValue: ViewModel())
    }

    var body: some View {
        VStack(spacing: 25) {
            if let vm.user.first_name == nil{
                ProgressView()
                    .task { await vm.getUser() }
            }else{
                Text(vm.user.first_name)
                .font(.title3)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
    ...
}

As far as annotating your ViewModel it is easy

@MainActor
class ViewModel:ObservableObject{
    //Your code here

    func fetchUser(token: String, userId: String) async -> User? {
        do{
            //Your code

            //Change to async await
            let (data, response) = try await URLSession.shared.data(for: request)
            //Your code
            return profileData
        }catch{
            //Set the user to `nil` if there is an error
            return nil
            print(error)
        }
    }
}

I suggest you look at the Apple SwiftUI Tutorials and the WWDC videos on async await you can find them in the Developer app or the developer website.

CodePudding user response:

You have a lot of holes in your code

1.

func fetchUser(token: String, userId: String) async -> User { }

Why is this async when you are using regular network API with a completion? It just returns an empty object.

2.

guard let url = URL(string: "ENDPOINT_URL") else {
    return self.user
}

Again, the function returns an empty object if the URL formation fails.

3.

// self.user = profileData

This commented out in the closure.

4.

@ObservedObject private var vm = ViewModel()

This needs to be @StateObject

Here are your options:

  1. Use dataTaskPublisher
  2. Use async network call Returning data from async call in Swift function

In either option, once you decode the data, you need to assign it to user which is a @Published property in your model. Depending on implementation, you will need to downstream it from your network class.

  • Related