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)
}
Related
So I'm having a problem with updating the height of a UITableViewCell when the text of its label gets changed.
I'm not good at explaining thing, therefore I've created a repo on Github so that you can just grab and run. The problem should show up when you scroll up and down the table view (to make the cell reused).
What I did was:
I created a table view with self-size cells.
Each of the cells has a UILabel in it.
I have a view model that will decide what's in the UILabel.
I use RxSwift to register the changes in the view model and update the text of the UITable accordingly.
At the same time, the cell will ask its delegate (in this case, the UITableViewController) to recalculate its height.
The delegate will then call tableView.beginUpdates and tableView.endUpdates to update the height of the cell.
Results:
When I scroll up the table view so that the first cell gets reused and then scroll down. The content of the first cell is duplicated across the table view (the second cell becomes a duplicate of the first one.)
What I tried:
I tried to call tableView.reloadData(). -> Infinite Loop
I tried to call tableView.reloadRows() -> This works but I don't know why.
What I want to achieve:
I want to keep Rx and MVVM together since I like the way Rx works.
I want to be able to change the label text of the cell and the height of the cell can be updated to fit the text.
Some code snippets: (Although it'd better to go check the repo out.):
Cell register view model
override func awakeFromNib() {
super.awakeFromNib()
viewModel.title
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] title in
guard let cell = self else { return }
cell.label.text = title
cell.delegate?.requestSizeRecalculate(cell: cell)
})
.disposed(by: disposeBag)
}
Delegate methods in UITableViewController
func requestSizeRecalculate(cell: SimpleCell) {
print("Size recalculating....")
method1(cell: cell)
//method2(cell: cell)
//method3(cell: cell)
}
private func method1(cell: SimpleCell) {
// Will not cause infinite loop but will have duplicates.
cell.setNeedsUpdateConstraints()
tableView.beginUpdates()
tableView.endUpdates()
}
private func method2(cell: SimpleCell) {
// Will cause infinite loop
tableView.reloadData()
}
private func method3(cell: SimpleCell) {
// This method works pretty well.
if let indexPath = tableView.indexPath(for: cell) {
tableView.reloadRows(at: [indexPath], with: .none)
}
}
It's my first time asking questions on Stackoverflow. I know I might have made things more complicated than it should be. But if anyone can offer any suggestions based on your experience, I would appreciate very much!
You can do:
tableView.beginUpdates()
tableView.reloadRows(at: [indexPathOfCell], with: .none)
tableView.endUpdates()
I have seen your code. Now you don't need to Calculate Height of row. when you give just top , leading , bottom and Trailing constrain. Xcode will calculate height of label and increase cell size According to label height. you need to pass just 1 method.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return UITableViewAutomaticDimension
}
you have done all the things correctly. you need to just write above method. Don't required this method now.
extension ViewController: SimpleCellDelegate {
func requestSizeRecalculate(cell: SimpleCell) {
print("Size recalculating....")
I have uploaded your project on this link with changes. Added 1 more large text in array to test self sizing.
Self Sizing Tableview
Here is Screenshot of Out:-
I hope this answer is helpful for you.
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
I am using xCode 7 beta and Swift to implement a tableview with MGSwipeTableCells. I am doing this because I need to have a swipe button on both the left and right of each cell. Both of these buttons needs to remove the cell from the tableview.
I tried doing this by using the convenience callback method when adding the buttons to the cells:
// Layout table view cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("newsFeedCell", forIndexPath: indexPath) as! NewsFeedCell
cell.layoutIfNeeded()
// Add a remove button to the cell
let removeButton = MGSwipeButton(title: "Remove", backgroundColor: color.removeButtonColor, callback: {
(sender: MGSwipeTableCell!) -> Bool in
// FIXME: UPDATE model
self.numberOfEvents--
self.tableView.deleteSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
return true
})
cell.leftButtons = [removeButton]
However, once I delete the first cell, all the indices are thrown off and the callback now deletes an incorrect cell. That is, if I delete cell_0, cell_1 now becomes the first 0th in the table. However, the callback for the buttons associated with cell_1 delete the cell with index 1 even though it is actually now the 0th cell in the table.
I tried to implement the MGSwipeTableCell delegate methods, but to no avail. None of these methods were ever called in the execution of my code. How should I fix this problem? Will implementing the delegate solve this issue? If, so would it be possible to provide an example? If not, can you please suggest an alternate way to have tableview cells with swipe buttons on both sides that can delete said cells?
You can also do something like this to get the correct indexPath:
let removeButton = MGSwipeButton(title: "Remove", backgroundColor: color.removeButtonColor, callback: {
(sender: MGSwipeTableCell!) -> Bool in
let indexPath = self.tableView.indexPathForCell(sender)
self.tableView.deleteSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
return true
})
Using the delegate methods will allow for cleaner button creation and table cell removal, because the buttons will only be created for the cell when it is swiped (saves memory) and you can capture a weak reference to the 'sender' (an MGTableViewCell, or custom type) in the handler, from which you can then get the index path. Follow their example on Github:
MGSwipeTableCell/demo/MailAppDemo/MailAppDemo/MailViewController.m
Then in your cellForRowAtIndexPath, be sure to set the cell's delegate to 'self.' It looks like you're missing this, and it should fix your problem with delegate methods.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let reuseIdentifier = "cell"
let cell = self.table.dequeueReusableCellWithIdentifier(reuseIdentifier) as! MGSwipeTableCell!
cell.delegate = self
// Configure the cell
return cell
}
Happy coding!
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:).
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.
`