Create sortable, ordered, and grouped data from @Query in SwiftD

ghz 8months ago ⋅ 150 views

I've been trying to wrap my head around a sorting and grouping process for a SwiftUI app I've been trying to build.

All the tutorials I've seen have been fairly "basic" when it comes to the sorting and filtering aspect - particularly when using SwiftData.

What I wanted to incorporate was not only sorting by one of the attributes and forward/reverse, but also grouping the data too.

For example, this is code from the Earthquake project by Apple (some items removed for brevity):

struct QuakeList: View {
    @Environment(ViewModel.self) private var viewModel
    @Environment(\.modelContext) private var modelContext
    @Query private var quakes: [Quake]

    init(
        sortParameter: SortParameter = .time,
        sortOrder: SortOrder = .reverse
    ) {
        switch sortParameter {
            case .time:
                _quakes = Query(sort: \.time, order: sortOrder)
            case .magnitude:
                _quakes = Query(sort: \.magnitude, order: sortOrder)
        }
    }

What they do in this is pass in the sortParameter and the sortOrder to this view and it re-renders the view on change/update.

How can I expand this so it also can handle grouping so the quakes variable would really be a multidimensional array or even a dictionary.

For example, I was trying to do this to perform the rendering:

enum GroupOption { // New grouping for Section
    case time
    case magnitude
    case none
}

struct ListScreen: View {
    @Environment(ViewModel.self) private var viewModel
    @Environment(\.modelContext) private var modelContext
    @Query private var quakes: [Quake]
    @State private var groupedQuakes: [[Quake]] = [] // New multidimensional array

    init(
        sortParameter: SortParameter = .time,
        sortOrder: ComparisonResult = .orderedAscending, // using ComparisonResult to store the enum value in defaults
        sortGrouping: GroupOption = .none
    ) {
        switch (sortParameter, sortOrder) {
            case (.time, .orderedAscending):
                _quakes = Query(sort: \.time, order: .forward)
            case (.time, .orderedDescending):
                _quakes = Query(sort: \.time, order: .reverse)

            case (.magnitude, .orderedAscending):
                _quakes = Query(sort: \.magnitude, order: .forward)
            case (.magnitude, .orderedDescending):
                _quakes = Query(sort: \.magnitude, order: .reverse)

            default:
                _quakes = Query(sort: \.time, order: .forward)
        }

        switch sortGrouping {
            case .time:
                groupedQuakes = Dictionary(grouping: _quakes.wrappedValue, by: { $0.time })
                    .sorted(by: { $0.key < $1.key })
                    .map({ $0.value })
            case .magnitude:
                groupedQuakes = Dictionary(grouping: _quakes.wrappedValue, by: { $0.magnitude })
                    .sorted(by: { $0.key < $1.key })
                    .map({ $0.value })
            case .none:
                groupedQuakes = [_quakes.wrappedValue]
        }
    }

Except, when I use it in the view body it is empty. So switching from the // 1 to // 2 makes the array of data return empty.

// 1
List(quakes, selection: $selectedId) { quake in
  QuakeRow(quake: quake)
}

// 2
List {
  ForEach(groupedQuakes, id: \.self) { group in
    Section {
      ForEach(group) { quake in
        QuakeRow(quake: quake)
      }
    } header: {
      groupHeader(for: group)
    }
  }
}
// ...
func groupHeader(for group: [Quake]) -> Text {
  guard let group = group.first else { return Text("Unknown") }
  switch groupOption {
    case .time:
      return Text(group.time.formatted(date: .numeric, time: .omitted))
    case .magnitude:
      return Text("\(group.magnitude)")
    case .none:
      return Text("All quakes")
  }
}

So when I return the general @Query private var quakes: [Quake] there is an array returned with the data.

Using the sorting included in the Apple test project the quakes are sorted correctly.

As soon as I try to add in grouping and sort that data returns blank arrays.

Is there something I'm overlooking?

Answers

It seems like you're trying to implement sorting and grouping functionality in your SwiftUI app using CoreData and SwiftUI's @Query property wrapper. The approach you're taking looks reasonable, but there might be a few things causing the grouped data to return empty.

Here are a couple of things to check and adjust in your implementation:

  1. Ensure Data is Loaded: Make sure that your CoreData fetch request is returning data before you try to group it. You can do this by printing the quakes array in your init method to see if it contains any data.

  2. Check Grouping Logic: Verify that the grouping logic is correct. In your init method, you are sorting the quakes array based on sortParameter and sortOrder, and then grouping it based on sortGrouping. Ensure that the sortGrouping value is set correctly and that the grouping logic is functioning as expected.

  3. Debug Grouped Data: Print the groupedQuakes array to see if it contains any data after grouping. This will help you determine if the grouping logic is working as intended.

  4. Inspect Data Structure: Ensure that the structure of your CoreData entity (Quake) matches the grouping logic you're using. For example, if you're grouping by time, make sure that the time attribute of the Quake entity is correctly populated and formatted.

  5. Verify View Rendering: Check that your view is rendering correctly by using Text elements or other UI components to display information from the groupedQuakes array. This will help you identify if the issue lies in the rendering logic or the data itself.

By carefully checking each of these points and debugging your code, you should be able to identify the cause of the empty grouped data and resolve the issue. Additionally, consider using breakpoints or print statements throughout your code to inspect variable values and execution flow during runtime, which can help pinpoint the problem more accurately.