swiftcore-datauikitnsfetchedresultscontrollernsfetchrequest

How to switch between different CoreData Entities in NSFetchedResultsController?


I am using a CoreData in my project. It has 2 separate Entities: Currency and Currency2. The entities have identical attributes, but store a different data which populates a tableView. And user can choose which data he wants to see in the tableView by picking the option in app settings (I can store his choice by saving it in UserDefaults).

The problem here is how can I populate the tableView with a different Entity data? I can't just change a name like in string literal in here from Currency to Currency2:

private var fetchedResultsController: NSFetchedResultsController<Currency>!

It will just give me an error. So I assume I should create one more fetchedResultsController...:

private var fetchedResultsController: NSFetchedResultsController<Currency2>!

But then I should double all the below code since I need to switch between them. And what If in future I will need to switch between 3 or 4 different Entities?

How can I make the code reusable and at the same time receive a desired switch result with NSFetchedResultsController?

For now my NSFetchedResultsController set up as follows:

class CurrencyViewController: UIViewController {

  private var fetchedResultsController: NSFetchedResultsController<Currency>!

   override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    setupFetchedResultsController()
   }

    //MARK: - TableView DataSource Methods

     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return fetchedResultsController.sections![section].numberOfObjects
     }


     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
           let cell = tableView.dequeueReusableCell(withIdentifier: "currencyCell", for: indexPath) as! CurrencyTableViewCell
           let currency = fetchedResultsController.object(at: indexPath)
    
           cell.shortName.text = currency.shortName
           cell.fullName.text = currency.fullName

           return cell
     }

   //MARK: - NSFetchedResultsController Setup & Delegates

      func setupFetchedResultsController(with searchPredicate: NSPredicate? = nil) {
           let predicate = NSPredicate(format: "isForCurrencyScreen == YES")
        
           var sortDescriptor: NSSortDescriptor {
            if pickedSection == "По имени" {
                return NSSortDescriptor(key: "fullName", ascending: sortingOrder)
            } else {
                return NSSortDescriptor(key: "shortName", ascending: sortingOrder)
            }

          fetchedResultsController = coreDataManager.createCurrencyFetchedResultsController(with: predicate, and: sortDescriptor)
          fetchedResultsController.delegate = self
          try? fetchedResultsController.performFetch()
          tableView.reloadData()
     }

      func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
          tableView.beginUpdates()
      }

      func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
          tableView.endUpdates()
      }
}

And this is also my coreDataManager.createCurrencyFetchedResultsController method:

  private let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

  func createCurrencyFetchedResultsController(with predicate: NSPredicate? = nil, and sortDescriptor: NSSortDescriptor? = nil) -> NSFetchedResultsController<Currency> {
        let request: NSFetchRequest<Currency> = Currency.fetchRequest()
        let baseSortDescriptor = NSSortDescriptor(key: "shortName", ascending: true)
        request.predicate = predicate
        
        return NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
    }

Solution

  • If your entities always have the same set of attributes and aren't fundamentally different, consider making them just one entity and differentiate them by adding an extra attribute. That will make the system infinitely extensible without writing much of extra code, and the code will be much simpler.

    If you really need them be separate entities, I'd suggest the following:

    1. Create a new base entity that will act as a parent class for both entities. Let's call it BaseCurrency.
    2. Specify it as a parent entity for both Currency and Currency2.
    3. Create shared attributes in BaseCurrency and remove them from the child entities. The result will look like this:

    Resulting Entity-Relationship Diagram

    1. Create a generic factory method that returns an NSFetchedResultsController for any entity:
    enum SortingField {
        case name
        case symbol
    }
    
    private func makeFetchedResultsController<T: BaseCurrency>(
        context: NSManagedObjectContext,
        sortBy: SortingField
    ) -> NSFetchedResultsController<T> {
        let request: NSFetchRequest<T> = NSFetchRequest(entityName: T.description())
    
        let sortDescriptor: NSSortDescriptor = {
            switch sortBy {
            case .name:
                return NSSortDescriptor(keyPath: \T.name, ascending: true)
            case .symbol:
                return NSSortDescriptor(keyPath: \T.symbol, ascending: true)
            }
        }()
        request.sortDescriptors = [sortDescriptor]
    
        return NSFetchedResultsController(
            fetchRequest: request,
            managedObjectContext: context,
            sectionNameKeyPath: nil,
            cacheName: String(describing: T.self)
        )
    }
    

    Note that this method doesn't accept any sort descriptors or predicates but rather an enum. It allows to abstract from concrete field names and be able to use key paths of a generic types inside the method. If you need more advanced sorting/filtering capabilities, it's possible to build upon this idea by introducing more sophisticated data structures that describe filters and sorting.

    1. When you need to create a controller for a specific entity, call the factory method:
    let frc: NSFetchedResultsController<Currency2> =
        makeFetchedResultsController(
            context: context,
            sortBy: .symbol
        )
    

    You have to explicitly specify the type of the controller here of course.

    This is just a basic limited example but hopefully you can use it as a foundation and build on top of it.