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
}
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With