// // FilterView.swift // Feels // // Created by Trey Tartt on 1/12/22. // import SwiftUI import CoreData class DataHolder: ObservableObject { // year, month, items @Published var data = [Int: [Int: [DayChartView]]]() @Published var numberOfRatings: Int = 0 var uncategorizedData = [MoodEntry]() { didSet { self.numberOfRatings = uncategorizedData.count } } } struct FilterView: View { typealias Year = Int typealias Month = Int let weekdays = [("Sun", 1), ("mon", 2), ("tue", 3), ("wed", 4), ("thur", 5), ("fri", 6), ("sat", 7)] let months = [(0, "J"), (1, "F"), (2,"M"), (3,"A"), (4,"M"), (5, "J"), (6,"J"), (7,"A"), (8,"S"), (9,"O"), (10, "N"), (11,"D")] @State private var toggle = true @State var selectedDays = [Int]() @State private var entryStartDate: Date = Date() @State private var entryEndDate: Date = Date() @State private var showFilter = false @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \MoodEntry.forDate, ascending: false)], animation: .spring()) private var items: FetchedResults @StateObject private var dataHolder = DataHolder() //[ // 2001: [0: [], 1: [], 2: []], // 2002: [0: [], 1: [], 2: []] // ] let columns = [ GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), GridItem(.flexible(minimum: 5, maximum: 50)), ] private func filterEntries(startDate: Date, endDate: Date) { let filteredEntries = PersistenceController.shared.getData(startDate: startDate, endDate: endDate, includedDays: selectedDays) self.dataHolder.data.removeAll() let filledOutData = buildGridData(withData: filteredEntries) self.dataHolder.data = filledOutData self.dataHolder.uncategorizedData = filteredEntries } private func buildGridData(withData data: [MoodEntry]) -> [Year: [Month: [DayChartView]]] { var returnData = [Year: [Month: [DayChartView]]]() if let earliestEntry = data.first, let lastEntry = data.last { let calendar = Calendar.current let components = calendar.dateComponents([.year], from: earliestEntry.forDate!) let earliestYear = components.year! let latestComponents = calendar.dateComponents([.year], from: lastEntry.forDate!) let latestYear = latestComponents.year! for year in earliestYear...latestYear { var allMonths = [Int: [DayChartView]]() // add back in if months header has leading (-1, ""), // and add back gridItem // var dayViews = [DayChartView]() // for day in 0...32 { // let view = DayChartView(color: Mood.missing.color, // weekDay: 2, // viewType: .text(String(day+1))) // dayViews.append(view) // } // allMonths[0] = dayViews for month in (1...12) { var components = DateComponents() components.month = month components.year = year let startDateOfMonth = Calendar.current.date(from: components)! let items = data.filter({ entry in let components = calendar.dateComponents([.month, .year], from: startDateOfMonth) let entryComponents = calendar.dateComponents([.month, .year], from: entry.forDate!) return (components.month == entryComponents.month && components.year == entryComponents.year) }) allMonths[month] = createViewFor(monthEntries: items, forMonth: startDateOfMonth) } returnData[year] = allMonths } } return returnData } private func createViewFor(monthEntries: [MoodEntry], forMonth month: Date) -> [DayChartView] { var filledOutArray = [DayChartView]() let calendar = Calendar.current let range = calendar.range(of: .day, in: .month, for: month)! let numDays = range.count for day in 1...numDays { if let item = monthEntries.filter({ entry in let components = calendar.dateComponents([.day], from: entry.forDate!) let date = components.day return day == date }).first { let view = DayChartView(color: item.mood.color, weekDay: Int(item.weekDay), viewType: .cicle) filledOutArray.append(view) } else { let thisDate = Calendar.current.date(bySetting: .day, value: day, of: month)! let view = DayChartView(color: Mood.missing.color, weekDay: Calendar.current.component(.weekday, from: thisDate), viewType: .cicle) filledOutArray.append(view) } } for _ in filledOutArray.count...32 { let view = DayChartView(color: Mood.missing.color, weekDay: 2, viewType: .cicle) filledOutArray.append(view) } return filledOutArray } var body: some View { VStack { filterButon .padding([.leading, .trailing, .top]) statsView .frame(minWidth: 0, maxWidth: .infinity, minHeight: 120, maxHeight: 120) .cornerRadius(25) .padding([.leading, .trailing]) Text("Total: \(self.dataHolder.numberOfRatings)") .font(.title2) if showFilter { filterView } gridView .onAppear(perform: { let monthEntries = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: selectedDays) entryStartDate = monthEntries.first!.forDate! entryEndDate = monthEntries.last!.forDate! self.dataHolder.data = buildGridData(withData: monthEntries) self.dataHolder.uncategorizedData = monthEntries // filterEntries(startDate: entryStartDate, endDate: entryEndDate) }) } } private var filterButon: some View { Button(action: { withAnimation{ showFilter.toggle() } }, label: { Text(showFilter ? "Close Filters" : "Show Filters") .frame(maxWidth: .infinity) .frame(height: 44) .foregroundColor(Color(UIColor.label)) .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(25) }).frame(maxWidth: .infinity) } struct StatsSubView: View { let data: [MoodEntry] let mood: Mood var body: some View { VStack { Text(String(Stats.getCountFor(moodType: mood, inData: data))) .font(.title) Text(mood.strValue) .foregroundColor(mood.color) } } } private var statsView: some View { ZStack { Color(UIColor.secondarySystemBackground) HStack { Spacer() ForEach(Mood.allValues, id: \.self) { mood in StatsSubView(data: self.dataHolder.uncategorizedData, mood: mood) Spacer() } } } .cornerRadius(25) .padding() } private var filterView: some View { VStack { VStack { ZStack { Color(UIColor.secondarySystemBackground) DatePicker( "Start Date", selection: $entryStartDate, displayedComponents: [.date] ).onChange(of: entryStartDate, perform: { value in filterEntries(startDate: self.entryStartDate, endDate: self.entryEndDate) }) .padding() } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44) .cornerRadius(25) .padding([.leading, .trailing]) ZStack { Color(UIColor.secondarySystemBackground) DatePicker( "End Date", selection: $entryEndDate, displayedComponents: [.date] ).onChange(of: entryStartDate, perform: { value in filterEntries(startDate: self.entryStartDate, endDate: self.entryEndDate) }) .padding() } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44) .cornerRadius(25) .padding([.leading, .trailing]) ZStack { Color(UIColor.secondarySystemBackground) HStack { Spacer() ForEach(weekdays.indices, id: \.self) { dayIdx in let day = String(weekdays[dayIdx].0) let value = weekdays[dayIdx].1 Button(day.capitalized, action: { if let index = selectedDays.firstIndex(of: value) { selectedDays.remove(at: index) } else { selectedDays.append(value) } filterEntries(startDate: entryStartDate, endDate: entryEndDate) }) .frame(maxWidth: .infinity) .foregroundColor(selectedDays.contains(value) || selectedDays.isEmpty ? .green : .red) } Spacer() } } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44) .cornerRadius(25) .padding([ .leading, .trailing]) } filterButon .padding([.leading, .trailing, .top]) } } private var monthsHeader: some View { LazyVGrid(columns: columns, spacing: 0) { ForEach(months, id: \.self.0) { item in Text(item.1) .textCase(.uppercase) } }.padding([.leading, .trailing, .top]) } private var gridView: some View { VStack { monthsHeader .cornerRadius(25) .padding([.leading, .trailing]) VStack { ScrollView { ForEach(Array(self.dataHolder.data.keys.sorted(by: >)), id: \.self) { yearKey in let yearData = self.dataHolder.data[yearKey]! Text(String(yearKey)) .font(.title) yearGridView(yearData: yearData, columns: columns) } } .padding() } } } private struct yearGridView: View { let yearData: [Int: [DayChartView]] let columns: [GridItem] var body: some View { ZStack { Color(UIColor.secondarySystemBackground) VStack { LazyVGrid(columns: columns, spacing: 0) { ForEach(Array(yearData.keys.sorted(by: <)), id: \.self) { monthKey in let monthData = yearData[monthKey]! VStack { monthGridView(monthData: monthData) } } } .padding([.leading, .trailing, .top, .bottom]) } } .cornerRadius(25) } } private struct monthGridView: View { let monthData: [DayChartView] var body: some View { VStack { ForEach(monthData, id: \.self) { view in view } } } } } struct FilterView_Previews: PreviewProvider { static var previews: some View { Group { FilterView() FilterView() .preferredColorScheme(.dark) } } }