CollectionView with infinite horizontal scrolling in both directions and pagination enabled - ios

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

Related

How to reload collectionView cell when it did end displaying?

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

How to achieve smooth expand/ collapse animation in UICollectionView with dynamic cell height?

I have implemented expand/ collapse animation, for a UICollectionView with dynamic cell height (Because different cell has different content).
This is the summary of my implementation
I am using UICollectionViewCompositionalLayout, because I want the cell able to adjust its own height to accommodate its content. (https://stackoverflow.com/a/51231881/72437)
I am using UIStackView in the cell. Reason is that, once I hide one of the UITextViews in the cell, I do not want the hidden UITextView to still occupy the space. Using UIStackView can avoid me from dealing with zero height constraint.
I am using performBatchUpdates and layoutIfNeeded to achieve the animation, based on https://stackoverflow.com/a/69043389/72437
Here's the final outcome - https://www.youtube.com/watch?v=2uggmpk0tJc
As you can see, the overall effect isn't really smooth, espcially when I toggle in between "Color" and "Print PDF", which are having larger content height.
This is what happen when I tap on the cell
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
var indexPaths = [IndexPath]()
for i in (0..<isExpanded.count) {
if isExpanded[i] != false {
isExpanded[i] = false
indexPaths.append(IndexPath(item: i, section: 0))
}
}
if isExpanded[indexPath.item] != true {
isExpanded[indexPath.item] = true
indexPaths.append(IndexPath(item: indexPath.item, section: 0))
}
collectionView.performBatchUpdates({}) { _ in
collectionView.reloadItems(at: indexPaths)
collectionView.layoutIfNeeded()
}
}
Do you have any idea, what else thing I can try out, so that the animation will look smoother? This is the complete code example for demonstration
https://github.com/yccheok/shop-dialog/tree/1a2c06b40327f7a4d6f744f1c3a05a38aa513556
Thank you!
You can get close to your goal by changing didSelectItemAt to this:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
for i in (0..<isExpanded.count) {
if i == indexPath.item {
// toggle selected row
isExpanded[i].toggle()
} else {
// set all other rows to false
isExpanded[i] = false
}
if let c = collectionView.cellForItem(at: IndexPath(item: i, section: 0)) as? CollectionViewCell {
c._description.isHidden = !isExpanded[i]
}
}
collectionView.performBatchUpdates(nil, completion: nil)
}
The default animation / compression of elements when showing and hiding elements in stack views is not always acceptable though. If you want to try to refine it, take a look at this discussion:
Change default StackView animation

Keep UICollectionView position after a reload using dynamic cell's height

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.

Loading data into UICollectionView bunch by bunch

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
}

How do I create a horizontal scrolling UICollectionView in Swift?

How can I make a horizontal scrolling collectionView that fills up cells going across the rows rather than down the columns?
I want there to 5 columns and 3 rows but when there is more than 15 items I want it to scroll to the next page. I'm having a lot of trouble getting this going.
Where you have a reference to your UICollectionViewFlowLayout(), just do:
layout.scrollDirection = .horizontal
Here is a nice tutorial for more info: https://www.youtube.com/watch?v=Ko9oNhlTwH0
Though for historical purposes, consider searching StackOverFlow quickly to make sure this isn't a duplicate.
Hope this helps.
Update:
Your items will fill horizontally first and if there is not enough room within the collectionview going to the right, they will go to next row. So, start by increasing your collectionview.contentsize (should be larger the screen to enable scrolling) and then set your collectionview item (cell) size.
flowLayout.itemSize = CGSize(width: collectionView.contentSize.width/5, height: collectionView.contentSize.height/3)
Option 1 - Recommended
Use custom layouts for your collection view. This is the right way to do this and it gives you a lot of control over how you want your cells to fill the collection view.
Here is a UICollectionView Custom Layout Tutorial from "raywenderlich"
Option 2
This is more like a hackish way of doing what you want. In this method you can access your data source in an order to simulate the style you need. I'll explain it in the code:
var myArray = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]
let rows = 3
let columnsInFirstPage = 5
// calculate number of columns needed to display all items
var columns: Int { return myArray.count<=columnsInFirstPage ? myArray.count : myArray.count > rows*columnsInFirstPage ? (myArray.count-1)/rows + 1 : columnsInFirstPage }
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return columns*rows
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)
//These three lines will convert the index to a new index that will simulate the collection view as if it was being filled horizontally
let i = indexPath.item / rows
let j = indexPath.item % rows
let item = j*columns+i
guard item < myArray.count else {
//If item is not in myArray range then return an empty hidden cell in order to continue the layout
cell.hidden = true
return cell
}
cell.hidden = false
//Rest of your cell setup, Now to access your data You need to use the new "item" instead of "indexPath.item"
//like: cell.myLabel.text = "\(myArray[item])"
return cell
}
Here is this code in action:
*The "Add" button just adds another number to myArray and reloads the collection view to demonstrate how it would look with different number of items in myArray
Edit - Group items into pages:
var myArray = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]
let rows = 3
let columnsInPage = 5
var itemsInPage: Int { return columnsInPage*rows }
var columns: Int { return myArray.count%itemsInPage <= columnsInPage ? ((myArray.count/itemsInPage)*columnsInPage) + (myArray.count%itemsInPage) : ((myArray.count/itemsInPage)+1)*columnsInPage }
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return columns*rows
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)
let t = indexPath.item / itemsInPage
let i = indexPath.item / rows - t*columnsInPage
let j = indexPath.item % rows
let item = (j*columnsInPage+i) + t*itemsInPage
guard item < myArray.count else {
cell.hidden = true
return cell
}
cell.hidden = false
return cell
}
Specify the height of the collection view and cell size. More details below:
Set the constraints of the UICollectionView, pinning the edges. Be sure to specify the UICollectionView's height or constraints so it's clear the cells can only scroll horizontally and not go down to the next line. The height should be the same or slightly larger than the cell height you specify in step 2.
Implement the UICollectionViewDelegateFlowLayout delegate and sizeForItemAt method. Here's a sample sizeForItemAt implementation.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cellWidth = 100
let cellHeight = 30
return CGSize(width: cellWidth, height: cellHeight)
}

Resources