Etienne Beaule
Etienne Beaule

Reputation: 207

Grouping FechedResults by Month/Year to display a swiftui list with sections

I have a core data entity (LogEntry) that includes a date attribute. I'm trying to migrate a view to SwiftUI and the view must show all the log entries grouped by Year Month (and sorted chronologically) like so:

JUN 2017

JUL 2017

I have been able to achieve the desired grouping with this code:

func group(_ result : FetchedResults<LogEntry>) -> [[LogEntry]] {
    return  Dictionary(grouping: result){ (element : LogEntry)  in
        sectionDateFormatter.string(from: element.date as Date)
    }.values.map{$0}
}

var body: some View {
    NavigationView {
        List {
            ForEach(group(logEntries), id: \.self) { (section: [LogEntry]) in  //Works but not ordered
                Section(header: Text( self.sectionDateFormatter.string(from: section[0].date as Date))) {
                    ForEach(section, id: \.self) { logEntry in
                        HStack {
                            Text("\(self.logDateFormatter.string(from: logEntry.date as Date))")
                            Spacer()
                            Text("\(logEntry.total) m").font(.subheadline)
                        }
                    }
                }
            }.id(logEntries.count)
        }
        .listStyle(PlainListStyle())
    }
}

I have been trying to add the sorting for a while and I can't seem to find the right way to do it in the context of SwiftUI.

I tried this:

func update(_ result : FetchedResults<LogEntry>)-> [[LogEntry]]{
        return  Dictionary(grouping: result){ (element : LogEntry)  in
            sectionDateFormatter.string(from: element.date as Date)
        }.values.sorted() { $0[0].date! < $1[0].date! }
    }

... but I get a "Type of expression is ambiguous without more context" error and it fails to compile.

I also tried this:

func groupedByYearMonth(_ result: FetchedResults<LogEntry>) -> [Date: [LogEntry]] {
      let empty: [Date: [LogEntry]] = [:]
      return logEntries.reduce(into: empty) { acc, cur in
        let components = Calendar.current.dateComponents([.year, .month], from: cur.date as Date)
          let date = Calendar.current.date(from: components)!
          let existing = acc[date] ?? []
          acc[date] = existing + [cur]
      }
    }

... but in this case, I couldn't figure out how to tweak the SwiftUI list ForEach to work with it.

Any pointers would be greatly appreciated!

Upvotes: 0

Views: 1072

Answers (1)

Joakim Danielson
Joakim Danielson

Reputation: 52013

You can sort the the fetched results if you use a different key in the temporary dictionary that can be properly sorted. So for the DateFormatter used in the function I set the format to "yyyy-MM" instead.

Note that I start by sorting the input to the function but this step is not needed if the fetched result is already sorted on date which I recommend.

func group(_ result : FetchedResults<LogEntry>) -> [[LogEntry]] {
    let sorted = result.sorted { $0.date < $1.date } 

    return Dictionary(grouping: sorted) { (element : LogEntry)  in
        dictionaryDateFormatter.string(from: element.date as Date)
    }.sorted { $0.key < $1.key }.map(\.value)
}

Upvotes: 2

Related Questions