Home > Mobile >  How to update filtered list in swiftui, when the value in the filter is changed?
How to update filtered list in swiftui, when the value in the filter is changed?

Time:02-06

Usual caveat of being new to swiftui and apologies is this is a simple question.

I have a view where I have a date picker, as well as two arrows to increase/decrease the day. When this date is update, I am trying to filter a list of 'sessions' from the database which match the currently displayed date.

I have a filteredSessions variable which applies a filter to all 'sessions' from the database. However I do not seem to have that filter refreshed each time the date is changed.

I have the date to be used stored as a "@State" object in the view. I thought this would trigger the view to update whenever that field is changed? However I have run the debugger and found the 'filteredSessions' variable is only called once, and not when the date is changed (either by the picker or the buttons).

Is there something I'm missing here? Do I need a special way to 'bind' this date value to the list because it isn't directly used by the display?

Code below. Thanks

import SwiftUI

struct TrainingSessionListView: View {
    
    @StateObject var viewModel = TrainingSessionsViewModel()
    @State private var displayDate: Date = Date.now
    @State private var presentAddSessionSheet = false
    
    private var dateManager = DateManager()
    
    private let oneDay : Double = 86400
    
    private var addButton : some View {
        Button(action: { self.presentAddSessionSheet.toggle() }) {
            Image(systemName: "plus")
        }
    }
    
    private var decreaseDayButton : some View {
        Button(action: { self.decreaseDay() }) {
            Image(systemName: "chevron.left")
        }
    }
    
    private var increaseDayButton : some View {
        Button(action: { self.increaseDay() }) {
            Image(systemName: "chevron.right")
        }
    }
    

    private func sessionListItem(session: TrainingSession) -> some View {
        NavigationLink(destination: TrainingSessionDetailView(session: session)) {
            VStack(alignment: .leading) {
                Text(session.title)
                    .bold()
                Text("\(session.startTime) - \(session.endTime)")
            }
        }
    }
    
    
    private func increaseDay() {
        self.displayDate.addTimeInterval(oneDay)
    }
    
    private func decreaseDay() {
        self.displayDate.addTimeInterval(-oneDay)
    }
    
    
    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    Spacer()
                    decreaseDayButton
                    Spacer()
                    DatePicker("", selection: $displayDate, displayedComponents: .date)
                        .labelsHidden()
                    Spacer()
                    increaseDayButton
                    Spacer()
                }
                .padding(EdgeInsets(top: 25, leading: 0, bottom: 0, trailing: 0))
                    
                Spacer()
                
                ForEach(filteredSessions) { session in
                    sessionListItem(session: session)
                }
                
                Spacer()
                
            }
            .navigationTitle("Training Sessions")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarItems(trailing: addButton)
            .sheet(isPresented: $presentAddSessionSheet) {
                TrainingSessionEditView()
            }
            
        }
    }
    
    var filteredSessions : [TrainingSession] {
        print("filteredSessions called")
        return viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }
    }
}

struct TrainingSessionListView_Previews: PreviewProvider {
    static var previews: some View {
        TrainingSessionListView()
    }
}

CodePudding user response:

There are two approaches and for your case and for what you described I would take the first one. I only use the second approach if I have more complex filters and tasks

You can directly set the filter on the ForEach this will ensure it gets updated whenever the displayDate changes.

ForEach(viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }) { session in
    sessionListItem(session: session)
}

Or you can like CouchDeveloper said, introduce a new state variable and to trigger a State change you would use the willSet extension (doesn't exist in binding but you can create it)

For this second option you could do something like this.

  1. Start create the Binding extension for the didSet and willSet
extension Binding {
    func didSet(execute: @escaping (Value) ->Void) -> Binding {
        return Binding(
            get: {
                return self.wrappedValue
            },
            set: {
                let snapshot = self.wrappedValue
                self.wrappedValue = $0
                execute(snapshot)
            }
        )
    }
    func willSet(execute: @escaping (Value) ->Void) -> Binding {
        return Binding(
            get: {
                return self.wrappedValue
            },
            set: {
                execute($0)
                self.wrappedValue = $0
            }
        )
    }
}
  1. Introduce the new state variable
@State var filteredSessions: [TrainingSession] = []
// removing the other var
  1. We introduce the function that will update the State var
func filterSessions(_ filter: Date) {
    filteredSessions = viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: date) }
}
  1. We update the DatePicker to run the function using the willSet
DatePicker("", selection: $displayDate.willSet { self.filterSessions($0) }, displayedComponents: .date)
  1. And lastly we add a onAppear so we fill the filteredSessions immidiatly (if you want)
.onAppear { filterSessions(displayDate) } // uses the displayDate that you set as initial value

Don't forget in your increaseDay() and decreaseDay() functions to add the following after the addTimeInterval

self.filterSessions(displayDate)

As I said, this second method might be better for more complex filters

CodePudding user response:

Thank you all for your responses. I'm not sure what the issue was originally but it seems updating my view to use Firebase's @FirestoreQuery to access the collection updates the var filteredSessions... much better than what I had before.

New code below seems to be working nicely now.

import SwiftUI
import FirebaseFirestoreSwift

struct TrainingSessionListView: View {
    
    @FirestoreQuery(collectionPath: "training_sessions") var sessions : [TrainingSession]
    
    @State private var displayDate: Date = Date.now
    @State private var presentAddSessionSheet = false
    
    
    private var dateManager = DateManager()
    
    private let oneDay : Double = 86400
    
    private var addButton : some View {
        Button(action: { self.presentAddSessionSheet.toggle() }) {
            Image(systemName: "plus")
        }
    }
    
    private var todayButton : some View {
        Button(action: { self.displayDate = Date.now }) {
            Text("Today")
        }
    }
    
    private var decreaseDayButton : some View {
        Button(action: { self.decreaseDay() }) {
            Image(systemName: "chevron.left")
        }
    }
    
    private var increaseDayButton : some View {
        Button(action: { self.increaseDay() }) {
            Image(systemName: "chevron.right")
        }
    }
    

    private func sessionListItem(session: TrainingSession) -> some View {
        NavigationLink(destination: TrainingSessionDetailView(sessionId: session.id!)) {
            VStack(alignment: .leading) {
                Text(session.title)
                    .bold()
                Text("\(session.startTime) - \(session.endTime)")
            }
        }
    }
    
    
    private func increaseDay() {
        self.displayDate.addTimeInterval(oneDay)
    }
    
    private func decreaseDay() {
        self.displayDate.addTimeInterval(-oneDay)
    }
    
    
    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    Spacer()
                    decreaseDayButton
                    Spacer()
                    DatePicker("", selection: $displayDate, displayedComponents: .date)
                        .labelsHidden()
                    Spacer()
                    increaseDayButton
                    Spacer()
                }
                .padding(EdgeInsets(top: 25, leading: 0, bottom: 10, trailing: 0))
                    
                if filteredSessions.isEmpty {
                    Spacer()
                    Text("No Training Sessions found")
                } else {
                    List {
                        ForEach(filteredSessions) { session in
                            sessionListItem(session: session)
                        }
                    }
                }
                Spacer()
                
            }
            .navigationTitle("Training Sessions")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarItems(leading: todayButton, trailing: addButton)
            .sheet(isPresented: $presentAddSessionSheet) {
                TrainingSessionEditView()
            }
        }
    }
    
    var filteredSessions : [TrainingSession] {
        return sessions.filter { $0.date == dateManager.dateToStr(date: displayDate)}
    }
    
}

struct TrainingSessionListView_Previews: PreviewProvider {
    static var previews: some View {
        TrainingSessionListView()
    }
}
  • Related