Navigation view -> destination - after Firestore update view jumps back

Hello together,

i do have a navigation view

struct SettingsView: View {
    
    private let choices = [
        Choice(imgName: "flag", topic: "Country / League"),
        Choice(imgName: "square.and.pencil", topic: "Scoring"),
        Choice(imgName: "tv.and.mediabox", topic: "Scoreboard"),
        Choice(imgName: "person", topic: "Profile settings"),
        Choice(imgName: "info.circle", topic: "Information")
    ]
    
    @State private var selectedSetting: Choice?
    
    var body: some View {
        NavigationView {
            VStack{
                List(choices) { choice in
                    NavigationLink(
                        tag: choice,
                        selection: $selectedSetting,
                        destination: {
                            DetailSettingsView(arg: "\(choice.topic)")
                        }, label: {
                            HStack {
                                Image(systemName: choice.imgName)
                                    .resizable()
                                    .scaledToFit()
                                    .frame(width: 24)
                                    .padding(.trailing)
                                
                                Text("\(choice.topic)")
                                    .font(.body)
                                Spacer()
                            }
                        }
                    )
                }
            }
        }
    }
}

destination points to a DetailSettingsView:

struct DetailSettingsView: View {
    
    // Store Settings by using class UserDefaults
    @AppStorage("country") var country: String = ""
    @AppStorage("club") var club: String = ""
    
    @EnvironmentObject var model: ContentModel
    @EnvironmentObject var settings: AppSettings
    
    let arg: String
    
    var body: some View {
        if arg == "Country / League" {
            VStack {
                Form {
                    Section(header: Text("Select Country and App-Language")) {
                        Picker ("Country", selection: $country) {
                            ForEach(model.countries) { arg in
                                Text(arg.name).tag(arg.id)
                                }
                            }
                        .onChange(of: country, perform: { value in
                            print(value)
                                model.getClubsInCountry(country: value)
                            if model.clubs.count > 0 {
                                club = model.clubs.first!.name
                            }
                            })
                    }
                    
                    Section(header: Text("Select Your Home Club")) {
                        Picker ("Club", selection: $club) {
                            ForEach(model.clubs) { arg in
                                Text(arg.name).tag(arg.id)
                                }
                            }
                    }
                }
            }
        }
        else if arg == "Scoring" {
                 // ...
        }
        else if arg == "Scoreboard" {
            Text("efg")
        }
    }
}

All Data’s are fetched from a Firestore database.

class ContentModel: ObservableObject {
    
    // Reference to Cloud Firestore database
    let db = Firestore.firestore()
    @Published var countries = [Country]()

func getSupportedCountries() {
        
        // Get a referene to the countries collection
        let db = Firestore.firestore()
        
        db.collection("countries")
            .getDocuments { snapshot, error in
            // Check there's no errors
            if error == nil {
                
                // Declare temp country list
                var temp = [Country]()
                
                for doc in snapshot!.documents {
                    var m = Country(id: doc["id"] as? String ?? "",
                                name: doc["name"] as? String ?? "",
                                language: doc["language"] as? String ?? "")
                    
                    temp.append(m)
                }
                
                DispatchQueue.main.async {
                    self.countries = temp
                }
            }
        }
        
    }
}

The problem I’m facing right now is, that when .onChange() applies, because the user has changed the country, new data get fetched from the Firestore database (model.getClubsInCountry(country: value)). After fetching the clubs from the selected country, the DetailSettingsView closes and SettingsView appears again.

Why? how can I enforce Swift to stay in the DetailView after fetching data from Firestore?

Thanks for your help!

Peter

I’ll make a wild guess and suggest that you should have the modifier:

.navigationViewStyle(.stack)

added to your closing brace of the NavigationView in your SettingsView.

unfortunately it didn’t solve the problem. With this modifier the DetailSettingsView stays for some tenth of a second after selecting a value and then disappears again. Something is triggering this behavior, but what?

From this, it looks to be a bug in SwiftUI

My question would be, “Is the Settings immediately causing the data to be reloaded from Firebase which narrows the countries you are viewing?”

If so then perhaps you should consider using filters to derive a subset of the data already retrieved.

hy Chris,

as soon as there is a change in the picker “country” i trigger reading the new datas from the database.

Would you be so kind the show me a short example to understand the filter concept?

Thanks and best regards

Peter

Thanks Mikaela,

there interesting potential solutions shown. I will try them in the evening and will keep you updated.

BR, Peter

OK so what you would do is read the data from Firebase as you currently do and populate the array countries but also create an array which contains all your data.

In your ContentModel, my suggestion is to create an additional array of allCountries with the same specification as your existing array of countries but this new array does not need to be a @Published property. ie:

var allCountries = [Country]()

In your method to getSupportedCountries(), add the line:

allCountries = temp

inside the DispatchQueue block. Like this:

DispatchQueue.main.async {
    self.countries = temp
    self.allCountries = temp
}

You now have an array of all Countries that forms the source from which you filter which means you do not need to make another call to the Firebase database.

To apply a filter to give you a filtered list in the array countries you could do this:

Create a variable in your ContentModel to store the country name you want to filter by. eg:

var filterByCountry = ""   //Initialised to a empty string.

The code to do the filtering would be something like this:

countries = allCountries.filter { $0.name == filterByCountry }

This would need to be in a method (function) that you would call after the user selected the country he/she wants to filter by.

In order for this to work you need a means of presenting a list of the fields you want to make a filter available for that does not involve navigating to a Settings View via a NavigationLink that could be affected by the ObservedObject changing while you are selecting the filter values .

My suggestion is that you create a Filter View that is presented by tapping on Toolbar item (for example) and then set the filters you require and tap on a button to apply them which will return you to the List View where the data presented is the filtered View.

The filters are defined in the ContentModel and set in this FilterView so you could filter by country, club, playerName… whatever you like.

If you need to progressively filter data by country, then club, then player (for example) you could do this:

var filteredData = allCoountries
filteredData = filteredData.filter { $0.name == filterByCountry }
filteredData = filteredData.filter { $0.clubName == filterByClub }
filteredData = filteredData.filter { $0.playerName == filterByPlayer }

countries = filteredData

Here you are progressively narrowing down your data.

Does that make sense?

I presume the way you are trying to do it now is that you are using the data to narrow down to the country first which gives you a list of possible clubs in that country which you then select from to further narrow the data.

Hy Chris,

thanks for your explanation. Now I understand what you have meant. For an old c-code programmer this is not the nicest coding style …

But for me this is a real serious questions. Is this the suggested way to deal with Firebase Firestore datas? Load everything and then start to filter? Could be a massive amount of datas. I’m coming from a complete different direction. Keep data in memory as low as possible. But than read in realtime from a database in dependence of user actions → dynamic behavior.

Are there best practice hints for correct usage of Firestore? Or what is your opinion about this topic?

The thing about Firebase is that constant calls to it will start to cost a lot because they charge based on traffic volume. if it’s a massive database then I guess you don’t have any option but that said, employing means to limit the number of calls to the bare minimum should be an objective.

This from a Google search:
“Firebase Realtime Database allows for 100 simultaneous connections, 1 GB stored, and 10 GB per month downloaded on the free tier. However, the Google Firebase cost on the paid tier translates to 200,000 per database, $ 5 per GB stored, and $ 1 per GB downloaded , while multiple databases per project are permitted”