I'm using Google Firebase's Cloud Firestore service with a SwiftUI project that details strategies in a video game on a per-map basis. I'm using a View, ViewModel, Model architecture. It might also be worth noting that I'm using the Swift Package Manager to import the APIs rather than CocoaPods.
I have a ScrollView with a few maps fetched from a Firestore Collection. Tapping on the map will present the user with another ScrollView listing the strategies fetched from a separate collection in Firestore. The issue I'm experiencing is that, whenever a UserDefault is changed (either using UserDefaults.standard.setValue(...)
or @AppStorage("settingName") var...
) the contents of the ScrollView disappear. There's no relationship between the Firestore data and UserDefaults (nothing being saved from one to the other).
This only happens to the detail view, not the main view, despite the code being copied and pasted from one ViewModel swift file to the other.
Here's the StratsViewModel.swift code:
class StratsViewModel: ObservableObject {
@Published var strats = [Strat]()
private var db = Firestore.firestore()
func fetchData(forMap: String) {
if strats.isEmpty {
db.collection("maps/\(forMap)/strats").getDocuments { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
return
}
self.strats = documents.map { (queryDocumentSnapshot) -> Strat in
let data = queryDocumentSnapshot.data()
let id = queryDocumentSnapshot.documentID
let name = data["name"] as? String ?? ""
let map = data["map"] as? String ?? ""
let type = data["type"] as? String ?? ""
let side = data["side"] as? String ?? ""
let strat = Strat(id: id, name: name, map: map, type: type, side: side)
return strat
}
}
}
}
}
In the MapsDetailView.swift (presented by MapsView.swift) ScrollView, the strategies are displayed like so:
struct MapsDetailView: View {
var map: Map
@ObservedObject private var viewModel = StratsViewModel()
var body: some View {
ScrollView {
ForEach(viewModel.strats, id: \.self) { strat in
NavigationLink(destination: StratView()) {
StratCell(strat: strat)
}
}
}
.onAppear() {
self.viewModel.fetchData(forMap: map.id)
}
}
}
If I were to add a button to the view, for example, that set any value to any key in UserDefaults, the dozens of items in the ScrollView disappear.
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
UserDefaults.standard.setValue("test", forKey: "testKey")
} label: {
Text("Don't press me!")
}
}
}
Using the TabBar to navigate to another view, and then back to the MapDetailView will trigger the .onAppear
method however, adding a breakpoint to the ForEach
line shown above, reveals that this loop doesn't run again until the view is entirely dismissed and reopened.
The issue also crops up when switching tabs in the TabBar as, onAppear, each view sets a tabSelection key to remember which tab the user last selected when the app is killed. Switching from one tab, back to the MapsDetailView tab will remove all cells in the ScrollView.
Any thoughts? Happy to share more source code if necessary.
When you initialize the viewModel
, you should use @StateObject
instead of @ObservedObject
. The two property wrappers are almost the same, except the @StateObject
will make sure that the object stays "alive" when the view is updated / re-rendered, rather than re-initializing. Here are some great resources on this topic: