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:
- Use dataTaskPublisher
- 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.