TableView with NSFetchedResultsController is Flickering so much - swift

I got a tableView with FRC. I use NSMergeByPropertyObjectTrumpMergePolicy as mergePolicy for managedContext save.
The problem is when a record updated "didChange" delegate method is calling 3 times. It's calling for in that order insert, delete, update. So my tableView flickering so much.
How can i prevent that? Because every record of my application is being updated 4 times so tableView is flickering 12 times..
My delegate methods:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .insert:
tableView.insertSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .fade)
case .delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .none)
case .move:
break
case .update:
break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
print("cs \(type.rawValue)")
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath! as IndexPath], with: .none)
break
case .delete:
tableView.deleteRows(at: [indexPath!], with: .none)
break
case .move:
tableView.deleteRows(at: [indexPath!], with: .none)
tableView.insertRows(at: [newIndexPath!], with: .none)
//tableView.moveRow(at: indexPath!, to: newIndexPath!)
break
case .update:
tableView.reloadRows(at: [indexPath!], with: .none)
break
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}

Make sure that you are not cleaning your database. Flickering is possible because of this:
service.fetchDatasWith { (result) in
switch result {
case .success(let data):
DataManager.clearData() // Cleaning
DataManager.saveInCoreDataWith(array: data) // Saving again
print(data)
case .error(let message):
DispatchQueue.main.async {
AlertPresenter.presentAlertWith(title: "Error", message: message, controller: self)
}
}
}

Related

How to properly refactor repeated code of 2 ViewControllers?

I have 1 UIViewController with a type of CustomTableView and 1 UITableViewController with a type of usual UITableView. Both conform to NSFetchedResultsControllerDelegate and implemented its delegate methods with the repeated code. For now it's in extensions.
Is it possible to move that code out to a separate swift file? I tried to move it to separate file with class NSFetchedResultsView, but when I copy that delegate methods to the new file, it doesn't know anything about tableView inside it's methods...
How can I separate that methods properly?
Delegate methods I want to separate:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .none)
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .none)
}
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .none)
}
default:
tableView.reloadData()
}
}
You can make an object that you can assign to be the delegate
class CommonFetchResultDelegate: NSFetchedResultsControllerDelegate {
var tableView: UITableView
// make init that takes the table view
init(tableView: TableView) {
self.tableView = tableView
}
// !!!implement all the other delegate functions!!!
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .none)
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .none)
}
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .none)
}
default:
tableView.reloadData()
}
}
}
// then in the view controller
var myDelegate: CommonFetchResultDelegate?
override viewDidLoad() {
super.viewDidLoad()
self.myDelegate = CommonFetchResultDelegate(tableView: self.tableView)
self.myDelegate = fetchResultController.delegate
}

Update the tableView - without refresh completely

I have created the table "tableView" (data from CoreData - NSManagedObject). I want to update the data in the tables when the new notification comes from NSNotification without refreshing the table completely. How to do it ?
Thanks for help!
If you want to back tableview with autoupdating results from CoreData then its best to use NSFetchedResultsController. Apple has sample code on how to do this here.
The sort version is you implement the delegate methods for willChangeContent and didChangeContent and forward the relevant changes (ie insert, delete and reload row) to the tableView, and this will keep your tableview in synch with your database.
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
switch type {
case .insert:
tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
case .delete:
tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
case .move:
break
case .update:
break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .fade)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .fade)
case .update:
tableView.reloadRows(at: [indexPath!], with: .fade)
case .move:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}

Core data causing miss layout

I have a generic viewModel that handles core data delegations. But when I insert a new data to the context core data gives me a weird error:
[error] fault: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to delete row 1 from section 1 which only contains 1 rows before the update with userInfo (null)
CoreData: fault: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to delete row 1 from section 1 which only contains 1 rows before the update with userInfo (null)
I don't delete any of the rows. Actually I'm just inserting. Here is my viewModel's delegation
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
delegate?.endUpdates()
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
delegate?.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
if let newIndexPath = newIndexPath {
delegate?.insertItemAt(indexPath: newIndexPath)
}
if let indexPath = indexPath {
delegate?.insertItemAt(indexPath: indexPath)
}
case .delete:
if let indexPath = indexPath {
delegate?.deleteItemAt(indexPath: indexPath)
}
case .move:
if let newIndexPath = newIndexPath {
delegate?.insertItemAt(indexPath: newIndexPath)
}
if let indexPath = indexPath {
delegate?.deleteItemAt(indexPath: indexPath)
}
case .update:
if let indexPath = indexPath {
delegate?.updateItemAt(indexPath: indexPath)
}
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let indexSet: IndexSet = IndexSet(integer: sectionIndex)
switch type {
case .insert:
delegate?.insertSection(indexSet: indexSet)
case .delete:
delegate?.deleteSection(indexSet: indexSet)
default:
break
}
}
Here is the ViewController that responds to actions
extension ChatVC: CoreDataViewModelDelegate {
func insertSection(indexSet: IndexSet) {
tableView.insertSections(indexSet, with: .automatic)
}
func deleteSection(indexSet: IndexSet) {
tableView.deleteSections(indexSet, with: .automatic)
}
func beginUpdates() {
tableView.beginUpdates()
}
func endUpdates() {
tableView.endUpdates()
}
func insertItemAt(indexPath: IndexPath) {
tableView.insertRows(at: [indexPath], with: .fade)
tableView.scrollToBottom(animated: true)
viewModel.sendReadACKForMessageAt(indexPath: indexPath)
}
func deleteItemAt(indexPath: IndexPath) {
tableView.deleteRows(at: [indexPath], with: .fade)
}
func updateItemAt(indexPath: IndexPath) {
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
As far as I know, the core data should handle most of the table view actions. I mean when I pop the screen and push the same VC to the navigation everything works fine. I couldn't solve the problem.

UITableView Delete Row Animation misbehaving

I have a table view hooked up to a fetchedResultsController. When the table goes into editing mode I can get the delete to happen but the animation is strange. The red delete button instead of fading up with the rest of the row stays at full height and moves right. Also when swiping to delete the row, red bar stays at full row height for its animation.
The delete operation is surrounded by a beginTableUpdates : endTableUpdates block.
A video of this is at https://youtu.be/YOxex0sROwU
I'm using MVVM (or attempting to!)
I have some closures defined
var willChangeContent: (() -> ())?
var didChangeContent: (() -> ())?
var didInsertRow: ((_ indexPath: IndexPath) -> ())?
var didDeleteRow: ((_ indexPath: IndexPath) -> ())?
var didUpdateRow: ((_ IndexPath: IndexPath) -> ())?
var didMoveRow: (( _ IndexPath: IndexPath, _ newIndexPath: IndexPath) -> ())?
And the fetchedResultsController in an extension to the viewModel
extension TrainingEventsTableViewViewModel: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
willChangeContent?()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
didChangeContent?()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
didInsertRow?(newIndexPath!)
case .delete:
didDeleteRow?(indexPath!)
case .update:
didUpdateRow?(indexPath!)
case .move:
didMoveRow?(indexPath!, newIndexPath!)
}
}
}
and in the uiTableViewController the closures are:
private func bindToModel() {
viewModel?.willChangeContent = { [unowned self] () in
self.tableView.beginUpdates()
}
viewModel?.didChangeContent = { [unowned self] () in
self.tableView.endUpdates()
}
viewModel?.didInsertRow = { [unowned self] (indexPath) in
self.tableView.insertRows(at: [indexPath], with: .automatic)
}
viewModel?.didDeleteRow = { [unowned self] (indexPath) in
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
viewModel?.didUpdateRow = { [unowned self] (indexPath) in
self.tableView.reloadRows(at: [indexPath], with: .automatic)
}
viewModel?.didMoveRow = { [unowned self] (indexPath, newIndexPath) in
self.tableView.deleteRows(at: [indexPath], with: .automatic)
self.tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
The only other different thing is that the cell is coming from a xib file rather than a prototype cell on the storyboard.

Swift - after saving in core data tableview is not updating

I'm saving data trough this function
var parameter: Parameter
if parameterToEdit == nil {
parameter = Parameter(context:context)
} else {
parameter = parameterToEdit!
}
if let pH = phTextField.text {
parameter.paramPH = (pH as NSString).doubleValue
}
...........................................................
ad.saveContext()
self.presentingViewController?.dismiss(animated: true, completion: nil)
my problem is that after saving the table view is not updating.
in my tableviewcontroller i have implemented this functions:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch(type){
case.insert:
if let indexPath = newIndexPath{
tableView.insertRows(at: [indexPath], with: .fade)
}
break
case.delete:
if let indexPath = indexPath{
tableView.deleteRows(at: [indexPath], with: .fade)
}
break
case.update:
if let indexPath = indexPath{
let cell = tableView.cellForRow(at: indexPath) as! ParamCell
configureCell(cell: cell, indexPath: indexPath as NSIndexPath)
}
break
case.move:
if let indexPath = indexPath{
tableView.deleteRows(at: [indexPath], with: .fade)
}
if let indexPath = newIndexPath{
tableView.insertRows(at: [indexPath], with: .fade)
}
break
}
}
Not sure why is not updating.... any idea?