UICollectionView triggers pagination without scrolling - ios

I want to achieve a smooth pagination experience in UICollectionView. The idea is simple: trigger an API call when the last cell of collectionView is reached.
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.row == dataGroup.data.count-1{
print("last cell visible" )
self.isPaginating = true
Service.shared.fetchData(page: self.page) { rawData, error in
if let error = error{
print("error while fetching app groups", error)
return
}
sleep(2)
if let data = rawData{
self.dataGroup?.data += data.data
DispatchQueue.main.async {
self.collectionView.reloadData()
}
self.page+=1
self.isPaginating = false
}
}
}
//returning cells
}
Even though I don't scroll the collectionView, if indexPath.row == dataGroup.data.count-1 becomes true and the API call is triggered. What can be the cause?
PS. I observe the data.count and pagination stops when all the data is loaded. I just removed it to simplify the code in the question.

Try to move your code in
collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath)
https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/1618087-collectionview
And add check like :
if collectionView.indexPathsForVisibleItems.lastObject.row == dataGroup.data.count-1. {
do something
}

Related

Replace CollectionViewCell with another CollectionViewCell?

A rather straightforward question - I have a collectionView with a couple of different cells, and I'd like to replace one of those cells with another cell.
Since func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { is how I set my cells at the beginning, I don't suppose I can use another one of those. I have tried this
if indexPath.item == assignDotCell {
return dotCell
}
and then, when the action happen, I set assignDotCell to the indexnumber I want (it's initiated to 1000), and then tried
self.collectionView.assignDotCell = self.numberIWant
self.collectionView.performBatchUpdates({
self.collectionView.reloadItems(at: [IndexPath (item: 5, section: self.activeRow)])
}, completion: nil)
but of course that doesn't work. I would prefer to not call the entire cellForItemAt function again, since it would hide some of my other cells. Can it even be called?
Thankful for all leads!
EDIT: I had an error in how I assigned the cells inside the function! In case anyone else has the same problem, here's how it looks now.
public var updateCell = false
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let dotCell = collectionView.dequeueReusableCell(withReuseIdentifier: dotID, for: indexPath) as! DotsCollectionViewCell
if updateCell {
for index in 0..<dotArray.count {
if indexPath.item == dotArray[index] {
return dotCell
}
}
}
}
and to call it:
self.collectionView.updateCell = true
self.collectionView.performBatchUpdates({
self.collectionView.reloadItems(at: [IndexPath (item: 5, section: self.activeRow)])
}, completion: nil)
Thank you for the help!

Refresh collectionView on viewdidload after retrieving UserDefaults

I have a collection view, and you can select the items in it and toggle them on and off by changing the background colour. The cells are toggled on/off thanks to a boolean I have in an arrow I made for all of the cells. I have saved the bool value but when I try to write them back into the array and use collectionView.reloadData()the app crashes. My collectionViewcode is:
extension OLLViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { //set the amount of items in the CollectionView to the amount of items in the OLLData dictionary
return OLLData.OLLCasesList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { //set each cell to a different mamber of the dict.
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OLLCell", for: indexPath) as! OLLCell
cell.imageView.backgroundColor = OLLData.OLLCasesList[indexPath.item]._isSelected ? UIColor.orange : UIColor.clear //change colour if selected
let image = OLLData.OLLCasesList[indexPath.item]._imageName
cell.label.text = image
cell.imageView.image = UIImage(named: image)
let savedIsSelected = defaults.bool(forKey: Key.isSelected)
OLLData.OLLCasesList[indexPath.item]._isSelected = savedIsSelected
//collectionView.reloadData() //when uncommented it crashes the app
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { //detect if case selected and reload CollectionView
let caseName = OLLData.OLLCasesList[indexPath.item]._imageName
print(caseName, OLLData.OLLCasesList[indexPath.item]._isSelected)
OLLData.OLLCasesList[indexPath.item]._isSelected = !OLLData.OLLCasesList[indexPath.item]._isSelected
defaults.set(OLLData.OLLCasesList[indexPath.item]._isSelected, forKey: Key.isSelected)
collectionView.reloadItems(at:[indexPath])
collectionView.reloadData()
if OLLData.OLLCasesList[indexPath.item]._isSelected == true { //if the item is selected, add to selectedCases array
selectedCases.append(OLLData.OLLCasesList[indexPath.item]._id)
selectedCaseNames.append(OLLData.OLLCasesList[indexPath.item]._imageName)
print(selectedCases, selectedCaseNames) //debugging
numberOfSelectedCases.text = String(selectedCases.count)
}
else if OLLData.OLLCasesList[indexPath.item]._isSelected == false { //remove from selectedCases array
selectedCases.removeAll(where: { $0 == OLLData.OLLCasesList[indexPath.item]._id })
selectedCaseNames.removeAll(where: { $0 == OLLData.OLLCasesList[indexPath.item]._imageName })
print(selectedCases, selectedCaseNames) //debugging
numberOfSelectedCases.text = String(selectedCases.count)
}
}
._isSelectedis the boolean that says whether the cell is 'toggled'.
Any ideas would be greatly appreciated.
First of all, uncommenting that line will produce an infinite loop. cellForRowAt happens because the collection view is reloading, so calling a refresh while the collection view is refreshing is no good.
So your issue is that you don't know how to display selected cells in your collection view, right?
Here's a function that fires right before the collection view is about to display a cell:
func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath)
{
<#code#>
}
Inside this function, you should:
Cast cell into your OLLCell (safely if you want to be thorough)
Look at your data and see if the cell should be selected OLLData.OLLCasesList[indexPath.item]._isSelected
Ask your casted cell to change its colors/UI/appearance according to your ._isSelected boolean
Step 3 has a VERY important caveat. You should be changing the UI when ._isSelected is false AND when it's true. Because the collection view reuses cells, old UI state will randomly recur. So setting it every time is a good way to ensure the behavior you want.
Here's an example:
func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath)
{
//Cast the vanilla cell into your custom cell so you have access
//to OLLCell's specific functions and properties.
//Also make sure the indexPath falls in the indices of your data
if let myCastedCell = cell as? OLLCell,
0 ..< OLLData.OLLCasesList.count ~= indexPath.item
{
myCastedCell.imageView.backgroundColor = OLLData
.OLLCasesList[indexPath.item]._isSelected
? UIColor.orange
: UIColor.clear
}
}

UIcollectionview's cell item hidden or not even being dequeue

I need to display an arrays of users on a UICollectionView with 1 cell 1 user info.
Based on the user online status, will determine on whether to show the online green dot on each cell.
My main problem now is when I scroll down to display more users, the online green dot some disappear and some will still remain. But when more cells being reuse, the green dot will randomly display on other cells. What should I implement to keep all the green dots display properly?
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if user.online != "online"{
cell2.onlineIcon.isHidden = true
}
}
Change your cellForItemAt code with below:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if user.online != "online"{
cell2.onlineIcon.isHidden = true
} else {
cell2.onlineIcon.isHidden = false
}
}
As you correctly mentioned cells are reused.
When you set a value of a UI element in an if clause you have to add an else clause to set a default value
if user.online != "online" {
cell2.onlineIcon.isHidden = true
} else {
cell2.onlineIcon.isHidden = false
}
or much simpler in one line
cell2.onlineIcon.isHidden = user.online != "online"

UICollectionView not loading fully until I scroll

I have a collection view that I want to display hourly weather in. I seem to have a problem with loading the cell, and for some reason scrolling forwards and then back loads the cell fully. Before I scroll the collection view, all of the constraints do not work and one label doesn't show it's info.
Before scrolling
After scrolling (this is how I want the cells to look like)
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return newhourlyWeather.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! hourlyWeatherCell
// Configure the cell
let hWeather = newhourlyWeather[indexPath.row]
if let HourlyTemp = hWeather.temperatureh {
cell.temperatureHLabel.text = "\(HourlyTemp)ยบ"
}
if let HourlyTime = hWeather.convertedTimeH {
cell.timeHLabel.text = "\(HourlyTime)"
}
if let HourlyRain = hWeather.precipProbabilityh {
cell.rainChanceHLabel.text = "\(HourlyRain)%"
}
cell.iconhView.image = hWeather.iconh
return cell
self.collectionView.reloadData()
}
Seems like you populate your cells asynchronously, if so then add a mycollectionview.reloadData() at the end.
I fixed the problem by adding cell.layoutIfNeeded() before the return cell. Everything loaded as expected without any scrolling!
I had the same issue and I solved calling cell.layoutIfNeeded() inside collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath).
Also if you are using UICollectionViewDiffableDataSource and you are applying the snapshot inside the viewWillAppear, you need to add some delay to make it work correctly like this:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
// apply here the snapshot
}

UICollectionView Enlarge cell on selection

I want to resize a particular cell in my collection view when it is selected. I found how to do so from previous posts, but ran into a problem. I found while debugging that the selection lagged behind by one, but I am not sure why.
For example, if I select one cell by tapping it, nothing happens. If I select another cell after, then the first cell I selected is enlarged. If I select a third cell, the second is enlarged. And so on.
This is how I implemented it, and only once cell is ever enlarged at the same time like I want:
var selectedIndexPath: NSIndexPath!
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
if selectedIndexPath != nil { //We know that we have to enlarge at least one cell
if indexPath == selectedIndexPath {
return CGSize(width: self.gallery.frame.size.width, height: self.gallery.frame.size.width)
}
else {
return CGSize(width: self.gallery.frame.size.width/3.0, height: self.gallery.frame.size.width/3.0)
}
}
else {
return CGSize(width: self.gallery.frame.size.width/3.0, height: self.gallery.frame.size.width/3.0)
}
}
func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
if selectedIndexPath != nil && selectedIndexPath == indexPath
{
selectedIndexPath = nil //Trigger large cell set back to normal
}
else {
selectedIndexPath = indexPath //User selected cell at this index
}
collectionView.reloadData()
}
I found while debugging that the selection lagged in didDeselectItemAtIndexPath in the way I described above, but am not sure why.
The problem is that you are're using func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath).
Pay attention to this phrase: didDeselectItemAtIndexPath.
didSelectItemAtIndexPath is a way to go.

Resources