UICollectionView scroll to item not working with horizontal direction - ios

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

Related

CollectionView scroll with button tap scrollToItem not working

I currently have a login and signup buttons labeled as 1 and 2. And underneath that, I have a collectionView with two cells (one for the login UI and one for register UI). When I click on the signup button(Label 2), I want to move the collectionView to the next cell.
I tried using the scrollToItem, but it does not do anything. Anyone know why?
#objc private func didTapRegisterButton() {
let i = IndexPath(item: 1, section: 0)
self.onboardingCollectionView.scrollToItem(at: i, at: .right, animated: true)
}
Figured it out. My collectionView had isPagingEnabled property as true. The scrollToItem doesn't work if that is set to true.
If you want to scroll between two cells on button click then first you need to uncheck(disable) Scrolling Enabled property and check(enable) Paging Enabled property of collectionView. (With this approach user can't scroll manually with swipe)
On button first action,
if self.currentIndexPath == 0 {
return
} else {
self.currentIndexPath += 1
self.yourCollectionView.scrollToItem(at: IndexPath(row: self.currentVisibleIndexPath, section: 0), at: .centeredHorizontally, animated: true)
}
On button second action,
if self.currentIndexPath == 1 {
self.currentIndexPath -= 1
self.yourCollectionView.scrollToItem(at: IndexPath(row: self.currentVisibleIndexPath, section: 0), at: .centeredHorizontally, animated: true)
} else {
return
}
And declare global variable
var currentIndexPath: Int = 0

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

How to detect if scrollToItem will scroll

I am trying to scroll a cell at a specific location (.bottom) and after the scroll animation finishes run some code. The problem is that scrollToItem function does not provide a completion handler therefor the only solution is to use the delegate (scrollViewDidEndScrollingAnimation). Using that I have one problem though, if the cell is already at that location .didEndScrollAnim. will never be called.
Is there a way to find if a cell is already at UICollectionView.ScrollPosition.bottom ?
collectionView.scrollToItem(at: indexPath, at: [.bottom,.centeredHorizontally], animated: true)
I want to be able to run a part of code when the cell is at the desired location [.bottom] and also animate the scroll if a scroll is needed.
Yes, you can use scrollViewDidEndDecelerating func and this func will call when scroll view grinds (comes) to a halt.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let secondItemIndex = IndexPath(item: 1, section: 0)
categoryCollectionView.selectItem(at: secondItemIndex, animated: true, scrollPosition: .centeredHorizontally)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let secondItemIndex = IndexPath(item: 2, section: 0)
categoryCollectionView.selectItem(at: secondItemIndex, animated: true, scrollPosition: .centeredHorizontally)
}
// Scroll way doesn't effect this func so you can specify any position as you want
I can think of a way, but it involves calling scrollViewDidScroll repeatedly, which does reduce the performance of the screen.
In the func scrollViewDidScroll:
if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) {
//reach bottom, here your code
}
I set the animated param to false and use UIView.animate() to detect the animation completion.
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
}, completion: { [weak self] _ in
// do something after scroll animation completed
}
)

Showing the last cell of Tableview

I want to show the bottom cells of my Tableview once the data are load I dont want to scroll every time to the bottom of Tableview to see the result
I have try this function in viewDidLoad but it did not work the page load at the top cells
if myArray.count > 0 {
tableView.scrollToRow(at: IndexPath(item:myArray.count - 1 , section: 0), at: .bottom, animated: false)
}
try use this
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [tableView] in
if myArray.count > 0 {
tableView.scrollToRow(at: IndexPath(item:myArray.count - 1 , section: 0), at: .bottom, animated: false)
}
}
You can leverage the usage of the scrollView's setContentOffset:animated: method
This answer should help you out: How to show last add cell on UITableview whileloading view or reloading tableview?

UICollectionView scrollToItem Not Working in iOS 14

I'm using a collection view, UICollectionView, and it works absolutely great...except that I cannot scroll to any particular item. It seems to always scroll to the "middle" is my best guess. In any case, whatever I send to scrollToItem seems to have no effect on the scrolling. I've put it in various locations throughout my view controller but with no success.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let lastIndex = IndexPath(row: messages.count-1, section: 0)
self.messagesView.scrollToItem(at: lastIndex, at: .bottom, animated: true)
}
UICollection View has bug in iOS 14 with scrollToItem. In iOS 14 it will only work if collection view paging will be desable.
So i got a solution if we have to scroll with both manual and programatically.
Specially for iOS 14 and above
self.collView.isPagingEnabled = false
self.collView.scrollToItem(at: IndexPath(item: scrollingIndex, section: 0), at: .left, animated: true)
self.collView.isPagingEnabled = true
You could try putting the scrolling code in viewDidLayoutSubviews which should get called after all table cells are loaded, which means your messages.count should work. Also, make sure you only have a single section in your collection view.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.scrollToBottom()
}
func scrollToBottom() {
DispatchQueue.main.async {
let lastIndex = IndexPath(item: messages.count-1, section: 0)
self.messagesView.scrollToItem(at: lastIndex, at: .bottom, animated: true)
}
}
Swift 5 / iOS 15
I was facing the same problem, so I tried this one-line code and get it done.
// here we slect the required cell instead of directly scrolling to the item and both work same.
self.collectionView.selectItem(at: IndexPath(row: self.index, section: 0), animated: true, scrollPosition: .left)
This is what worked for me, in case you want paging enabled:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.isPagingEnabled = false
collectionView.scrollToItem(
at: IndexPath(item: 1, section: 0),
at: .centeredVertically,
animated: false
)
collectionView.isPagingEnabled = true
}

Resources