1
votes

I’m fetching entities from Core Data in SwiftUI and would like to sort them like so:

  1. If there’s a next date, sort them ascending
  2. Underneath those that have a next date, sort the remaining by date created

I have:

@FetchRequest(entity: MyEntity.entity(), sortDescriptors: [
    NSSortDescriptor(keyPath: \MyEntity.nextDate, ascending: true),
    NSSortDescriptor(keyPath: \MyEntity.dateCreated, ascending: true)
]) private var entities: FetchedResults<MyEntity>

The problem is those that do not have a next date are all sorted above those that do have a non-nil next date, as seen in this other question.

I tried to subclass NSSortDescriptor to move nil values to the end, but it never calls any of the overridden functions.

final class NilsLastSortDescriptor: NSSortDescriptor {
    override func copy(with zone: NSZone? = nil) -> Any {
        return NilsLastSortDescriptor(key: self.key, ascending: self.ascending, selector: self.selector)
    }

    override var reversedSortDescriptor: Any {
        return NilsLastSortDescriptor(key: self.key, ascending: !self.ascending, selector: self.selector)
    }

    override func compare(_ object1: Any, to object2: Any) -> ComparisonResult {
        if (object1 as AnyObject).value(forKey: self.key!) == nil && (object2 as AnyObject).value(forKey: self.key!) == nil {
            return .orderedSame
        }
        if (object1 as AnyObject).value(forKey: self.key!) == nil {
            return .orderedDescending
        }
        if (object2 as AnyObject).value(forKey: self.key!) == nil {
            return .orderedAscending
        }
        return super.compare(object1, to: object2)
    }
}

I also tried to use the NSSortDescriptor API that includes a comparator block, however this crashes with

Exception: "unsupported NSSortDescriptor (comparator blocks are not supported)"

How could I achieve the desired sort behavior?

2
show how you call the subclass NSSortDescriptor in a @ FetchRequest, pleaseE.Coms
@E.Coms updatedJordan H

2 Answers

1
votes

The @FetchRequest uses the sort descriptor as part of a fetch request, and consequently you cannot use comparator blocks in the definition of the fetch request (at least, not with an SQLite store). However, you can sort the results of the fetch (entities) as part of the processing of the body of the view, using sorted(by:):

var body: some View {
        VStack {
        List{
            ForEach(self.entities.sorted(by: { first, second in
                if first.nextDate == nil && second.nextDate == nil { return (first.dateCreated <= second.dateCreated) }
                if first.nextDate == nil { return false }
                if second.nextDate == nil { return true }
                if first.nextDate! == second.nextDate! { return (first.dateCreated <= second.dateCreated) }
                return (first.nextDate! < second.nextDate!)
            }), id: \.self) { myEntity in
                // Your code for your cells...
                ...
            }
        }
    }
}

I've assumed for simplicity that dateCreated is non-optional. If not, you will need to amend the predicate for the sort to handle the nil values. It could probably be written more prettily, too - but I hope you get the idea.

Just to note it's sorting in memory, so no idea of performance and/or memory impact of this solution.

0
votes

In CoreData, sorting is only using method from NSString class like "compare:" or "localizedStandardCompare:" something like those.

As you cannot override those methods or even change them in most cases, you may consider other workarounds.

Instead of "Nil" date, you can set a Special Date which is very late if the desired data is 'Nil'. In this way you can sort those dates into the bottom. You may not display those special dates, or display them as "Nil" in SwiftView.

You can just play NSString game, not the subclass