I’m attempting to retrieve data for a user profile, and I’m not sure how to initialize the observable objects in my view model. I see a lot of examples of where an array of objects will be returned, but not one single object.
How can I initialize the profile
variable in the view model with a null value before I attempt to fetch the profile data, or should I not use this method for retrieving this data?
Also on a side note, is it best practice to write a common class (like a RestManager class) to handle all API requests and have one method which invokes the URLSession dataTaskPublisher method?
Model:
struct Profile: Codable, Hashable, Identifiable {
var id: UUID
var address: String
var city: String
var emailAddress: String
var firstName: String
var lastName: String
var smsNumber: String
var state: String
var userBio: String
var username: String
var zipCode: Int
}
ViewModel:
class ProfileViewModel: ObservableObject {
// Cannot create Profile() as it needs data to be instantiated
@Published var profile: Profile = Profile()
init() {
fetchProfile()
}
func fetchProfile() {
guard let url = URL(string: "https://path/to/api/v1/profiles") else {
print("Failed to create URL")
return
}
let requestData = [
"username": "testuser"
]
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "GET"
request.httpBody = try? JSONEncoder().encode(requestData)
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { output in
guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
throw HTTPError.statusCode
}
return output.data
}
.decode(type: Profile.self, decoder: JSONDecoder())
.retry(3)
.replaceError(with: Profile()) // Same issue here - requires data to be instantiated
.eraseToAnyPublisher()
.receive(on: DispatchQueue.main)
.assign(to: &$profile)
}
}
View:
struct ProfileSummary: View {
@ObservedObject var viewModel: ProfileViewModel
var body: some View {
NavigationView {
VStack {
Text(viewModel.profile.firstName)
Text(viewModel.profile.lastName)
}
.navigationTitle("Profile")
.onAppear {
viewModel.fetchProfile()
}
}
}
}
3
Answers
you could try something like this, making "Profile" an optional:
(Note, in your "ProfileViewModel", I can’t see where you actually assign a value to "profile". Seems you are missing the sink/receiveValue)
Edit: to fix your other problems in "ProfileViewModel", you could try:
A possible solution – to avoid optionals – is a static property with creates an empty sample instance
and use it
and also in the
replaceError
operator lineOr create an enum for all possible states for example
In the view
switch
on the state and render appropriate UISide notes:
let
).eraseToAnyPublisher()
is pointless if the pipeline ends in the same scope.Instead of letting a view attempt to render content that does not exist (how the heck can that be implemented anyway??)
specify clearly what to render at any time.
So, instead of attempting to draw an optional whose value is
.none
:Your "view state" or a part of it:
In your View Model:
Your
NoContent
represents exactly what to render when the profile value is absent. This also enables you to render a nice representation for "no profile available (yet)" since you can render any view you (or UX) wants to look at when you are about to load the profile – or when an error occurred, or …