Replace CollectionViewCell with another CollectionViewCell? - ios

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!

Related

UICollectionView triggers pagination without scrolling

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
}

Invalid update: invalid number of items in section 0. datasource updated first

I have read and looked over the several posts that mentions this. I tried my best to follow along but I am still having an issue.
self.items.append(contentsOf: newItems)
let newIndexPath = IndexPath(item: self.items.count - 1, section: 0)
DispatchQueue.main.async {
self.collectionView.insertItems(at: [newIndexPath])
}
Items is the array where I have all of the items, I am adding newItems. I did a print and I know there is new items. So for newIndexPath it would be the next items.count - 1. I tried using self.items.count - 1 and self.collectionView.numberOfItems(inSection: 0)
Are you using below delegates methods? Your code is very limited and doesn't give much information. However, I think you are trying to update the number of Items in section of collectionView without reloading the collectionView data.
Better to do below:
extension ViewController : UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
else { fatalError("Unexpected cell in collection view") }
cell.item = self.items[indexPath.row]
return cell
}
If you are updating the items array, then append the new item and reload the collectionView will update the list. You can do as below:
self.items.append(contentsOf: newItems)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
No need to insert new item in collectionView.

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

Get next cell in UIcollectionView in Swift

I need to get the next cell inside cellForItem within a collection view so that I can update a view object. When I try the following below it doesn't work. I've also tried indexPathForVisibleItems passing in indexPath.row + 1 and the produces an index out of range error.
let index = IndexPath(row: indexPath.row + 1, section: indexPath.section)
if let nextCell = collectionView.cellForItem(at: index) as! MKRCell {
nextCell.setupWaitView(time: timeToWait)
nextCell.waitViewHeightConstraint.constant = 80
nextCell.waitView.alpha = 1
nextCell.waitView.isHidden = false
}
Is this possible to achieve or will I need to do this via another way?
Thanks
No, it is not possible to get the cell object before initialization in cellForItemAt but here
you can receive the call before displaying the cell from UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView,willDisplay cell: UICollectionViewCell,forItemAt indexPath: IndexPath) {
if let myCell = cell as? MKRCell {
}
}
AND
If you want to set up the cell you have to setup view in the UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
}
You should update the cell in:
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
Remember to modify only the cell you'll be returning from this method. Other cells might not have exist at that moment.
Alternatively you can keep a weak reference to the cell and update it when needed.

Collection View delete item in depending on its location

I have created a collection view that pulls from an array of images (8 right now, but user can add more). I originally was using a scrollview, but found it easier with a collection, and thanks to this great community, went to a collection view. I need to find the indexPath to delete an item at a given point. So here is some code I have so far, but I am new to this specifically. Here is some code I currently have.
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var myCollectionView: UICollectionView!
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! UserFeedCollectionViewCell
cell.myImage.image = UIImage(named: imageArray[indexPath.row])
cell.myImage.layer.cornerRadius = 12.0
cell.myImage.clipsToBounds = true
return cell
}
//delete item at current item - 2
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.row > 2 {
myCollectionView.deleteItems(at: [])
}
}
Hope this will help and feel free to ask in the comments if you have a question.
Edit: Paging is enabled, and it is horizontal scrolling, and each image takes up the whole cell.
How about keeping an array of tapped on image indices?
Define a variable for the indexes at the top:
var selected = [IndexPath]()
Then implement didSelectItemAt as:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selected.append(indexPath)
if selected.count > 2 {
let ndx = selecte.count - 3
let twoBack = selected[ndx]
myCollectionView.deleteItems(at:[twoBack])
}
}
The above would work fine for the first selection, but at that point, you would need to figure out how you handle the next selection - whether you wipe the selected array and start over, or need to keep track of further selections to handle the next input.
Depending on how you want to proceed, the selected array would either need to be wiped or to be modified to remove the item that was deleted.

Resources