kye
kye

Reputation: 2246

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

Upvotes: 55

Views: 61299

Answers (15)

Vamsi Mallela
Vamsi Mallela

Reputation: 1

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

Upvotes: 0

Ash
Ash

Reputation: 9351

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.

Upvotes: 0

Dheeraj Agarwal
Dheeraj Agarwal

Reputation: 51

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.

Upvotes: 5

Yura Buyaroff
Yura Buyaroff

Reputation: 1736

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

Upvotes: 1

barche
barche

Reputation: 319

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.

Upvotes: 10

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

Upvotes: 9

FreeBird0323
FreeBird0323

Reputation: 9

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

Upvotes: -2

Mr.Zee
Mr.Zee

Reputation: 135

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.

Upvotes: 8

LunaCodeGirl
LunaCodeGirl

Reputation: 5640

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

Upvotes: 100

alitosuner
alitosuner

Reputation: 1034

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

Upvotes: 11

Nikolay
Nikolay

Reputation: 145

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

Upvotes: -1

Kedar Sukerkar
Kedar Sukerkar

Reputation: 1565

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**

Upvotes: 11

Alvin George
Alvin George

Reputation: 14294

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.

Upvotes: 1

Pippo
Pippo

Reputation: 1473

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

Upvotes: 19

kl.woon
kl.woon

Reputation: 2055

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.

Upvotes: 48

Related Questions