I have a vertical UICollectionView with flow layout and I'm trying to implement an infinite scroll behaviour. When the view comes near to the end, the api request 10 more registers on the server side and then I update the collection data. But when I do that my table don't keeps on the same cell I was before the reload. How can I keep the position after a reload?
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == viewModel.numberOfItemsInSection - 3 {
self.viewModel.fetchNextRepositoriesPage() { shouldUpdateData in
self.repositoriesListView.repositoriesCollectionView.reloadData()
self.repositoriesListView.layoutIfNeeded()
}
}
}
private func reloadCollectionData() {
let offset = collectionView.contentOffset
collectionView.reloadData()
view.layoutIfNeeded()
collectionView.contentOffset = offset
}
EDIT:
I noticed that this problem only occurs when I use estimadedItemSize on my collectionView. When the itemSize is a constant the problem does not occur.
Related
I have created a User Onboarding as a Collection View with 5 cells (pages).
I have 2 types of Collection View Items: welcomeCell which is always a first and single cell in Onboarding (indexPath.item == 0) and tutorialCell (all other cells) which has a tableView and can scroll its content vertically.
I want to add the next behaviour:
user scrolls tableView content and swipes to the next page
if user swipes back to the page he scrolled I want the page layout to be reloaded like it was initially set.
For now if a user swipes to the page he scrolled, he will see its position where he ended the scroll.
I suggests that collectionView content can be reloaded in didEndDisplaying method:
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item != 0 {
self.collectionView.reloadItems(at: [indexPath])
}
}
But with this code I receive a strange cells behaviour: I can swipe up tableView on one page and the came content position will be on another page, but some page can be loaded with normal content position. Here is a GIF: GIF
I also tried collectionView.reloadData() in scrollViewDidScroll and scrollViewDidEndDecelerating but receive the similar behaviour.
Would you be so kind to share some experience and help to understand what's the best practice for my case? Maybe I should find a way to not reload a page content at all, but reload imageView height anchor in constraints?
Try with DispatchQueue method:
if indexPath.item != 0 {
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
After some experiments and information research I ended up with following results:
I slightly modified my initial goal: with below code if user, while swiping, will decide to stay on the current page, then data won't be reloaded. In other cases - reload.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OnboardingCell", for: indexPath) as! OnboardingCollectionViewCell
// reset the current tableView position to initial one
if cell.tableView.window != nil {
cell.tableView.contentOffset = CGPoint.zero
cell.tableView.layoutIfNeeded()
cell.tableView.reloadData()
}
return cell
}
private var activePage = 0 {
didSet {
if activePage != oldValue {
// the method will fire cellForItemAt code
collectionView.reloadData()
}
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
for cell in collectionView.visibleCells {
activePage = collectionView.indexPath(for: cell)?.row ?? 0
}
}
Also if you have a navigation controls, like previous and next buttons, don't forget to reload data like so:
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
collectionView.reloadData()
}
I'm trying to create a view showing the dates for all weekdays in one single row with horizontal scrolling and pagination enabled. Similar to how it works in the iPhone's calendar app, one full week should be visible at once.
To do that, I created a collectionView and set it up to display those seven items. When the user scrolls to the left (to see the next week), the function collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) is called. Here I add the next items and reload the collectionView.:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item > numberofitems - 10 {
numberofitems += 15
DispatchQueue.main.async {
self.myCollectionView.reloadData()
}
}
The collectionView is then reloaded:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! myCollectionViewCell
dayComponent.day = -3 * 7 - Calendar.current.component(.weekday, from: Date()) + indexPath.item + 1 + indecesadded
cell.setup(date: Calendar.current.date(byAdding: dayComponent, to: Date())!)
}
This seems to work pretty well.
However, what I also need, is being able to scroll in the other direction (backwards) and add new cells before the visible ones. I tried using the same method and adding the contentOffset:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item < 10 {
numberofitems += 14
indecesadded += 14
DispatchQueue.main.async {
self.myCollectionView.reloadData()
self.myyCollectionView.setContentOffset(CGPoint(x: self.myCollectionView.contentOffset.x + 2 * self.view.bounds.width, y: 0), animated: false)
}
}
}
The problem is that after adding the new cells (mid-scrolling), the scrolling stops abruptly. When I manually beginn scrolling again, the paging seems to be off, meaning that the last day of one week does not correspond to the first day of the next - 1. (see images below).
before scrolling to previous week:
after scrolling:
after manually scrolling collectionView again:
So the question is: How do I insert items before the currently existing cells in a collectionView with paging enabled without messing up the scrolling and paging?
maybe you should try using scrollViewDidScroll like:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset
lastContentOffset = offset //save the offset if u need to set it later
if offset.x > 30{
print("right")
scrollRight()
} else if offset.x < -30 {
scrollLeft() // here you can implement the code bellow preformBatchUpdates
print("left")
}
}
if you want to have a sort of jumpy experience, like when u scroll it turns the page but it doesn't show how the numbers smoothly go away like on built in IOS Calendar on IPhone, that's the code i've tried for that:
collectionView.performBatchUpdates(
{
collectionView.insertSections([0])
sectionIndex += 1
}, completion: nil)
collectionView.performBatchUpdates({
collectionView.deleteSections([sectionIndex - 1])
sectionIndex -= 1
}, completion: nil)
that is implied when i override scrollViewDidScroll and scroll left for example. same is done for the other direction, the idea is that you keep maintaining only 1 section, so the adding of the sections doesn't mess up the position of the items in collectionView. i am actually working on the same project so if someone could provide a sample code for how i can be done like on the IPhone calendar it would also help me, hope that snippet gives you any new ideas or solves your problem
the answer here here works but i get some weird results
I am implementing self sizing UITableViewCell containing UICollectionView which extends its height based on the number of items received from API.
The views are structured like this
UITableView
UITableViewCell
UICollectionView
UICollectionViewCell
UICollectionViewCell
UICollectionViewCell
When the UICollectionView receives new data, UITableViewCell frame is updated with the following code
layoutIfNeeded()
collectionView.reloadData()
collectionView.collectionViewLayout.invalidateLayout()
collectionView.layoutIfNeeded()
collectionView.snp.updateConstraints { make in
make.height.equalTo(collectionView.collectionViewLayout.collectionViewContentSize).priority(.high)
}
The problematic part is adding pagination on scroll to the bottom of the UICollectionView. The self sizing code causes that willDisplay cell is called for each cell of the UICollectionView causing // Fetch more data request to trigger, which fetches new data, updates UICollectionView height, triggering the API request again, and it's an infinite loop.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
// Fetch more data
}
}
Any idea how to avoid this cycle? Thanks.
I am building an chat app and I have a problem with the collection view. In my app I just want to do something like whatsapp, in starting I load the first 50 messages and when we scroll down then want to fetch other messages bunch by bunch. But when I doing this and call the collection view reloadData method then it scroll to the top message. And I don't want they scrolling. Can you help me. Please..
I just simply want to make the collectionView similar to whatsapp's chat view.. i.e. load more messages when user scroll to the older messages.
What you are asking for is just feeding (inserting) cell into collection view without reloading it.
Check this appledoc, on inserting item.
determine the new indexPaths and then feed it to collection view using following
yourCollectionView.insertItems(at indexPaths: newIndexPaths)
Detect when you reach at the end and fetch more data
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
{
let lastSectionIndex = collectionView.numberOfSections - 1
let lastRowIndex = collectionView.numberOfItems(inSection: lastSectionIndex) - 1
if indexPath.section == lastSectionIndex && indexPath.row == lastRowIndex
{
//fetch more items
//add items to data array
//Reload collectionview
}
}
Try pagination concept
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row + 1 == dataArray.count && dataArray.count < completeData.count {
fetchMoreData()
}
.....
}
func fetchMoreData() {
//Fetch and add more data to your dataArray.
//Reload collectionView
}
have a collectionView that potentially loads a ton of cells depending on the data. Currently my architecture is viewDidLoad (get first 25 from dataSource), then in willDisplayCell (get rest of datasource in sets of 25 until I hit a rate limit). The problem is that it appears that its pulling down all the data at once. I would like to not pull down more data for the cells unless the user is scrolling (i.e. unless those cells are necessary to be displayed). How can I do this?
I know that I need to work with the cell and indexPath inside willDisplayCell but I can't quite piece it together?
override func viewDidLoad() {
super.viewDidLoad()
getFirstPinsAndImages()
}
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if maxExceeded == false {
getRestOfPinsAndImages()
}
}
What I needed to do was limit the data call to only when the indexPath.row of willDisplayCell (i.e. the row that is about to be displayed) was greater than the number of items in section (cells loaded) less some arbitrary number (to give a buffer for loading time):
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if indexPath.row > (self.collectionView(collectionView, numberOfItemsInSection: 0) - 10) {
if maxExceeded == false {
getRestOfPinsAndImages()
}
}
}