I'm trying to insert a row after another row deletion animation is completed. I've been trying doing the following:
tableView.beginUpdates()
CATransaction.begin()
CATransaction.setCompletionBlock {
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
}
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Left)
CATransaction.commit()
tableView.endUpdates
This gave me the usual assertion failure when the count of rows is not the same as it's been expecting.
Then I've tried using an UIView animation with a completion block:
tableView.beginUpdates()
func animations() {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Left)
}
func completion() {
if count == self.payments.count && self.payments.isEmpty { insert() }
}
UIView.animateWithDuration(0.5, animations: { animations() }) { _ in completion() }
tableView.endUpdates()
Both attempts is giving me the same error. Is it possible or should I look into custom animation for inserting / deleting tableview rows?
Edit:
I managed to make it work by moving tableView.endUpdates() to the completion block. But the insertion animation still animates at the same time when the row is being deleted.
Is there another way of doing this?
if you know how much time does your animation takes to complete just add this function to wait for a given amount of time before executing some code:
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
Usage:
delay(seconds: 0.5) {
//code to be delayed "0.5 sec"
}
I think you're putting code in wrong position
It should be like this:
CATransaction.begin()
CATransaction.setCompletionBlock {
// animation has finished
}
tableView.beginUpdates()
// do some work
tableView endUpdates()
CATransaction.commit()
Reference: How to detect that animation has ended on UITableView beginUpdates/endUpdates?
Related
Here is my code, I just want to add animation for row at storyIndexRow in my tableView
view.tapHandle {
if self.storyIndexRow < self.storyContents.count {
//Add content
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
self.messageArr.append(self.storyContents[self.storyIndexRow])
self.storyIndexRow += 1
})
}
}
You can do this in a performBatchUpdates
Not sure about the rest of your code and how that interacts, or how to reference the tableView within your tap function. If you are editing the data source and then reloading the tableView you can use something like:
view.tapHandle {
if self.storyIndexRow < self.storyContents.count {
self.messageArr.append(self.storyContents[self.storyIndexRow])
self.storyIndexRow += 1
tableView.performBatchUpdates({
tableView.reloadData()
}, completion: { (didComplete) in
// Do anything else after animation completion here
})
}
}
I want to scroll to a given index (self.boldRowPath), but when I debug scrollToRow is performed before reloadData().
How to know reloadData has finished ?
func getAllTimeEvent() {
self.arrAllTimeEvent = ModelManager.getInstance().getAllTimeEvent(from: self.apportmentDateFrom, to: self.apportmentDateTo)
self.tblTimeEvent.reloadData()
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
}
Any help will be much appreciated.
Try doing this, it should work:
self.tblTimeEvent.reloadData()
DispatchQueue.main.async(execute: {
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
})
This will execute the scrollToRow on the main thread, that means after the reloadData is done (because it is on the main thread)
As explained in this answer, the reload of the UITableView happens on the next layout run (usually, when you return control to the run loop).
So, you can schedule your code after the next layout by using the main dispatch queue. In your case:
func getAllTimeEvent() {
self.arrAllTimeEvent = ModelManager.getInstance().getAllTimeEvent(from: self.apportmentDateFrom, to: self.apportmentDateTo)
self.tblTimeEvent.reloadData()
DispatchQueue.main.async {
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
}
}
You can also force the layout by manually calling layoutIfNeeded. But this is generally not a good idea (the previous option is the best):
func getAllTimeEvent() {
self.arrAllTimeEvent = ModelManager.getInstance().getAllTimeEvent(from: self.apportmentDateFrom, to: self.apportmentDateTo)
self.tblTimeEvent.reloadData()
self.tblTimeEvent.layoutIfNeeded()
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
}
you can make sure reload is done using...
In Swift 3.0 + we can create a an extension for UITableView with a escaped Closure like below :
extension UITableView {
func reloadData(completion: #escaping () -> ()) {
UIView.animate(withDuration: 0, animations: { self.reloadData()})
{_ in completion() }
}
}
And Use it like Below where ever you want :
Your_Table_View.reloadData {
print("reload done")
}
hope this will help to someone. cheers!
You can make a pretty solid assumption that a UITableView is done reloading when the last time tableView(_:willDisplay:forRowAt:) is called for the visible sections on the screen.
So try something like this (for a tableView where the rows in section 0 take up the available space on the screen):
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastRowIndex = tableView.numberOfRows(inSection: 0)
if indexPath.row == lastRowIndex - 1 {
// tableView done reloading
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
}
}
You can use CATransaction for that
CATransaction.begin()
CATransaction.setCompletionBlock {
// Completion
}
tableView.reloadData()
CATransaction.commit()
tableView.reloadData { [weak self] in
self?.doSomething()
}
I have a table view which received data from a real-time database. These data are added from the bottom of the table view and so on this table view has to scroll down itself to show new data.
I've found a method to do it however I'm not satisfied because the scroll always start from the top of the list. Not very beautiful.
Here is the code of this method :
func tableViewScrollToBottom(animated: Bool) {
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
let numberOfSections = self.clientTable.numberOfSections
let numberOfRows = self.clientTable.numberOfRowsInSection(numberOfSections-1)
if numberOfRows > 0 {
let indexPath = NSIndexPath(forRow: numberOfRows-1, inSection: (numberOfSections-1))
self.clientTable.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Bottom, animated: animated)
}
})
}`
Is there a way to modify this method in order to scroll only from the previous position ?
The issue is probably how the rows were inserted into the table. For example, if you add rows to the end using something like this, you get a very smooth UI:
#IBAction func didTapAddButton(sender: AnyObject) {
let count = objects.count
var indexPaths = [NSIndexPath]()
// add two rows to my model that `UITableViewDataSource` methods reference;
// also build array of new `NSIndexPath` references
for row in count ..< count + 2 {
objects.append("New row \(row)")
indexPaths.append(NSIndexPath(forRow: row, inSection: 0))
}
// now insert and scroll
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .None)
tableView.scrollToRowAtIndexPath(indexPaths.last!, atScrollPosition: .Bottom, animated: true)
}
Note, I don't reload the table, but rather call insertRowsAtIndexPaths. And I turned off the animation because I know they're off screen, and I'll then scroll to that row.
I'd like to visually scroll through my whole tableView. I tried the following, but it doesn't seem to perform the scrolling. Instead it just runs through the loops. I inserted a dispatch_async(dispatch_get_main_queue()) statement, thinking that that would ensure the view is refreshed before proceeding, but no luck.
What am I doing wrong?
func scrollThroughTable() {
for sectionNum in 0..<tableView.numberOfSections() {
for rowNum in 0..<tableView.numberOfRowsInSection(sectionNum) {
let indexPath = NSIndexPath(forRow: rowNum, inSection: sectionNum)
var cellTemp = self.tableView.cellForRowAtIndexPath(indexPath)
if cellTemp == nil {
dispatch_async(dispatch_get_main_queue()) {
self.tableView.scrollToRowAtIndexPath(indexPath!, atScrollPosition: .Top, animated: true)
self.tableView.reloadData()
}
}
}
}
}
I found a solution. Simply use scrollToRowAtIndexPath() with animation. To do so I had to create a getIndexPath() function to figure out where I want to scroll. Has more or less the same effect as scrolling through the whole table if I pass it the last element of my tableView.
If you want it to happen slower with more scrolling effect, wrap it inside UIView.animateWithDuration() and play with 'duration'. You can even do more animation if you want in its completion block. (No need to set an unreliable sleep timer, etc.)
func animateReminderInserted(toDoItem: ReminderWrapper) {
if let definiteIndexPath = indexPathDelegate.getIndexPath(toDoItem) {
self.tableView.scrollToRowAtIndexPath(definiteIndexPath, atScrollPosition: .Middle, animated: true)
}
}
Is there a way to either specify the duration for UITableView row animations, or to get a callback when the animation completes?
What I would like to do is flash the scroll indicators after the animation completes. Doing the flash before then doesn't do anything. So far the workaround I have is to delay half a second (that seems to be the default animation duration), i.e.:
[self.tableView insertRowsAtIndexPaths:newRows
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView performSelector:#selector(flashScrollIndicators)
withObject:nil
afterDelay:0.5];
Just came across this. Here's how to do it:
Objective-C
[CATransaction begin];
[tableView beginUpdates];
[CATransaction setCompletionBlock: ^{
// Code to be executed upon completion
}];
[tableView insertRowsAtIndexPaths: indexPaths
withRowAnimation: UITableViewRowAnimationAutomatic];
[tableView endUpdates];
[CATransaction commit];
Swift
CATransaction.begin()
tableView.beginUpdates()
CATransaction.setCompletionBlock {
// Code to be executed upon completion
}
tableView.insertRowsAtIndexPaths(indexArray, withRowAnimation: .Top)
tableView.endUpdates()
CATransaction.commit()
Expanding on karwag's fine answer, note that on iOS 7, surrounding the CATransaction with a UIView Animation offers control of the table animation duration.
[UIView beginAnimations:#"myAnimationId" context:nil];
[UIView setAnimationDuration:10.0]; // Set duration here
[CATransaction begin];
[CATransaction setCompletionBlock:^{
NSLog(#"Complete!");
}];
[myTable beginUpdates];
// my table changes
[myTable endUpdates];
[CATransaction commit];
[UIView commitAnimations];
The UIView animation's duration has no effect on iOS 6. Perhaps iOS 7 table animations are implemented differently, at the UIView level.
That's one hell of a useful trick!
I wrote a UITableView extension to avoid writing CATransaction stuff all the time.
import UIKit
extension UITableView {
/// Perform a series of method calls that insert, delete, or select rows and sections of the table view.
/// This is equivalent to a beginUpdates() / endUpdates() sequence,
/// with a completion closure when the animation is finished.
/// Parameter update: the update operation to perform on the tableView.
/// Parameter completion: the completion closure to be executed when the animation is completed.
func performUpdate(_ update: ()->Void, completion: (()->Void)?) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
// Table View update on row / section
beginUpdates()
update()
endUpdates()
CATransaction.commit()
}
}
This is used like so:
// Insert in the tableView the section we just added in sections
self.tableView.performUpdate({
self.tableView.insertSections([newSectionIndex], with: UITableViewRowAnimation.top)
}, completion: {
// Scroll to next section
let nextSectionIndexPath = IndexPath(row: 0, section: newSectionIndex)
self.tableView.scrollToRow(at: nextSectionIndexPath, at: .top, animated: true)
})
Shortening Brent's fine answer, for at least iOS 7 you can wrap this all tersely in a [UIView animateWithDuration:delay:options:animations:completion:] call:
[UIView animateWithDuration:10 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
[self.tableView beginUpdates];
[self.tableView endUpdates];
} completion:^(BOOL finished) {
// completion code
}];
though, I can't seem to override the default animation curve from anything other than EaseInOut.
Here's a Swift version of karwag's answer
CATransaction.begin()
tableView.beginUpdates()
CATransaction.setCompletionBlock { () -> Void in
// your code here
}
tableView.insertRowsAtIndexPaths(indexArray, withRowAnimation: .Top)
tableView.endUpdates()
CATransaction.commit()
For me I needed this for a collectionView. I've made a simple extension to solve this:
extension UICollectionView {
func reloadSections(sections: NSIndexSet, completion: () -> Void){
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.reloadSections(sections)
CATransaction.commit()
}
}
Nowadays if you want to do this there is new function starting from iOS 11:
- (void)performBatchUpdates:(void (^)(void))updates
completion:(void (^)(BOOL finished))completion;
In updates closures you place the same code as in beginUpdates()/endUpdates section. And the completion is executed after all animations.
As tableView's performBatch method is available starting from iOS 11 only, you can use following extension:
extension UITableView {
func performUpdates(_ updates: #escaping () -> Void, completion: #escaping (Bool) -> Void) {
if #available(iOS 11.0, *) {
self.performBatchUpdates({
updates()
}, completion: completion)
} else {
CATransaction.begin()
beginUpdates()
CATransaction.setCompletionBlock {
completion(true)
}
updates()
endUpdates()
CATransaction.commit()
}
}
}
Antoine's answer is pretty good – but is for UICollectionView. Here it is for UITableView:
extension UITableView {
func reloadSections(_ sections: IndexSet, with rowAnimation: RowAnimation, completion: (() -> Void)?) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.reloadSections(sections, with: rowAnimation)
CATransaction.commit()
}
}
Called like so:
tableView.reloadSections(IndexSet(0), with: .none, completion: {
// Do the end of animation thing
})
If someone is facing the problem when tableView is ignoring animation parameters from UIView.animate and using "from up to down" default animation for reloading rows, I've found a strange solution:
You need to:
Silence tableView animation
Use transitionAnimation instead
Example:
let indicesToUpdate = [IndexPath(row: 1, section: 0)]
UIView.transition(with: self.tableView,
duration: 0.5,
options: [.transitionCrossDissolve,
.allowUserInteraction,
.beginFromCurrentState],
animations: {
UIView.performWithoutAnimation {
self.tableView.reloadRows(at: indicesToUpdate,
with: .none)
}
})
PS: UIView.transition(..) also has optional completion :)
You could try to wrap the insertRowsAtIndexPath in a
- (void)beginUpdates
- (void)endUpdates
transaction, then do the flash afterwards.