How can i know when default functions animations finish? - ios

In my application, i listed delivered notifications as sorted by date in tableView. If user tap to notifications from device notification screen, app highlights row. But before user presses notification on device main screen, if users scroll towards the end of the tableView, when user presses notification, app scrolls the tableView to the row with as an animated and also highlights the row.
But if users scrolled contentView to end of the tableView, highlight is not working. I think scroll to row animation function and highlight animation function working at same time.
How can i catch when tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) animation finished. If i know when scrollToRow animation finish, i can run highlight row code after.
Thanks.
DispatchQueue.main.async {
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
if let cell = self.tableView.cellForRow(at: indexPath) as? NewsItemCell {
let highlightView = UIView.init(frame: cell.contentView.frame)
highlightView.backgroundColor = .white
highlightView.alpha = 0.0
cell.contentView.addSubview(highlightView)
print(indexPath)
UIView.animate(withDuration: 1.0, delay: 0, options: .curveEaseIn) {
highlightView.alpha = 1.0
} completion: { Bool in
UIView.animate(withDuration: 1.5, delay: 0, options: .curveEaseOut) {
highlightView.alpha = 0.0
} completion: { Bool in
highlightView.removeFromSuperview()
}
}
}
}

A UITableView is a subclass of UIScrollView.
You should be able to implement the UIScrollViewDelegate method scrollViewDidEndScrollingAnimation(_:) in your table view delegate (usually the owning view controller) and then highlight the row in your implementation of that method. (You'll probably need to add logic and state variables to track the fact that you are in this situation.)
Edit:
Note that as mentioned in the answer from #trndjc linked by HangerRash, you should your follow-on animation code from a call to Dispatch.main.async(). (That will help avoid stutters in the animations.)
From that answer:
Second, despite the fact that the method is called on the main thread, if you're running a subsequent animation here (one after the scroll has finished), it will still hitch (this may or may not be a bug in UIKit). Therefore, simply dispatch any follow-up animations back onto the main queue which just ensures that the animations will begin after the end of the current main task (which appears to include the scroll-to-row animation). Doing this will give you the appearance of a true completion.
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
DispatchQueue.main.async {
// execute subsequent animation here
}
}

Related

UIView.animateKeyframes not working properly after iOS 15 update

I have UITableView that update rapidly via WebSocket and RXSwift. Every update will play flash animation. Everything works well in iOS11 - iOS14 but after the iOS15 update, the animation has weird behavior. It isn't play properly. It skip most of animation updates. Sometime it play animation in all rows at the same time.
Edited: I've got another issue; when I press on the button in the cell, the action didn't fire. It take a lot of click on it to make it fired, looks like it can't touch the button while updating. Sometime I press on button in cell 1 but the action fired as cell 2 context.
(Cell information was hidden for secret)
From the video, on the left was run on iOS11-iOS14. The animation works smoothly while on the right the animation was skipped.
The code to update the animation is:
func flash()->Observable<Void>{
return Observable.create { (observer) -> Disposable in
UIView.animateKeyframes(withDuration: 0.5, delay: 0, options: []){
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
self.background.alpha = 0
}
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.5) {
self.background.alpha = 0.2
}
UIView.addKeyframe(withRelativeStartTime: 1, relativeDuration: 0.5) {
self.background.alpha = 0
}
}
return Disposables.create()
}
}
Call it like this. I didn't dispose someBehaviorRelay because it removes the animation
someBehaviorRelay.subscribe { [unowned self] value in
flash().subscribe().disposed(by: disposeBag)
}
And I reassign disposeBage when reuse
override func prepareForReuse() {
disposeBag = DisposeBag()
}
Is there any suggestion for me to solve this problem? Thank you.
UPDATE
I found the solution is use reconfigure instead of reloadData for UITableView
// if iOS15
let indexPaths = tableView.indexPathsForVisibleRows!
if !indexPaths.isEmpty {
tableView.reconfigureRows(at: indexPaths)
} else {
tableView.reloadData()
}
I found the solution already, updated in the question.

UIRefreshControl is not disappearing after the tableview scrolls to top programmatically

When i want to the update data source of the tableview, firstly i want to scroll to top (to header view) then show the UIRefreshControl view, after data source updated then i want to hide the UIRefreshControll.
To do that i tried following line:
self.kisilerTable.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: false)
self.kisilerTable.setContentOffset(CGPoint(x: 0, y: self.kisilerTable.contentOffset.y - (self.refreshControl.frame.size.height)), animated: true)
self.kisilerTable.refreshControl?.beginRefreshing()
Above code is scrolling to top then it shows the UIRefreshControl. And after my data updated i tried to hide it with following code:
self.kisilerTable.reloadData()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.kisilerTable.refreshControl?.endRefreshing()
}
But the problem is, above code is hiding the indicator animation but it leaves a space there. As you can see in the screenshot:
If i try to hide with following code:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.kisilerTable.reloadData()
self.kisilerTable.refreshControl?.endRefreshing()
}
It is hiding but, i am losing the hide animation. I don't want to lose hide animation.
How can i resolve this problem?
I think it happens, because in the beginning you use setContentOffset to leave a space, but that space is not removed.
I will suggest another implementation, which works very well:
First you declare the refreshControll before the method viewDidLoad():
let refreshControl = UIRefreshControl()
Now do you need create a method for stop the animation when needed:
func stopAnimatingRefreshControl() {
if let refreshControl =
tableView.subviews.first(where: { $0 is UIRefreshControl})as? UIRefreshControl,
refreshControl.isRefreshing {
refreshControl.endRefreshing()
}
}
Now you create an #objc function that be called when scrolls the table to update, normally in this moment you call methods that make your server request to update data.
When this data is updated, you call the function stopAnimatingRefreshControl():
#objc func refreshMyData() {
// call methods for get data from server
getDataFromServer()
}
func getDataFromServer() {
// request completed and data is updated, now i need stop the loading.
DispatchQueue.main.async {
self.tableView.reloadData()
}
stopAnimatingRefreshControl()
}
Finally, after creating the necessary methods, you need to link our #objc function to refreshControll and our tableView, do it in method viewDidLoad() :
refreshControl.addTarget(self, action: #selector(refreshMyData), for: .valueChanged)
tableView.addSubview(refreshControl)
Now you're done, I hope I helped.

Access a UIView's properties on a background thread

I am trying to have my CollectionView scroll it's first cell after the view appears, and then again whenever a button is pressed. The problem is that the collectionView hasn't generated all it's cells at any of the view lifecycle functions.
My solution is to beging a while loop on a background thread that checks to see if collectionView.visibleCells.count > 0, and when it is, return to the main thread and scroll to the first cell. However, I get an error, telling me that I shouldn't access visibleCells off the main thread, and the app chugs when I do.
How can I achieve this functionality on the main thread, or check the number of cells in the background thread?
private func scrollToFirst() {
DispatchQueue.global(qos: .background).async { [weak self] in
if (self != nil) {
while(self!.collectionView.visibleCells.count != 0) {
DispatchQueue.main.async { [weak self] in
self!.collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: true)
}
}
}
}
}
There is a delegate method willDisplay that gets called right before a collectionViewCell gets displayed. If you previously had no cells and this gets called, then you know you are about to go from zero to more than zero cells.
Yeah, don't do that. UIKit is not thread-safe, so the data structures of view objects may change out from under you when you try to view them from a background threads.
It seems like there should be a better way to deal with this than waiting for cells to appear.
If you can't figure out a cleaner way to do it, you could use a Timer object, which runs on the main thread. That code might look something like this:
private func scrollToFirst(afterDelay delay: Double = 0.2) {
_ = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) {
timer, [weak self] in
guard strongSelf = self else {
return
}
if strongSelf.collectionView.visibleCells.count != 0 {
self!.collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: true)
}
}
That code would fire once, and scroll to the beginning of the collection view if there are once the timer fires.

Problems pausing UIViewPropertyAnimation

I am using UIViewPropertyAnimator to make small contentOffset animations.
To keep my view controller lean I have the animation code in the UIScrollView subclass which is to be animated. I originally did my animations in a UIView.animate block, but I noticed that when the view disappears (i.e. another view is pushed on top of the view) the animation jumps to the end, so I am trying to implement a pause in the animation. In addition I wanted to reduce the CPU load through unnecessary animation calls.
var animator: UIViewPropertyAnimator?
...
func showAnimation() {
...
if animator == nil {
animator = UIViewPropertyAnimator(duration: duration, curve: .linear, animations: { [unowned self] in
self.contentOffset = endPoint
})
animator?.addCompletion { [unowned self] (position) in
if position == .end {
self.afterAnimation()
print("completion called")
}
}
}
}
func pauseAnimation() {
if animator?.state == .active {
animator?.pauseAnimation()
}
print("paused animation")
}
The method afterAnimation() just determines if the showAnimation() is to be called again or not.
In my view controller I basically have the following:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myScrollView.showAnimation()
}
override func viewWillDisappear(_ animated: Bool) {
viewWillDisappear(animated)
myScrollView.pauseAnimation()
}
Now this works well as long as the user does not stay on another screen too long.
For example if the user calls up the settings screen (which gets pushed onto the
navigation stack) the pause method is called - just as expected. If the user stays
in that screen too long, then the animator's completion method is called, even though
the animation is not completed. Once the user dismisses the settings screen again the
animation is frozen and does not continue.
I also tried working with
animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: duration, delay: 0, options: .curveLinear, animations: { [unowned self] in
self.contentOffset = endPoint
print("starting animation")
}) { [unowned self] _ in
self.afterAnimation()
print("completion called")
}
but the results were the same. After a while the completion block gets called.
How can I best solve this issue? Thanks!
EDIT: Through different print statements I am slowly having the idea that even when the animator gets paused, the animation's internal counter continues and calls the completion block when the animation should be finished.

Dynamically scroll through tableView with scrollToRowAtIndexPath

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

Resources