skip to Main Content

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()
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    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()
        }
    }
    

  2. 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search