Dynamically change sort order a SwiftUI Sectioned List

I'm trying to dynamically change the sort order to sections SwiftUI list.

The Core Data model consists of an Item Entity with a group and timestamp attribute.

import SwiftUI
import CoreData


struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @State var counter: UInt32 = 0
    @State var order: SortOrder = .reverse

    @SectionedFetchRequest(
        sectionIdentifier: \.group!,
        sortDescriptors: [SortDescriptor(\.timestamp, order: .reverse)],
        predicate: nil,
        animation: .default
    )
    private var items: SectionedFetchResults<String, Item>

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { section in
                    Section(header: Text("\(section.id)")) {
                        ForEach(section) { item in
                            NavigationLink {
                                VStack {
                                    Text("Item at \(item.timestamp!, formatter: itemFormatter)")
                                    Text("\(item.group!)")
                                }
                            } label: {
                                VStack {
                                    Text(item.timestamp!, formatter: itemFormatter)
                                }
                            }
                        }
                    }
                }
            }
            .navigationTitle("Sectioned List")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
                ToolbarItem(placement: .principal) {
                    Button(action: toggleSortOrder) {
                        Label("Sort Oder", systemImage: "arrow.up.arrow.down")
                    }
                }
            }
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
            newItem.group = "Group #\(counter)"
            
            counter = (counter + 1) % 3

            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
    
    private func toggleSortOrder() {
        order = order == .reverse ? .forward : .reverse
        items.sortDescriptors = [SortDescriptor(\.timestamp, order: order)]
    }
}

As soon as I toggle the sort order, the app crashes with the following error code. The same code with a non-sectioned list works perfectly. In UIKit applications there was a possibility to set tableView.beginUpdates()and tableView.endUpdates(). Is there any similar functions available in SwiftUI?

2022-01-24 22:09:17.323096+0100 SectionedListSortOrder[69887:2007668] *** Assertion failure in -[_UITableViewUpdateSupport _computeRowUpdates], UITableViewSupport.m:568
2022-01-24 22:09:17.347996+0100 SectionedListSortOrder[69887:2007668] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView internal inconsistency: encountered out of bounds global row index while preparing batch updates (oldRow=8, oldGlobalRowCount=8)'

Replies

Hi, I checked out some WWDC 2021 videos and found some hints. The following code changes the sort order of the groups. That's works fine so far. But in addition to that I'd like to change the sort order of the timestamps as well.

How can I do this? As soon as I add a second sort descriptor, the app crashes with the previous error.

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @State var counter: UInt32 = 0
    @State var order: SortOrder = .reverse

    @SectionedFetchRequest(
        sectionIdentifier: \.group!,
        sortDescriptors: [SortDescriptor(\Item.group, order: .reverse)],
        predicate: nil,
        animation: .linear
    )
    private var sectionedItems: SectionedFetchResults<String, Item>

    var body: some View {
        NavigationView {
            List {
                ForEach(sectionedItems) { section in
                    Section(header: Text("\(section.id)")) {
                        ForEach(section, id: \Item.id) { item in
                            NavigationLink {
                                VStack {
                                    Text("Timestamp: \(item.timestamp!, formatter: itemFormatter)")
                                    Text("UUID: \(item.uniqueId!)")
                                    Text("\(item.group!)")
                                }
                            } label: {
                                VStack {
                                    Text(item.timestamp!, formatter: itemFormatter)
                                }
                            }
                        }
                    }
                }
            }
            .navigationTitle("Sectioned List")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
                ToolbarItem(placement: .principal) {
                    Button(action: toggleSortOrder) {
                        Label("Sort Order", systemImage: "arrow.up.arrow.down")
                    }
                    .onChange(of: order) { _ in
                        print("onChange order = \(order)")
                        let sortDescriptors = [SortDescriptor(\Item.group, order: order)]
                        let config = sectionedItems
                        config.sortDescriptors = sortDescriptors
                        config.sectionIdentifier = \Item.group!
                    }
                }
            }
        }
    }

    private func addItem() {
        // ...
    }

    private func toggleSortOrder() {
        order = (order == .reverse ? .forward : .reverse)
        print("toggleSortOrder: order = \(order)")
    }
}
  • I further investigated the issue. It seems that the sectionIdentifier keyPath must be equal to the sortDescriptors keyPath. If so, the mechanism in developer.apple.com/videos/play/wwdc2021/10017 works.

    If not, the app crashes with the error shown above. The dynamic reconfigure seems to be very limited.

Add a Comment

I have the same issue. I wonder, if sectionIdentifier and sortDescriptor have to be identical, how am I able to use only certain aspects of a field as sectionIdentifier?

Example 1: I have a model with a date field. I want to group/section by day. For that I created a computed property (with @objc so it's usabke as sectionIdentifier), that trims the date down to a String, representing the iso day of the date ("2022-10-25"). This isn't usable inside sortDescriptor. There I have to use the correct attribute, and then the app crashes.

Example 2: Having a title: String, wanting to group/section by first letter. Again I needed an @objc calculated property @objc var firstLetterOfTitle: String, and using \.title in sortDescriptors. Crash as soon as you change sort order.

Any ideas?