Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVVM with CoreData and NSFetchedResultsController

I am trying to start using MVVM with Swift. I've got a UITableViewController using NSFetchedResultsController, and a background process that adds/edits data in the CoreData db.

I don't know where to handle the fetchedResultsControllerDelegate methods. The viewModel or the viewController?

Because the UITableView gets auto-updated when data in CoreData is updated, it seems to me like making a wrapper view model object around each NSManagedObject instance, and binding that to the table view, is not the right way to do this. That way kind of defeats the purpose of using a NSFetchedResultsController.

Here's the simple MVC code I'm using, what's the best way to MVVM-ify this?

// Data model
@objc(Post)
public class Post: NSManagedObject {
  @NSManaged public var userName: String?
  @NSManaged public var createdAt: Date?

  @nonobjc public class func fetchRequest() -> NSFetchRequest<Post> {
    return NSFetchRequest<Post>(entityName: "Post")
  }
}
// View controller
class ViewController :  UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate {
  var fetchedResultsController: NSFetchedResultsController<Post>
  var tableView:UITableView = {
    return UITableView()
  }()

  override func viewDidLoad(){
    view.addSubview(tableView)
    tableView.delegate = self
    tableView.dataSource = self

    let fetchRequest:NSFetchRequest<Post> = Post.fetchRequest()
    fetchRequest.predicate = NSPredicate(format: "foo = %@ and bar = %@", argumentArray: ["baz", "loreum"])
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Post.createdAt), ascending: false)]

    self.fetchedResultController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                             managedObjectContext: context,
                                                             sectionNameKeyPath: #keyPath(Post.createdAt),
                                                             cacheName: nil)
    self.fetchedResultController.delegate = self

    do {
      try self.fetchedResultController.performFetch()
    } catch let error as NSError {
      fatalError("Error: \(error.localizedDescription)")
    }
  }

  // MARK - NSFetchedResultsControllerDelegate
  func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { /** do stuff **/ }
  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { /** do stuff **/ }
  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
    switch type {
    case .insert:
      tableView.insertSections(IndexSet(integer: sectionIndex), with: .left)
    case .delete:
      tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
    default:
      break;
    }
  }
  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { /** do stuff **/ }

  // MARK - UITableViewDataSource
  func numberOfSections(in tableView: UITableView) -> Int {
    return fetchedResultsController.sections?.count ?? 0
  }
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { /** do stuff **/ }
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "my-cell-identifier", for: indexPath) as! CustomUITableViewCell
    configureCell(cell, indexPath: indexPath)
    return cell
  }

  // MARK - Cell configuration
  func configureCell(_ cell:CustomUITableViewCell, indexPath:IndexPath){
    let o = fetchedResultsController.object(at: indexPath)
    cell.textLabel?.text = o.userName
    // configure cell more here
  }
}
like image 845
spnkr Avatar asked Sep 17 '25 13:09

spnkr


2 Answers

Apple itself sometimes uses a really nice Data Provider pattern that I quite like.

You can find an example here: https://developer.apple.com/documentation/coredata/synchronizing_a_local_store_to_the_cloud

This pattern factors out the FetchedResultsController into a Provider class, and you would pass the Viewcontroller (using weak var) as the FetchedResultsControllerDelegate to this Provider class.

I hope you'll find looking at this helpful.

like image 65
Ch Ryder Avatar answered Sep 20 '25 04:09

Ch Ryder


You can make it more MVVM, but why do you want to? If you're just looking for more explicit separation, ok, but keep in mind you will have to do a lot of currying. To answer your original question, you would create a new object (has to be object because it will be the FRC delegate) that is also your table view data source/delegate. This new view model would subscribe to FRC change related delegate methods, transform the managed objects into your cell view models, etc. and then signal to the view controller to reload data, etc. Or you could create slightly transformed delegate functions (or closures) for the view controller to subscribe to so it knows when to reload the table view and query your view model for count, object at index, etc.

In truth though, this is already the function of the FRC and to swim against that seems like unnecessary work and could be more error prone.

like image 30
Procrastin8 Avatar answered Sep 20 '25 04:09

Procrastin8