I have a subview which is a List containing a ForEach on a Published array variable from an ObservedObject. The subview is displayed within a TabView and NavigationView on the main ContentView.
However, the List does not render the updated array when it is updated from a network request using the onAppear function.
Placing the network call in another callback, for example, an on-tap gesture on another view element causes the view to update correctly.
I have tried using willSet and calling objectWillChange manually, to no success.
ServerListView.swift:
struct ServerListView: View {
@ObservedObject var viewModel: ViewModel
@State private var searchText = ""
var body: some View {
List {
TextField("Search", text: self.$searchText)
ForEach(self.viewModel.servers) { server in
Text(server.name)
}
}
.navigationBarTitle(Text("Your Servers"))
.onAppear(perform: fetchServers)
}
func fetchServers() {
self.viewModel.getServers()
}
}
ViewModel.swift:
class ViewModel: ObservableObject {
private let service: ApiService
@Published var servers: [Server] = []
init(service: ApiService) {
self.service = service
}
func getServers() {
self.service.getServers { [weak self] result in
switch result {
case .failure(let error):
print(error)
case .success(let serverListModel):
DispatchQueue.main.async {
self?.servers = serverListModel.servers
}
}
}
}
}
If this view is not placed within a parent view, then the functionality works as intended.
Edit for more information: The ApiService class just handles a URLSession dataTask and the model conforms to Decodable to decode from JSON.
The parent View creates ServerListView as follows in the example, where service.authenticated is a Bool which is set true or false depending on whether the client is authenticated with the API. I have tested without any other code, just by creating the ServerListView inside the body and nothing else. The same outcome is seen.
struct ContentView: View {
var service: ApiService
@ViewBuilder
var body: some View {
if service.authenticated {
TabView {
NavigationView {
ServerListView(ServerListViewModel(service: service))
}
.tabItem {
Image(systemName: "desktopcomputer")
Text("Servers")
}
}
}
else {
LoginView(service: service)
}
}
}
After further testing, placing the API Call in the struct initializer in ServerViewList.swift means the code works as intended. But, this does mean the API call is placed twice. The struct appears to be constructed twice, but the onAppear is only called once.