How to execute code after an animation without using the completion block? - ios

I would like to execute some code after a built-in animation is completed.
I have a UITableView with a lot of cells/rows. Sometimes, when I do some operations and then I need to scroll to the top of my tableView. For that I use :
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: true)
But I need to perform some code once the top is reached, such a simple select and deselect of the 1rst row.
I implemented func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) from the UIScrollViewDelegate. It works fine (more or less, sometimes the animation isn't really smooth and we don't see the select/deselect "animation") except when I am already at the top of the tableView, then scrollViewDidEndScrollingAnimation isn't called.
So is there a way to execute some code once scrollToRowAtIndexPath:atScrollPosition:animated has been called?
EDIT:
When I'm talking about doing some operations, I'm talking about moving a row using moveRowAtIndexPath:toIndexPath from UITableView.
So when scrolling is needed it's fine, both animations take about the same amount of time. But when no scroll is needed, then the execution of the code I want to do after the animation, starts at the same time than the animation

You can use this Swift code which I adapted from an old Objective-C answer to scroll the view.
// First, test whether the tableView needs to scroll to the new position
var originalOffset = tableView.contentOffset.y;
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: false)
var offset = tableView.contentOffset.y;
if (originalOffset == offset) {
// No animation is needed since its already there
doThingAfterAnimation();
} else {
// We know it will scroll to a new position
// Return to originalOffset. animated:NO is important
tableView.setContentOffset(CGPointMake(0, originalOffset), animated: false);
// Do the scroll with animation so `scrollViewDidEndScrollingAnimation:` will execute
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: true)
}
And then of course:
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
doThingAfterAnimation();
}

Related

Animate View Off-Screen To The Right and Back In From the Left

I'm trying to replicate an animation found on the Apple App Store seen here. The app icons move from left to right continuously like a carousel and I'm trying to produce the same behaviour for a single UIView. Is anyone able to help with this? Really struggling:
Something I tried but it stops and repeats the animation without continuing from left to right.
UIView.animateKeyframes(withDuration: 5, delay: 0, options: .repeat, animations: {
viewOfImageViews.center.x += container.bounds.width +
}, completion: nil)
Here is one way you can archive this effect:
Add a collection view
Set cell width the same width as your superview
Set your collection view scroll direction to horizontal
Now in the code when the collection is loaded you scroll to the first section
DispatchQueue.main.async {
self.collView.scrollToItem(at: IndexPath(item: 0, section: 1), at: .left, animated: true)
}
Now you can add this code below so when animation is done it updates the cells and scroll again
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
colors.append(colors.removeFirst())
collView.reloadData()
DispatchQueue.main.async {
self.collView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .left, animated: false)
self.collView.scrollToItem(at: IndexPath(item: 0, section: 1), at: .left, animated: true)
}
}
Change colors array to your array name which contains the images

UICollectionView scroll to item not working with horizontal direction

I have a UICollectionView within a UIViewController with paging enabled. For some strange reason collectionView.scrollToItem works when the direction of the collectionview is vertical but doesn't when direction is horizontal. Is this there something I'm doing wrong or is this supposed to happen?
//Test scrollToItem
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let i = IndexPath(item: 3, section: 0)
collectionView.reloadData()
collectionView.scrollToItem(at: i, at: .top, animated: true)
print("Selected")
}
For iOS 14
Apparently there is a new bug in UICollectionView that is causing scrollToItem to not work when paging is enabled. The work around is to disable paging before calling scrollToItem, then re-enabling it afterwards:
collectionView.isPagingEnabled = false
collectionView.scrollToItem(
at: IndexPath(item: value, section: 0),
at: .centeredHorizontally,
animated: true
)
collectionView.isPagingEnabled = true
Source: https://developer.apple.com/forums/thread/663156
For this part:
collectionView.scrollToItem(at: i, at: .top, animated: true)
When the scroll direction is horizontal you need to use at: left, at: right or at: centeredHorizontally. at: top is for vertical direction.
I had trouble implementing this in a flow layout with entered paging per item. The .centeredHorizontally just wouldn't work for me so i use scroll to rect and Check there is data before scrolling:
if self.collectionView?.dataSource?.collectionView(self.collectionView!, cellForItemAt: IndexPath(row: 0, section: 0)) != nil {
let rect = self.collectionView.layoutAttributesForItem(at: IndexPath(item: data[index], section: 0))?.frame
self.collectionView.scrollRectToVisible(rect!, animated: false)
}
Swift 5.1, Xcode 11.4
collectionView.scrollToItem(at: IndexPath(item: pageNumber , section: 0), at: .centeredHorizontally, animated: true)
self.collectionView.setNeedsLayout() // **Without this effect wont be visible**
I try a lot of things and fail. This saves my day.
func scrollToIndex(index:Int) {
let rect = self.collectionView.layoutAttributesForItem(at: IndexPath(row: index, section: 0))?.frame
self.collectionView.scrollRectToVisible(rect!, animated: true)
}
Reference: https://stackoverflow.com/a/41413575/9047324
I have this issue: when button tapped (for horizontal collection scroll to next item) it always returns to first item with UI bug.
The reason was in this parameter: myCollection.isPagingEnabled = true
Solution: just disable paging before scroll to next item, and enable it after scroll.
After adding items to collectionView and reloadData(), scrollToItem was not working because reloading data has not finished yet.
Because of this I added performBatchUpdates like this :
self.dataSource.append("Test")
self.collectionView.performBatchUpdates ({
self.collectionView.reloadData()
}, completion: { _ in
self.collectionView.scrollToItem(at: IndexPath(item: 3, section: 0),
at: .centeredHorizontally,
animated: false)
})
I know it's not about this question but it will be helpful for this title.
For me, I had to scroll collection view on main thread like:
DispatchQueue.main.async {
self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
}
This will not work if you call scrollToItem before viewDidLayoutSubviews. Using scrollToItem in viewDidLoad will not work. So call this function after viewDidLayoutSubviews() is completed.
For iOS 14+
It's so stupid but it works.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.collectionView.scrollToItem(at: i, at: .top, animated: true)
}
Swift 3:
This worked for me on horizontal collection view.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//Show 14th row as first row
self.activityCollectionView?.scrollToItem(at: IndexPath(row: 14, section: 0), at: UICollectionViewScrollPosition.right, animated: true)
}
You can choose whether the desired cell at index to be on right or left on scroll position.
UICollectionViewScrollPosition.right or UICollectionViewScrollPosition.left.
Swift 5 if with animation horizontally
func scrollToIndex(index:Int) {
self.collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
}
Your example:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
print("Selected: \(indexPath.row)")
collectionView.reloadData()
}
As #Dheeraj Agarwal points out, none of the 'ScrollTo' functionality of UICollectionView OR UIScrollView will seem to work properly if it is called before the view has been laid out. To be accurate, I think they WORK, but the effects are immediately nullified because laying out a UICollectionView causes it to reset to its minimum bounds, probably because of all the cell layout functions that will trigger, and the fact its content size may change.
The solution is to make sure this function is called after layout occurs, but it's not that simple. It's entirely likely that a collection view may be told to layout its content again and again in response to various changes - setting delegates, the contents updating, the view controller being added to a parent and therefore changing size. Each time this happens it'll reset to 0:0 offset.
You'll therefore have to keep a reference to the desired offset / cell index / frame until such a time as you are CERTAIN there will be no more unexpected layout updates. You can't just nil it out immediately as your collection view's layout might change multiple times before the view appears. I'm currently storing a frame in an attribute and calling the function in layoutFrames every time (my collection view's parent is a custom view, not a view controller). Although this has the slightly annoying feature of scrolling back again if the user rotates their phone, I consider it acceptable since this is a custom keyboard and most users will work with it in one orientation or the other, they won't keep flipping their phone around just to select a single value.
Solutions like calling DispatchQueue.main.asyncAfter are fragile. They work because the function call gets delayed until after the first layout occurs, but this may not always-and-forever solve the problem.
I guess the 'Scroll To' functions were only ever intended to be used in response to direct user input after the collection view is already populated.
CollectionView.isPagingEnabled = false
CollectionView.scrollToItem(at:IndexPath(item: YourcellIndex(4), section: at which section you want cell(0)), at: .centeredHorizontally, animated: false)
CollectionView.layoutSubviews()
CollectionView.isPagingEnabled = true
sometimes you can set
collectionView.collectionlayout.invalidate()
collectionView.scrollToItem(row:2,section:0)
If you have networking which can make disturb main thread, you have to avoid or after finish that, have to apply this code.
for example
self.group.enter()
some thread
self.group.leave()

TableView scrollToRow is blocking the main ui thread

I have a UITableView with about 500 items.
When i call tableView.scrollToRow(at: indexPath, at: .bottom, animated: false) the main UI thread is getting blocked for 3 seconds.
Is there a way to fix this? or is the problem scrolling 500 items?
Thanks
the problem is not with reloadData it was with scrollToRow
From discussion about how to use a table view for chat:
We can use a table view which uses a transform to flip the Y coordinate. We then need to do the same for each of the cells so they are not upside down.
The procedure is to build a normal messaging table view where the newest message is on top (instead of bottom). Then put the table view on some superview and invert its coordinate system:
chatContainer?.transform = CGAffineTransform(scaleX: 1.0, y: -1.0)
The cell containing the messages should also have some sort of superview for all the contents which needs to be flipped:
override func awakeFromNib() {
super.awakeFromNib()
containerView?.transform = CGAffineTransform(scaleX: 1.0, y: -1.0)
}
So the cell is basically flipped twice so it is shown correctly.
You may find an example project here.
Use time profiler to identify where exactly is the issue. The thing is that in general the UITableView performance is not effected by a number of items loaded. The view itself will load as many items as it needs to fill the whole screen.
You may test this by logging a method in a cellForRowAtIndexPath. So I am guessing this method may be the one that is slow. Check how you access the data from it, maybe there is some heavy logic on it. Or the cell layout may be bugged and very slow.
In a general case if you have extremely large amount of data consider using core data and NSFetchedResultsController which is designed specifically for this situations. But still note that loading 500 elements in a table view should work smoothly without any special optimizations.
You should do something like this; If user scroll down from top to bottom of the tableview scrollview delegate method fire its "scrollViewDidScroll" method and detect if user bottom of tableview or not then fetch other data and append your array and reload the tableview. Thats it!
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat actualPosition = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height - (someArbitraryNumber);
if (actualPosition >= contentHeight) {
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
}
I'm not 100% sure about this solution, didn't had that problem myself.
Maybe just dispatch it?
extension UITableView {
func tableViewScrollToBottom(animated: Bool) {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
let numberOfSections = self.numberOfSections
let numberOfRows = self.numberOfRows(inSection: numberOfSections-1)
if numberOfRows > 0 {
let indexPath = IndexPath(row: numberOfRows-1, section: (numberOfSections-1))
self.scrollToRow(at: indexPath, at: UITableViewScrollPosition.bottom, animated: animated)
}
}
}
}

Scrolling bottom in tableview and performance

I need to scroll to bottom. I have a chat app like whatsapp. So when view appears table view should show last row. I am achiving this with following line and works nice.
tableView.setContentOffset(CGPointMake(0, CGFloat.max), animated: false)
Also I need to scroll to bottom when keyboard appears. I am using Auto Layout and above line is not working. For to do this i am using following line:
func scrollToLastRow(animated: Bool) {
if self.numberOfRowsInSection(0) > 0 {
self.scrollToRowAtIndexPath(NSIndexPath(forRow: self.numberOfRowsInSection(0) - 1, inSection: 0), atScrollPosition: .Bottom, animated: animated)
}
}
This is a extension for Tableview.
This solution is working fine when there is no too much message. Then I tried with 5000 messages (so tableview have 5000 rows, but i am paging them) And when keyboard appears i see cpu usage is %98-100. I think the second code is problem for pagination, it causes loading every message to ram and my app freezes and receiving ram warning.
How to scroll to bottom without any performance issue?
If you have pagination, you can try to only load your current page as well as the final page, assuming you have 20 messages in each page, in this case your table have 40 rows only. Then you can use your function:
func scrollToLastRow(animated: Bool) {
if self.numberOfRowsInSection(0) > 0 {
self.scrollToRowAtIndexPath(NSIndexPath(forRow: self.numberOfRowsInSection(0) - 1, inSection: 0), atScrollPosition: .Bottom, animated: animated)
}
}
try this method :
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.tableView.numberOfSections
let numberOfRows = self.tableView.numberOfRowsInSection(numberOfSections-1)
if numberOfRows > 0 {
let indexPath = NSIndexPath(forRow: numberOfRows-1, inSection: (numberOfSections-1))
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Bottom, animated: true)
}
})
If your are creating IM app. I recommend you reverse the tableView.
So the first row appears at bottom, and don't needs scrolling at beginning anymore.
Here is a cocoapod could help: https://github.com/marty-suzuki/ReverseExtension
If you stil want to scroll to specific row
implement the UIScrollViewDelegate.scrollViewDidEndScrollingAnimation.
and goes like this.
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if scrollView.contentSize.height > lastContentHeight {
self.tableView.scrollToBottom()
self.lastContentHeight = scrollView.contentSize.height
}
}

Scroll to top with estimated cell heights

What is the proper way of scrolling a UITableView to the top when using estimated cell heights by implementing tableView:estimatedHeightForRowAtIndexPath:?
I noticed that the usual method does not necessarily scroll to the top if there is enough estimation error.
[self.tableView setContentOffset:CGPointMake(0, 0 - self.tableView.contentInset.top) animated:animated];
I came across a similar issue (I wasn't trying to scroll the tableview to the top manually but the view wasn't scrolling correctly when tapping the status bar).
The only way I've come up with to fix this is to ensure in your tableView:estimatedHeightForRowAtIndexPath: method you return the actual height if you know it.
My implementation caches the results of calls to tableView:heightForRowAtIndexPath: for efficiency , so I'm simply looking up this cache in my estimations to see if I already know the real height.
I think the issue comes from tableView:estimatedHeightForRowAtIndexPath: being called in preference over tableView:heightForRowAtIndexPath: even when scrolling upwards over cells that have already been rendered. Just a guess though.
How about tableView.scrollToRow? Solved the issue for me.
Swift 3 example:
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
how about this snippet code
[UIView animateWithDuration:0.0f animations:^{
[_tableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:NO]; //1
} completion:^(BOOL finished) {
_tableView.contentOffset = CGPointZero; //2
}];
scroll to offset that calculated by estimatedHeightForRowAtIndexPath
setContentOffsetZero
inspiration from https://github.com/caoimghgin/TableViewCellWithAutoLayout/issues/13
let point = { () -> CGPoint in
if #available(iOS 11.0, *) {
return CGPoint(x: -tableView.adjustedContentInset.left, y: -tableView.adjustedContentInset.top)
}
return CGPoint(x: -tableView.contentInset.left, y: -tableView.contentInset.top)
}()
for section in (0..<tableView.numberOfSections) {
if 0 < tableView.numberOfRows(inSection: section) {
// Find the cell at the top and scroll to the corresponding location
tableView.scrollToRow(at: IndexPath(row: 0, section: section),
at: .none,
animated: true)
if tableView.tableHeaderView != nil {
// If tableHeaderView != nil then scroll to the top after the scroll animation ends
CATransaction.setCompletionBlock {
tableView.setContentOffset(point, animated: true)
}
}
return
}
}
tableView.setContentOffset(point, animated: true)

Resources