Execute code after animation triggered by NSFetchedResultsController has finished - swift

I want to execute a scroll animation after NSFetchedResultsController has finished animating moving a tableview row between sections.
The update of the section happens when I dismiss a view controller. This triggers the animation of the tableview which works fine. after this animation I want the tableview to scroll to the new location. However this does not work the scrolling starts before the animation has finished.
self.dismissViewControllerAnimated(true, completion: {
shoppingItem.category = self.categoryTextField.text // this line triggers the move between sections
dispatch_async(dispatch_get_main_queue(), {
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
do {
try self.coreDataStack.context.save()
} catch {
fatalCoreDataError(error)
}
})
})
I realize that the solution lies in managing the threads but I cannot figure out how to set this up correctly.
SOLUTION:
By delaying the scrolling by one second the animation works fine:
var time = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), 1 * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue(), {
self.indexPathOfChangedItem = self.fetchedResultsController.indexPathForObject(shoppingItem)
self.itemsTableView.scrollToRowAtIndexPath(self.indexPathOfChangedItem, atScrollPosition: .Bottom, animated: true)
})
Note that the definition of the new indexpath, called indexPathOfChangedItem also has to be in the delayed call as this index path is not up to date until the animation triggered by the NSFetchedResultsController is completed.

The FRC isn't doing the animation, it's just observing the change and telling you, your code then tells the table view and it does the animation.
The animation takes no parameters to tell you when it's done or to specify how long it should take :-( raise a radar with Apple.
The simple option is to guess how long the animation runs for and dispatch after a delay to run your scroll after the animation. It's probably trial and error to find the right amount of time to wait, and once found it's unlikely to change / be different for other devices.

Related

CollectionView: Unable to animate ReloadItems

ReloadItems does not seem to allow for animation.
I tried using collectionView:( willDisplay) but the reloadItems() actually overtakes the animation (i.e. cutting it off and reloading the cell)
I tried performBatchUpdates() but I get outOfRange error in this block:
collectionView.performBatchUpdates({
// reload one or more items
collectionView.reloadItems(at: [tappedArray[0], tappedArray[1]])
}) { (_) in
// do animations here
if let cell1 = collectionView.cellForItem(at: self.tappedArray[1]) {
self.animateCell(cell1)
}
}
If a single item in a collectionView is reloaded, how can I add animation(s) to the re-display of that item?
Issue resolution: Animation done in a closure is NOT on the main thread. Since all animation should be on the main thread, this was causing my UI to glitch, or "not allow for animation".

scrollViewDidScroll called with strange contentOffsetY

I have a UIViewController that holds a NSFetchedResultsController. After the insertion of rows to the top, I want to keep the rows visible as they where before the insertions. This means I need to make some calculations to keep the contentOffSetY right after the update. The calculation is correct, but I noticed that scrollViewDidScroll gets called after it scrolled to my specified contentOffsetY, this results in a corrupted state. This is the logging:
Will apply an corrected Y value of: 207.27359771728516
Scrolled to: 207.5
Corrected to: 207.27359771728516
Scrolled to: 79.5 <-- Why is this logline here?
You can directly clone an example project: https://github.com/Jasperav/FetchResultControllerGlitch (commit https://github.com/Jasperav/FetchResultControllerGlitch/commit/d46054040139afeeb648e1e0b5b113bd98685b4a, the newest version of the project only glitches, the weird call to the scrollViewDidScroll method is now gone. If you fix the glitch I award the bounty. Just clone the newest version, run it and scroll a little bit. You will see strange content offset's (glitches)). Run the project and you will see the strange output. This is the controllerDidChangeContent method:
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
let currentSize = tableView.contentSize.height
UIView.performWithoutAnimation {
tableView.endUpdates()
let newSize = tableView.contentSize.height
let correctedY = tableView.contentOffset.y + newSize - currentSize
print("Will apply an corrected Y value of: \(correctedY)")
tableView.setContentOffset(CGPoint(x: 0,
y: correctedY),
animated: false)
print("Corrected to: \(correctedY)")
}
}
If I call tableView.layoutIfNeeded right after the tableView.endUpdates(), the delegate is already called. What does it cause to call the delegate method? Is there any way it does not scroll?
I downloaded your code and made some tweaks to fix the glitching issue. Here is what I have done.
set estimatedRowHeight property of table view to some number
init() {
super.init(frame: .zero, style: .plain)
estimatedRowHeight = 50.0
register(MyTableViewCell.self, forCellReuseIdentifier: "cell")
}
created a function to handle the UITableView reload action without modifying the current contentOffset
func reloadDataWithoutScroll() {
let lastScrollOffset = contentOffset
beginUpdates()
reloadData()
layoutIfNeeded()
endUpdates()
layer.removeAllAnimations()
setContentOffset(lastScrollOffset, animated: false)
}
Updated controllerDidChangeContent function to make use of the reloadDataWithoutScroll function
UIView.performWithoutAnimation {
tableView.performBatchUpdates({
tableView.insertRows(at: inserts, with: .automatic)
})
inserts.removeAll()
tableView.reloadDataWithoutScroll()
}
When I execute with these changes, it doesn't scroll when a new row is added. However, it does scroll to show the new added row when the current contentOffset is 0. And I don't think that would be a problem, logically speaking.

Strange animation when changing height constraint on a CollectionView (autolayout)

I have a collectionView which I need to animate height changes for, so it expands up from the foot of the screen, & back down again. It's hard to explain what's going wrong so I've attached a recording of the simulator. The animation is fine for expanding, but very odd for the collapse - the content partially disappears as it animates, then reappears when it's done. The animation code is triggered during the scrollView 'scrollViewDidScroll(_ scrollView: UIScrollView)' delegate method, with logic to determine whether to collapse or expand. The expand animation, whose code is pretty much identical, works fine.
Animation code -
private func collapseTable() {
guard self.isTableChangingSize == false else { return } // as the delegate method is triggered during the animation, we use a flag to prevent multiple collapse calls
guard self.tableHeightConstraint.constant != self.tableCollapsedHeight else { return } // don't collapse if we're already collapsed
self.isTableChangingSize = true
self.tableHeightConstraint.constant = self.tableCollapsedHeight
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
}) { _ in
self.isTableChangingSize = false
}
}
Here are a couple of videos of what's happening -
Normal speed
Slow motion
After far too long messing about trying different approaches, & tweaking everything, it seems that the problem was allowing scrolling during the animation block. If you set -
self.collectionView.isScrollEnabled = false
before the animation block, and -
self.collectionView.isScrollEnabled = true
in the completion, all is good.

UIView animate with completion block not working

I have the following small function wich contains two primary actions which I would like to occur. A three second animation, and then upon completion of that animation, another function call. What is happening, however, is that the line of code that calls another function is being executed before the UIView animation has completed.
func switchToNormalState() {
print("Switching to Normal State")
currentState = "normal"
intervalCounter = intervalCounter + 1
setVisualsForState()
UIView.animateWithDuration(Double(normalDuration), animations: { () -> Void in
self.ProgressBar.setProgress(1.0, animated: true)
}, completion: { _ in
self.switchToFastState()
})
}
I have studied other similar posts looking for the solution but none of the solutions have solved my puzzle. What am I doing wrong in the completion block in order to make these actions a sequence and not simultaneous?
Assuming that ProgressBar is a UIProgressView, the setProgress method animates itself and doesn't need to be contained in a UIView.animateWithDuration call.
This means the setProgress animation is probably longer than the duration you're putting in the UIView.animateWithDuration call, so self.switchToFastState() is being called when the UIView animation ends- before the setProgress animation completes.
Solutions
Change the duration. You can hack around this by finding/guessing the duration of the setProgress animation and using that as your UIView animation duration. This should mimic the behavior you are going for.
Change the duration but use GCD. This is the same idea as above, but doesn't use the UIView animation method. You can see this question for how to use GCD.
NSTimer or Subclassing. There are solutions here that use NSTimer or subclass UIProgressView.
I am not sure if this is the best solution, but this is what I ended up going with:
ProgressBar.progress = 1.0
UIView.animateWithDuration(Double(normalDuration), animations: { () -> Void in
self.ProgressBar.layoutIfNeeded()
}, completion: { (_) -> Void in
self.switchToFastState()
})
This solution removes the setProgress (which may be animated) from the animation block and this allows the UIView animation to execute without interuption.

Why layoutIfNeeded() allows to perform animation when updating constraints?

I have two states of UIView:
I have animation between them using #IBaction for button:
#IBAction func tapped(sender: UIButton) {
flag = !flag
UIView.animateWithDuration(1.0) {
if self.flag {
NSLayoutConstraint.activateConstraints([self.myConstraint])
} else {
NSLayoutConstraint.deactivateConstraints([self.myConstraint])
}
self.view.layoutIfNeeded() //additional line
}
}
Animation is working only when I add additional line:
But when I remove that line, then my UIView is updated but without any animation, it happens immediately.
Why this line makes such difference? How is it working?
As I understand it, changing a constraint doesn't update the layout of your views right away. It queues the changes, and they only take place the next time the system updates layout.
By putting layoutIfNeeded inside your animation block it forces the view changes to be applied during the animation block. Otherwise they take place later, after the animation has been set up.