So I'm using the NSFetchedResultsController for an entity that is always increased by 10 or any number divisible by 10. The issue I'm having is that after the delegate methods are called for the NSFetchResultsController and the rows are inserted, the tableView will scroll to some random spot (usually towards the bottom of the table). I don't know if this is an issue with the heights of the cells (dynamic), or the fact that the cells contain images that need to be loaded, or if I'm handling the delegate methods wrong, or if I'm creating the objects wrong. Any help would be appreciated
Also, I'm using MoPub hence the mp_
NSFetchedResultsControllerDelegate Methods
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.mp_beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case NSFetchedResultsChangeType.insert:
guard let insertIndexPath = newIndexPath else { return }
self.tableView.mp_insertRows(atIndexPaths: [insertIndexPath], with: .fade)
case NSFetchedResultsChangeType.delete:
guard let deleteIndexPath = indexPath else { return }
self.tableView.mp_deleteRows(atIndexPaths: [deleteIndexPath], with: .fade)
default:
break
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.mp_endUpdates()
}
Inserting method
for index in 0..<posts.count {
let post = posts[index]
guard let _ = RecentPost.createInManagedObjectContext(context: context, post: post, sequence: index) else { continue }
}
The createInManagedObjectContext is just a method I use to insert the new object, and the post.count is always a dividend of 10
Since my cell heights are dynamic, I was setting the estimatedRowHeight to 300, changing it to 1000 seemed to fix my problem. If you know of any other way to fix it besides this, or any improvement to my code, let me know. Thanks
This is what lead me to my answer.
https://stackoverflow.com/a/38867302/4745553
EDIT
I actually was still having an issue and since I was responding to multiple changes, I was just using the controllerDidChangeContent delegate method and called tableView.reloadData() in their. Their is no animation but that's okay with what I'm doing.
This helped me fix it as well. https://stackoverflow.com/a/3013353/4745553
Related
I must be missing something simple but cannot identify what it is. BASIC and FORTRAN sure seemed a lot easier... just not as cool as this stuff is.
App is to display & sort flashcards stored in CoreData based on a predicate. After selecting options which modify the predicate, a UITableView is displayed listing the cards meeting the criteria in abbreviated form. Selecting a card row segues to a 2nd view controller with identical predicate setup in UITableView only more content shown & "Skill Level" can be modified and saved via CoreData on this 2nd view controller. Depending on the fetchRequest predicate, a card may no longer match the criteria and might be excluded from a subsequent fetchRequest. This all seems to function properly.
Selecting "Back" returns to the 1st UITableView controller from the 2nd, via an unwind segue. At this point, the fetchRequest is again executed along with a tableview.reloadData(). On return, flashcards sort in the proper order based on any changes. Flashcards that no longer meet the predicate criteria are properly excluded from the table.
The problem is this - the custom cell contents do not reflect any changes made (stars graphic doesn't change and numeric value of the Skill Level doesn't change when printed from the tableView). TableView sorts correctly and doesn't include items it shouldn't which seems to indicate the updated information was saved and has somehow been taken into consideration by the tableView. TableView just doesn't show it on the screen.
If I select "Back" again, returning to the "Home" or initial viewController & immediately segue to it again the tableView displays properly.
I've tried placing tableView.reloadData() in various locations(with and without .self), moving the fetch & reloads in and out of DispatchQueue.main.async. Tried replacing the control in the custom cell with images but that didn't work either. Can't seem to find anything quite like it on stack overflow or elsewhere on internet.
Any assistance would be greatly appreciated.
Custom Cell:
import UIKit
class CardsTableViewCell: UITableViewCell {
// MARK: Properties
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var difficultyImageView: UIImageView!
#IBOutlet weak var knowItControl: KnowItControl!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
UnwindFromSegue:
#IBAction func unwindToCardsTableViewController(sender: UIStoryboardSegue) {
DispatchQueue.main.async{
// RELOAD THE FLASHCARDS FOR TABLEVIEW
do {
try self.fetchedResultsController.performFetch()
} catch let error as NSError {
print("Fetching error: \(error), \(error.userInfo)")
}
print("\n UNWIND - About to reload table")
self.tableView.reloadData()
}
print("UNWIND SEGUE from DetailedCardsView")
// Considered giving up and just dumping back to Main Screen.
// dismiss(animated: true, completion: nil)
}
Assembling the tableView using extensions in the tableViewController
// MARK: - ConfigureCardsTableViewCell
extension CardsTableViewController {
func configure(cell: UITableViewCell, for indexPath: IndexPath) {
guard let cell = cell as? CardsTableViewCell else { return }
let flashCards = fetchedResultsController.object(at: indexPath)
cell.knowItControl.rating = Int(flashCards.cardKnowIt)
cell.questionLabel.text = flashCards.cardQuestion
// * * * ASSIGN THE IMAGE BASED ON THE levelDifficulty * * *
switch flashCards.levelDifficulty {
case "e"?:
cell.difficultyImageView.image = UIImage(named: "activeLetterE")
case "g"?:
cell.difficultyImageView.image = UIImage(named: "activeLetterG")
case "m"?:
cell.difficultyImageView.image = UIImage(named: "activeLetterM")
default:
cell.difficultyImageView.image = UIImage(named: "activeLetterE")
}
// ************************* END OF ASSIGN IMAGE ****************************************
print("assembling cell \(indexPath.row) *** knowIt - \(Int(flashCards.cardKnowIt))") // Observe what is going on, see if value was updated (it's not).
}
}
// * * * * T A B L E V I E W D A T A S O U R C E * * * *
// MARK: - UITableViewDataSource
extension CardsTableViewController: UITableViewDataSource {
// * * * C O N F I G U R E T H E C E L L V I A A C A L L * * *
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cardCellReuse", for: indexPath)
configure(cell: cell, for: indexPath)
print(" Configuring cell \(indexPath)") // Observe what is going on.
return cell
}
// general tableView functions for number of rows, sections, etc.
}
The viewController also contains the generic NSFetchedResultsControllerDelegate which I believe accurately matches all the examples I've come across.
Screen shots shown here.
1 - Initial load of 1st tableView from "Home" page properly displays content.
2 - Segued to 2nd tableView where changes were made and saved to CoreData.
3 - Returned to 1st tableView using unwind. Cells are sorted correctly but don't contain updated values.
4 - Left 1st tableView via Back and immediately loaded it again and the screen properly displays content. Just wish it did it without having to "go home & return".
NSFetched Results Controller Delegate:
// MARK: - NSFetchedResultsControllerDelegate
extension CardsTableViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
print("\n **** tableView.beganUpdates called")
}
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: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .update:
let cell = tableView.cellForRow(at: indexPath!) as! CardsTableViewCell
configure(cell: cell, for: indexPath!)
print("case .update was called.")
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
print("\n **** tableView.endUpdates called")
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let indexSet = IndexSet(integer: sectionIndex)
switch type {
case .insert:
tableView.insertSections(indexSet, with: .automatic)
case .delete:
tableView.deleteSections(indexSet, with: .automatic)
default: break
}
}
}
I am using just one viewController to add a task and then display it. I am using NSFetchedResultsController for fetching from the coreData. I am also using NSSortDescriptor to sort tasks according to priority.
Now here is the issue, I press a '+' button and a textview with keyboard appears. I write a task and press add. By doing that I am saving the object in the coreData. At the back there is a tableview that gets updated, however, the order is not correct. If for example the first task is say 'Task 1' and then second task say 'task 2', the 'task2' appears on the top of 'task 1', but when I relaunch the app, the task2 shows under the task1 which is what it should be.
When you add a lot of tasks with different priority levels, the priority sorting is respected but the order is all over the place within that priority. Relaunching the app fixes it.
I want the tableview to be updated correctly. What am I doing wrong? Here are some code snippets:
Inserting a new row when the data is added.
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .delete:
tableView.deleteRows(at: [indexPath!], with: .fade)
case .insert:
tableView.insertRows(at: [newIndexPath as! IndexPath], with: UITableViewRowAnimation.automatic)
default:
return
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
Notice 'Low 7' is in the blue priority area but not under 'Low 6'. It gets there when I relaunch the app
In the second pic, medium 6,7,8 all above. Relaunching the app will bring them under medium 5.
The premise: I have a UITableViewController that conforms to NSFetchedResultsControllerDelegate. I also have a fetched results controller and managed object context as variables in the controller. My tableView displays a table with one section of core data objects from the fetched results controller.
What I'm trying to implement is swipe to delete. The object selected for deletion is actually deleted, however the wrong indexPath is being animated to delete and I don't know why. I currently have the following methods that I believe are relevant:
// This method is being called in viewDidLoad, adding all of the CoreData objects to an array called fetchedResults.
func performFetch() {
do { try fetchedResultsController?.performFetch()
fetchedResults = fetchedResultsController?.fetchedObjects as! [Date]
} catch let error as NSError {
print(error.localizedDescription)
}
}
// tableViewDataSource methods
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let objectToDelete = fetchedResults[indexPath.row]
fetchedResultsController?.managedObjectContext.deleteObject(objectToDelete)
print("commitEditingStyle-indexPath = \(indexPath)")
do { try managedContext.save()
} catch let error as NSError {
print(error.localizedDescription)
}
}
}
// NSFetchedResultsControllerDelegate methods
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject object: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Delete:
if let indexPath = indexPath {
print("didChangeObject indexPath = \(indexPath)")
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
default:
return
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}
As you can see, I print the indexPath for the tableView:commitEditingStyle method as well as the controller:didChangeObject method. Here are the 2 print statements:
commitEditingStyle-indexPath = {length = 2, path = 0 - 3}
didChangeObject-indexPath = {length = 2, path = 0 - 0}
Why is the didChangeObject method picking up the wrong indexPath? When I swipe to delete the object, the object is deleted at the proper indexPath (in this case 3...) but the table view cell that animates deletion is indexPath 0 (the first cell in my table view). What gives?
Remove all usage of fetchedResults from your code. You are caching the initial set of objects that the FRC knows about, but you aren't tracking additions or removals in that cache. The cache is also a waste of memory because you can always get exactly what you want from the FRC and it also tracks changes.
So, what you're seeing should be what appears to be a random difference and is due to indexing differences between the cache array and the FRC. They should match initially, and if you only ever delete the last item it should be ok, but any other deletion would cause them to fall out of sync.
Background: In a tableView, each cell contains a button shown as an image. The effect I wanna show is that when I click the image(button actually), the button's image change to another one.
Bug: Say there are many data (over one page), and if I click one cell's button, the button's image will change BUT with a side effect that the whole tableView will first scroll up a little bit(like a bounce effect) then scroll down to the former place.
What I've done:
in ViewDidLoad method:inboxListTableView.estimatedRowHeight = 60
inboxListTableView.rowHeight = UITableViewAutomaticDimension
When I clicked the button (true and false represent different image and the definition in tableView(cellForRowAtIndex:) method):
let cell = sender.superview?.superview as InboxListCellTableViewCell
if let indexPath = inboxListTableView.indexPathForCell(cell) {
let actionToUpdate = actions[indexPath.row]
actionToUpdate.imageButton = !actionToUpdate.imageButton.boolValue
var e: NSError?
if !MOContext.save(&e) {
println(e?.description)
}
}
Then in Fetch Result Controller Delegate method:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
inboxListTableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath!, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath!) {
switch type {
case .Insert:
inboxListTableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
case .Delete:
inboxListTableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
case .Update:
inboxListTableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
default:
inboxListTableView.reloadData()
}
actions = controller.fetchedObjects as [Action]
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
inboxListTableView.endUpdates()
}
I think the bug may caused by two aspects: CoreData and Self sizing cells (dynamic cell height). Because I didn't hit the bug when I use array to hold actions ([Action]) instead of CoreData. However, when I comment inboxListTableView.rowHeight = UITableViewAutomaticDimension this code, it didn't "bounce back/ scroll up" but the height of the cell is fixed.
Any ideas? Anything can help! Thank you:)
I had the exact same problem: a little bounce when using dynamic cell heights. The difference in my case is that I didn't use NSFetchedResultsController and I also changed the heights of the cells (expand and collapse them).
After a lot of experimenting I've found a simple solution. Just call scrollToRowAtIndexPath after reloadRowsAtIndexPaths. It worked for me.
tableView.reloadRows(at: [indexPath], with: .none)
tableView.scrollToRow(at: indexPath, at: .none, animated: true)
I call this code when I expand and collapse the cells.
I hope you can use this in your project.
I think the pragmatic solution to your problem is to simply not use the NSFetchedResultsControllerDelegate method for this particular update.
You can change the image of the button directly in the button handler. You have to check for this situation in the .Update case of the fetched results controller delegate and make sure you don't call the reloadRowsAtIndexPaths method.
Now you are avoiding the automatic UI-update mechanism and the issue should not occur.
BTW, it is very bad practice to rely on the table view cell's view hierarchy for accessing the index path (superview.superview...). A few years ago, Apple changed the way cells work and code that relied on similar paradigms broke. The same with collection view cells.
Instead, do something like this:
func buttonHandler(sender: UIButton) {
let point = sender.convertPoint(CGPointZero, toView:self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(point)
}
I have a UITableViewController with static cells, and the next screen is a View to Add new entries with Core Data, this all works fine and inserts the values correctly into the database.
Inside each of the static cells, I have a label which I refer to with an #IBOutlet reference.
In Core Data, for instance, one of the attributes of my Entity model is booksNumber. Every time I add a new record to my database I want my numBooksLabel inside my static cell of the table view to update and show the total number of all books I have added. But I don't know where to perform this logic.
My code related to the table view looks like this:
func controller(controller: NSFetchedResultsController,
didChangeObject anObject: AnyObject,
atIndexPath indexPath: NSIndexPath,
forChangeType type: NSFetchedResultsChangeType,
newIndexPath: NSIndexPath) {
switch type {
case .Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
case .Update:
let cell = self.tableView.cellForRowAtIndexPath(indexPath)
// cell.textLabel?.text = "something"
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
case .Move:
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
self.tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
case .Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
default:
return
}
}
I have no tableView override functions as I believe they are not necessary for static cells UITableViews, correct me if I'm wrong.
Sorry if it's a trivial question but I'm brand new to Swift and to the whole xCode world and would really appreciate some help!
Thanks in advance.
I finnally solved it thanks to some gentle co-worker advice!
This is the code:
#IBOutlet var booksLabel: UILabel!
// Report logic inside event
override func viewWillAppear(animated: Bool) {
var books = 0
let entities = self.fetchedResultsController.fetchedObjects as [Entity]
for e in entities {
if e.books != nil {
books += e.books as Int
}
}
self.booksLabel.text = String(books)
}
I hope it may help someone else.