froggomad
froggomad

Reputation: 1915

UICollectionView Won't scrollToItem(at: IndexPath, at: position)

I'm using this method to scroll my collection view.

import UIKit
extension UICollectionView {
    func scrollToLast() {
        guard numberOfSections > 0 else {
            print("number of sections < 0")
            return
        }
        let lastSection = numberOfSections - 1
        guard numberOfItems(inSection: lastSection) > 0 else {
            print("number of items < 0")
            return
        }
        let lastItemIndexPath = IndexPath(item: numberOfItems(inSection: lastSection) - 1,
                                          section: lastSection)
        self.scrollToItem(at: lastItemIndexPath, at: .centeredVertically, animated: true)
        print("last Item (section, item #): \(lastItemIndexPath)")
    }
}

I'm not calling this method until after the view has loaded and the collection view has items in it. It was working previously until I added a label to my cell prototype, and now though the view looks as it should with the new label, nothing will scroll using this method. I can still scroll the view manually. Note: output of the above method correctly lists the last item at the last indexPath when the method is called.

I have the following warning/error in console, but it doesn't seem to coincide with when the scroll is called, so not sure if its relevant:

2019-11-04 06:01:13.017384-0800 AppName[17598:4494554] [CollectionView] An attempt to prepare a layout while a prepareLayout call was already in progress (i.e. reentrant call) has been ignored. Please file a bug. UICollectionView instance is (; layer = ; contentOffset: {0, 0}; contentSize: {0, 0}; adjustedContentInset: {0, 0, 0, 0}; layout: ; dataSource: appBundle.instance

An example of how I'm calling it: (this is in viewDidLoad)

DataService.instance.messageListener(roomId: room.id, { (result) in
        self.messageArr.append(result)
        self.messageArr = self.messageArr.sorted(by: { $0.date < $1.date})
        self.collection.reloadData()
        self.collection.scrollToLast()
        self.messageTextArea.text = ""
})

Edit: I've removed all of calls to the main queue, and the behavior has changed. The view will now scroll to the top of the collection view when the messageListener method is triggered, but when using the same scrollToLast() method elsewhere, the view scrolls to the last item as it should - but only after the view is scrolled manually to the bottom. I've also tried moving the code to viewDidAppear per an answer below, but there was no effect.

Upvotes: 1

Views: 442

Answers (3)

froggomad
froggomad

Reputation: 1915

I'm not sure why this method isn't working, but I've changed the method to scrolling to the bottom of the content view instead of scrolling to a specific item, and this works fine as a workaround. This shouldn't be the accepted answer to this question, so I'll leave it open in case someone can resolve the original issue.

Here's the workaround:

extension UICollectionView {
    func scrollToBottomOfContent() {
        let contentHeight: CGFloat = self.contentSize.height
        let heightAfterInserts: CGFloat = self.frame.size.height - (self.contentInset.top + self.contentInset.bottom)
        if contentHeight > heightAfterInserts {
            self.setContentOffset(CGPoint(x: 0, y: self.contentSize.height - self.frame.size.height), animated: true)
        }
    }
}

Upvotes: 0

Chris
Chris

Reputation: 4391

When viewDidLoad is called, all views will be loaded but I find collection views and table views do not necessarily have all of their data loaded yet. Try calling your scrolling method in viewWillAppear or viewDidAppear instead.

Upvotes: 1

Mark
Mark

Reputation: 711

It is not related to your question but i have some recommendations:
1) You don't need to call DispatchQueue.main.async before calling self.scrollToItem, because you already switch to main thread in DataService.instance.messageListener.
2) You have potential multithreading problems with messageArr, because you edited it from your DataService messageListener completionHandlers thread (and it could be on it's on thread). As i understand messageArr is datasource for your collection view. So when you scrolling your collection view or when it's reloaded, collectionView would access messageArr from main thread. That would lead to crash or undefined behaviour, because you access messageArr array from two threads without locking. One of the solutions is just to async entire DataService.instance.messageListener completion handler to main queue.

    DataService.instance.messageListener(roomId: room.id, { (result) in
        DispatchQueue.main.async {
            self.messageArr.append(result)
            self.messageArr = self.messageArr.sorted(by: { $0.date < $1.date })
            self.collection.reloadData()
            self.collection.scrollToLast()
            self.messageTextArea.text = ""
        }
    })

Upvotes: 1

Related Questions