Welcome back to another edition of the man who cannot manage state in Swift!
In this addition, we are going to figure out why the View is not updating despite an @Published variable is being changed.
The exhibited behavior is that our APIResultView
enters the .loading
state but never exits it, despite us recieving data! A classic problem.
Here is our lovely feature code for this episode:
A VM that seems to be constructed correctly!
///View Model for the Admin page
class AdminVM : ViewModel {
var community_id: Int = 0 //Current session's community identifier
var profile_id: Int = 0 //Current session's profile identifier
///Pulled Notifications from database
@Published var Notifications: ViewResult<[AdminNotification]> = ViewResult(defaultValue: [])
///API Dispatcher
private var dispatcher : ViewModelAPIDispatcher = ViewModelAPIDispatcher()
func Initialize(community_id: Int, profile_id: Int){
self.community_id = community_id
self.profile_id = profile_id
self.OnViewOpen()
}
func OnViewOpen() {
dispatcher.SendRequestViewUpdate(.getAdminNotifications(community_id: community_id)){ result in
self.Notifications.SetData(result)
}
}
func Refresh() {
self.Notifications.Refresh()
self.OnViewOpen()
}
The view it is attached to…
struct AdminView: View {
private var community_id = 0
private var profile_id = 0
@StateObject private var VM = AdminVM()
init(community_id: Int, profile_id: Int) {
self.community_id = community_id
self.profile_id = profile_id
}
var body: some View {
APIResultView(apiResult: $VM.Notifications) { notifications in
//Success display code
}
}.refreshable {
VM.Refresh()
}.onAppear(){
VM.Initialize(community_id: community_id, profile_id: profile_id)
}
}
and the view containing logic of what is being displayed!
struct APIResultView<Content : View, T>: View {
@Binding var apiResult : ViewResult<T> //Actual data being displayed
var successDisplay : (T) -> Content //What to display if suscessful
//What to display if we have an object, are waiting, or get a code back.
var body: some View {
switch apiResult.state{
case .object:
successDisplay(apiResult.data!)
case .loading:
ProgressView("Loading...")
case .code(let code):
HttpStatusView(statusCode: code)
}
}
}
We see some familiar faces here, so lets see what we’ve tried.
Initially, I was initilizing my state object as with the following constructor within in the View init():
StateObject(wrappedValue: AdminVM(...))
I read on the docs that this could lead to unexpected behavior, so now it is initialized in the onAppear() of the view.
I also have breakpointed to ensure that .SetData() for the ViewResult is being called, and it is.
Lastly, I’ve found that if I add an @State variable to the view, our characters behave as expected, which is bonkers! If someone could explain why that is, that would be wonderful.
It is also worthy to that this design works with in my many other, numerous views, but that have more complex state, so I’ve not run into this until now it seems.