Updating Cell Height after Fetching an Image from the Database - ios

I'm trying to update the height of my tableview cell after loading an image from the database asynchronously.
The use case is:
Fetch the image when the cell is visible. So i have to call `loadImageWith(callBack: () -> ()) in the cell it self.
When i receive the callBack, i would like to update the particular cell with the image.
For the image there is no placeholder etc. So if no image is available the cell is empty.
I have tried to call
self.tableView.beginUpdates()
self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.fade)
self.tableView.endUpdates()
in the callBack like described in other threads but obviously this leads to an infinite loop.
I also tried to call cell.needsLayout or/and cell.layoutIfNeeded after adding the image to the image view.
with no effect on the row.
I'm not using any third party library, because i need to fetch the image data from the local database not from a server.
If im loading all the images in viewDidLoad and call self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.fade)
then it works but the requirement is to load just the visible cells.
Hope any has a solution for this issue.

you can set the height of cell after image uploaded as
self.tableView.beginUpdates()
self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.fade)
let selectedCell = tableView.cellForRow(at: indexPath)
selectedCell.height = yourHeight
self.tableView.endUpdates()

Related

UITableView - Destructive UIContextualAction does not reload data

I'm trying to use the iOS 11 way to add swipe actions in a table view row. I want to add an action to delete a row.
My test table view displays numbers from 0 to 9, the data source is a simple array of integers, called numbers:
class ViewController: UIViewController {
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}
When I select a row, I print the associated value of numbers:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(numbers[indexPath.row])
tableView.deselectRow(at: indexPath, animated: true)
}
I implement the new delegate trailingSwipeActionsConfigurationForRowAt as below:
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let delete = UIContextualAction(style: .destructive, title: "Delete") { (_, _, completionHandler) in
self.numbers.remove(at: indexPath.row)
completionHandler(true)
}
return UISwipeActionsConfiguration(actions: [delete])
}
When I swipe on a row, the deletion works fine. But, and that's my problem, when I select another row after the deletion, the associated IndexPath is wrong...
Example:
I select the first row, the value printed is 0: OK.
I delete the first row.
I select the new first row, the value printed is 2, because the IndexPath passed in parameter is row 1, instead of row 0: not OK.
What do I do the wrong way?
It is important to remember that there's a model, the numbers array in your case, and a view, the cells that are displayed. When you remove the element from numbers you are only updating the model.
In most cases, as you have it coded, you would get an error shortly thereafter because the model is out of sync with the view.
However, my testing indicates that when the style is .destructive and you pass true to completionHandler Apple is sort of removing the row for you. Which is why you are seeing the row disappear. But there's something not quite right about it and I can't find any documentation on it.
In the meantime, just do this:
self.numbers.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completionHandler(true)
And always remember that if you change with the model you need to update the view.
It is very confusing that calling the completion handler with true updates the view by removing the cell, but does not update the table view's notion of which which remaining cells are at which index path.
The documentation around this is very scant, so I ran a test and found that calling the completion handler with true FIRST then calling deleteRows results in the "most correct" animation.
Batch updates with completion called first:
tableView.beginUpdates()
completion(succeeded)
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
Batch updates with completion called second:
tableView.beginUpdates()
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(succeeded)
tableView.endUpdates()
Completion called first:
completion(succeeded)
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
Completion called second
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(succeeded)
Image mid-animation:
The right panels both have the remaining cell partially covered by the cell that is being removed, while the bottom left panel introduces a white view from somewhere.

UITableView issue, it scrolls halfway up after updating or inserting a new cell

My TableView is doing some kind of auto-scrolling after any kind of updates are applied to it. If I were to append a new element to the array that contains the items which are presented in the UITableView, for some reason my Table View scrolls up somewhat randomly up around 50% up the middle of the contents of the TableView.
Here's the function that deals with updating, not inserting an item into the Table View:
func updateLastMessage() {
var section = msgSections.count - 1
var row = msgSections[section].msg.count - 1
let indexPath = NSIndexPath(forRow: row, inSection: section)
self.tableView.beginUpdates()
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
self.tableView.endUpdates()
}
This function above works perfectly, but after I call reloadRowsAtIndexPaths on self.tableView, the table view automatically scrolls up 50% of the contents of the Table View. I get exactly the same result if I were to insert an item instead:
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
self.tableView.endUpdates()
Also, if I were to comment out both beginUpdates() and endUpdates(), there seems to be no effect to the scrolling.
I only know of one hack to somewhat get around this issue by using setContentOffset to return the scroll position back to where it was before a message was updated, or a new message is inserted:
func updateLastMessage() {
let currentContentOffset = self.tableView.contentOffset
var section = msgSections.count - 1
var row = msgSections[section].msg.count - 1
let indexPath = NSIndexPath(forRow: row, inSection: section)
self.tableView.beginUpdates()
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
self.tableView.endUpdates()
self.tableView.setContentOffset(currentContentOffset, animated: false)
}
This hack doesn't work in my favor, because it scrolls slowly to the top and then jolts back to where it was which looks kind of bad.
Does anyone know exactly what could be the mechanism that's causing the TableView to scroll up so much after any appended item or updated item??
It seems to work perfectly and the animation looks very nice as but only when there aren't any items in the table view.
Never call reloadRowsAtIndexPaths: withRowAnimation: inside beginUpdates() and endUpdates().
The tableView will automatically call the necessary reloading of rows at the end of endUpdates().
Just call beginUpdates() and endUpdates() without any code inside. It'll work.

Random scrolling while reloading uitableviewcell, iOs/Swift

I am developing news feed and I am using uitableview to display data. I am loading each cell data synchronically in other thread and use protocol method to display loaded data:
func nodeLoaded(node: NSMutableDictionary) {
for var i = 0; i < nodesArray.count; ++i {
if ((nodesArray[i]["id"] as! Int) == (node["id"] as! Int)) {
nodesArray[i] = node
CATransaction.setDisableActions(true)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
CATransaction.setDisableActions(false)
}
}
}
The problem is that when I scrolling down (while tableview updating), tableview push me on the top. I was searching answer on this problem, but nothing helps me. I already tried to disable animation:
CATransaction.setDisableActions(true)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
CATransaction.setDisableActions(false)
2)
UIView.setAnimationsEnabled(false)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
UIView.setAnimationsEnabled(true)
But the story always the same.
I have the desired effect when I simply don't use self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
, but in this case data reload only if I scroll down and then return back to cell.
Maybe the problem is that I use auto layout and tableview recalculate its height every time? But I don't know how to fix it
You can build the reload mechanism yourself.
Iterate over all visible cells (tableView.visibleCells) and apply the same logic to them as you would in -tableView:cellForRowAtIndexPath:.
If you need the index path to perform this update, you can ask the table view for it (-indexPathForCell:).

Add completion for deleteRowAtIndexPaths with an extension or didEndDisplayingCell

I'd like to do something after deleteRowsAtIndexPaths has finished it's animation.
I can achieve this by wrapping it in animateWithDuration but it doesn't feel like the right way of doing it.
Another way is using didEndDisplayingCell but I can't update the section here or else it will get in an infinite loop.
What I'm trying to do is:
Delete a cell by swiping it
Remove it from my data model
Delete the row with deleteRowsAtIndexPaths
After the row has been deleted and animation has ended:
Reload sections by calling reloadSections
The code I'm using:
func deleteObject(ojbectName: String) {
let indexPath = // create indexPath
// Delete the item in data model and table
myData.removeAtIndex(index)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Left)
// Reload section after row animation has ended
let indexSet = NSIndexSet(indexesInRange: NSMakeRange(myData[indexPath.section].startIndex, myData[indexPath.section].endIndex))
tableView.reloadSections(indexSet, withRowAnimation: .None)
}
I tried creating an extension for UITableView that didn't go well. Is there someone who can tell me how to create one or how to use didEndDisplayingCell so I can reload the section after the animation has ended?
Try implementing didTransitionToState in your subclassed UITableViewCell. Check out the Apple docs on this.
`

Dynamic cell height causing bounce effect when calling reloadRowsAtIndexPaths method

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)
}

Resources