Search code examples
swiftcore-datansfetchedresultscontrollernssortdescriptornslocalizedstring

Sort Core-Data objects by translated properties


My app was recently localized and now I have an issue with NSSortDescriptor and Core Data.

Basically, I have a fetch request that uses an NSSortDescriptor on objects with predefined English properties, and now I need to sort using the translated version instead.

When in the base language (English), this works:

func initTodayGrouped(delegate: NSFetchedResultsControllerDelegate) -> NSFetchedResultsController<Category> {
    let request: NSFetchRequest<Category> = Category.fetchRequest()
    request.predicate = NSPredicate(format: "active == %d", true)
    let sort = NSSortDescriptor(key: #keyPath(Category.title), ascending: true, selector: #selector(NSString.localizedCompare(_:)))
    let sectionSort = NSSortDescriptor(key: #keyPath(Category.typeSection), ascending: true, selector: #selector(NSString.localizedCompare(_:)))
    request.sortDescriptors = [sectionSort, sort]
    let controller = NSFetchedResultsController(fetchRequest: request,
                                                managedObjectContext: managedContext,
                                                sectionNameKeyPath: #keyPath(Category.typeSection),
                                                cacheName: nil)
    controller.delegate = delegate
    return controller
}

Category is my core data entity, and Category.typeSection, a string, is the property used for sorting sections.

extension Category {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Category> {
        return NSFetchRequest<Category>(entityName: "Category")
    }

    @NSManaged public var active: Bool
    @NSManaged public var color: String?
    @NSManaged public var icon: String?
    @NSManaged public var id: UUID?
    @NSManaged public var index: Int16
    @NSManaged public var subtitle: String?
    @NSManaged public var title: String?
    @NSManaged public var typeSection: String? // <-- HERE
    @NSManaged public var events: NSSet?

}

Now I need to use the localized version of this property.

I thought of making an NSString extension with a method which would replace/override NSString.localizedCompare(_:) which would use NSLocalizedString instead of the string itself, but I think I can't get to expose it to the Objective-C runtime (I get "unsupported sort descriptor"). Then I thought maybe add a computed property in the entity extension, which would return the NSLocalizedString, like Category.translatedTypeSection but I get "Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'keypath translatedTypeSection not found in entity Category'").

In the end, I think this answer by Rob Napier would work because OP has a very similar issue, but I'm unable to translate this to Swift.


TL;DR

My core data entities have a typeSection property, which is a predefined (not user input) string in English, used for sorting sections.

Now that the app is localized in other languages, I need the sorting to be done on the translated version of this property instead of the base language version that is recorded in the database.


Solution

  • It's not about exposing your code to ObjC or translating code. You're having trouble because when you use Core Data with SQLite, sort descriptors are executed in SQLite. Neither ObjC nor Swift code can be used for sorting, only expressions that can be converted to SQLite sorting rules. That's what "unsupported sort descriptor" means, it's the framework telling you it can't convert that descriptor into something that runs in SQLite.

    The key path not found error happens because, although the sort descriptor is acceptable for SQLite, it only works on keys that exist in the persistent store. Computed properties don't exist in SQLite, so the error happens because, in SQL terms, you're trying to sort by a column that doesn't exist there.

    The answer you link has a critical difference from your code. You're trying to use a sort descriptor as part of your fetch, which requires a sort descriptor that can work in SQLite. The linked answer performs the fetch with no sort descriptor at all. Then it sorts the results in memory. You could do that too, and either approach you've tried should work, because both approaches are fine for sorting arrays in memory.