0

First of all, sorry about the post length but I am very new to iOS and SwiftUI development and I don't want to miss any details. I did some small projects with Kotlin on Android and Flutter, so I had some experience in app development.

Context

I trying to create a simple app that persists the user data on CoreData and I trying to follow MVVM architecture to develop the app. I was inspired by the following post on Medium. And I have the following files:

  • DataSource.swift: Class that abstracts the initialization of NSPersistentContainer.
  • Entity.swift: Protocol for CoreData entity class standardization.
  • ProductEntity.swift: Particular CoreData class definition that conforms Entity protocol.
  • Model.swift: Class with Entity generic that abstracts the model instantiation and updating process.
  • ProductModel.swift: Particular CoreData entity model definition that inherits Model<ProductEntity> (where exception raises).

The exception

I got an exception initializing the ProductsModel class (ProductsModel.swift, check it below) and I don't have any idea about where are the error source and its reason.

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'An instance of NSFetchedResultsController requires a fetch request with sort descriptors'

I hope you can give me some clues! :)

The code

DataSource.swift:

import Foundation
import CoreData

let defaultDatabase = "DB"

class DataSource {
    static let shared = DataSource()
    public let container: NSPersistentContainer
    
    init(dbName: String = defaultDatabase) {
        container = NSPersistentContainer(name: dbName)
        container.loadPersistentStores { (_, err) in
            if let error = err as NSError? {
                print("NSError \(error) - \(error.userInfo)")
                return
            }
        }
    }
    
    func save() {
        do {
            print("Saving context")
            try self.container.viewContext.save()
            print("Successfully saved context")
        } catch {
            print("ERROR: \(error as NSObject)")
        }
    }
}

Entity.swift:

import CoreData

protocol Entity: NSFetchRequestResult {
    associatedtype CurrentEntity: NSManagedObject
    static var name: String { get }
}

ProductEntity.swift:

import os
import CoreData

@objc(ProductEntity)
public class ProductEntity: NSManagedObject, Entity {
    typealias CurrentEntity = ProductEntity
    static let name: String = "Product"
}

extension ProductEntity : Identifiable {
    public var ID: String {
        self.objectID.uriRepresentation().absoluteString
    }
}

extension ProductEntity {
    @NSManaged public var desc: String?
    @NSManaged public var name: String
    @NSManaged public var price: Double
    @NSManaged public var rations: Int16
    @NSManaged public var shoppingList: NSSet?
}

Model.swift:

import Combine
import CoreData
import os

class Model<T: Entity>: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
    var records = CurrentValueSubject<[T.CurrentEntity], Never>([])
    private let controller: NSFetchedResultsController<T.CurrentEntity>
    
    override init() {
        controller = NSFetchedResultsController(
            fetchRequest: NSFetchRequest<T.CurrentEntity>(entityName: T.name),
            managedObjectContext: DataSource.shared.container.viewContext,
            sectionNameKeyPath: nil, cacheName: nil
        )
        
        super.init()
        
        controller.delegate = self
        
        do {
            try controller.performFetch()
            records.value = (controller.fetchedObjects ?? []) as [T.CurrentEntity]
        } catch {
            NSLog("Error: could not fetch objects")
        }
    }
    
    public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        guard let records = controller.fetchedObjects as? [T.CurrentEntity] else { return }
        self.records.value = records
    }
    
    public func save() {
        DataSource.shared.save()
    }
}

ProductModel.swift:

import os

class ProductsModel: Model<ProductEntity> {
    static let shared: ProductsModel = ProductsModel() // <-- This line raise the exception
}
lucasmenendez
  • 116
  • 1
  • 11
  • As the exception says, the `NSFetchRequest` that you pass to the `NSFetchedResultsController` must have at least one sort descriptor. https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller. You could pass a sort key to your model initialiser. Honestly, if you are just starting out you are probably making life hard for yourself trying to adopt MVVM unless you have a lot of experience with this on another platform. MVVM is not a natural fit for iOS, particularly if you are using UIKit unless you use Combine or a similar binding framework. – Paulw11 Sep 22 '21 at 11:20

1 Answers1

1

NSFetchedResultsController is a tool which could manage your search results from Core Data. It needs at least one sort descriptor to maintain the list of your fetch request. So you should improve the NSFetchRequest you defined like below to resolve the exception.

let req = NSFetchRequest<T.CurrentEntity>(entityName: T.name)
req.sortDescriptors = [NSSortDescriptor(key: "someKeyForSort", ascending: true)]

In addition, "someKeyForSort" is the name of a property of T.CurrentEntity. If ProductEntity is the type, "name" could be the key assuming you want NSFetchedResultsController to maintain the fetched results sorted by name in ascending order.

Turtleeeeee
  • 171
  • 7