52

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")
  }
kye
  • 2,166
  • 3
  • 27
  • 41
  • you can set scroll direction from vertical to horizontal using `UICollectionFlowLayout` – Cruz Jan 27 '17 at 01:53
  • @SwiftyCruz the direction is already set to horizontal – kye Jan 27 '17 at 03:57
  • 1
    then check basically, is your collectionView contentSize's width larger than collectionView bounds width it should be large for horizontal scroll – Cruz Jan 27 '17 at 04:09
  • Thanks @cruz I wasn't providing estimated sizes and was deferring size calculations too long. Fixing this helped solve my issue. – nteissler Oct 03 '19 at 19:26

15 Answers15

88

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

LunaCodeGirl
  • 5,432
  • 6
  • 30
  • 36
47

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.

kl.woon
  • 2,036
  • 1
  • 18
  • 10
  • That makes sense, I should've thought about that. Thanks for your help. – kye Jan 27 '17 at 04:16
  • This is a bit self-serving, but for those of you coming to this page any time post iOS 14 I suggest you scroll down and look at my answer, it seems to be a bug in collection view that many are encountering. – LunaCodeGirl Sep 25 '21 at 00:04
19

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)
    }
Pippo
  • 1,439
  • 1
  • 18
  • 35
  • 1
    but if `else` ? – Denis Kharitonov Apr 28 '17 at 13:53
  • 1
    This worked for me where the others did nothing -- In my case I already know the desired cell exists so I didn't need the if check – mwu Sep 05 '17 at 16:00
  • 2
    This works with swift 5.3 - IOS 14 - xCode 12. the usual scrollToItem is not working anymore in IOS 14 for me, and it was working in IOS 13.7 with swift 5.2 (one week ago). Thanks a lot ! – Tom3652 Sep 21 '20 at 15:44
11

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**
Kedar Sukerkar
  • 1,410
  • 1
  • 16
  • 22
  • 8
    You should not call `layoutSubviews()` method directly. If you want to force a layout update, call the `setNeedsLayout()` method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the `layoutIfNeeded()` method. https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews – Maor May 26 '19 at 11:16
10

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.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
9

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

  • 1
    Tried all other solutions but none worked!. This worked for me like a charm. Even better you could put this method in CollectionView Extension. – Sandeep Rana Feb 14 '22 at 10:31
8

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)
    }
alitosuner
  • 984
  • 1
  • 10
  • 15
  • Strange, but only with this it become work. Despite it was already on the main thread. – Nike Kov Feb 19 '20 at 11:02
  • Yeah odd, I'm using UICollectionViewCompositionalLayout with a horizontal scroll on paging. The scrollToItem call "with .centeredHorizontally" is triggered by scrollViewDidEndDecelerating. When scrolling left, it's always good, but when scrolling right, it always snaps to some middle position between cells. I wrapped it in a main thread after reading this, despite it already being on the main thread, and it works as expected.... wierdness – lzl Mar 30 '20 at 13:05
  • It works because this makes the block of code execute on the next run loop, it's like a delay, it is similar to using .layoutIfNeeded() that triggers an instantly update. – Juan Boero Mar 25 '21 at 00:38
7

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.

Newsonic
  • 413
  • 4
  • 19
Mr.Zee
  • 125
  • 1
  • 9
4

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.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
2

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)
 }
Yura Buyaroff
  • 1,718
  • 3
  • 18
  • 31
  • @LanceSamaria This is the oldest secret of iOS development: "When in doubt (or race condition) always rely on `DispatchQueue.main.asyncAfter`" – Adam Sep 14 '22 at 13:07
  • @Adam lol, thanks . I do have a few questions though. You have objectA and objectB both making a change to objectC. There’s now a race condition, which object to you apply DispatchQueue, why that object, and how long to set the deadline for? – Lance Samaria Sep 14 '22 at 16:42
  • Race conditions are a deep subject and you shouldn't ever delay writing explicitly by time. You should look into how to restrict access to a property so only one thread can make changes at a time. You can use `OperationQueue` or actor pattern – Adam Sep 15 '22 at 07:42
1

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.
Alvin George
  • 14,148
  • 92
  • 64
0

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()
  }
Nikolay
  • 127
  • 2
  • 3
0

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.

Ash
  • 9,064
  • 3
  • 48
  • 59
0

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

  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Nov 08 '22 at 02:48
-1

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()
atline
  • 28,355
  • 16
  • 77
  • 113
FreeBird0323
  • 11
  • 1
  • 2