This question already has an answer here:
Scrolling in UICollectionView selects wrongs cells - Swift
(1 answer)
Closed 5 years ago.
I have a UICollectionViewController which uses a custom cell. There is a function get called when the user taps on a cell, to distinguish the selected cell I change the background color of the cell to the green.
The problem is, when user taps on another cell, the previous one should be unselected, another function will be called. as long as the collectionView is not scrolled it works fine, but when the user scrolls the collectionView and selected one goes out of the visible rect of the screen, my deselect function does not work and there will be two cells with a green background.
It's the demo:
You can see there is a cell with green background at the top, and another one at the end.
Here are methods for selecting and deselecting cells:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? CategoryCollectionViewCell {
cell.selectItem()
}
}
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
for _cell in collectionView.visibleCells {
if let __cell = _cell as? CategoryCollectionViewCell {
__cell.deselectItem()
}
}
if let indexPath = collectionView.indexPathsForSelectedItems {
if indexPath.count > 0 {
if let _cell = collectionView.cellForItem(at: indexPath.first!) as? CategoryCollectionViewCell {
_cell.deselectItem()
}
}
}
return true
}
Try this
let selectedIndexPath : IndexPath
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? CategoryCollectionViewCell {
cell.selectItem()
}
if let preViousSelectedcell = collectionView.cellForItem(at: selectedIndexPath) as? CategoryCollectionViewCell {
preViousSelectedcell.deselectItem()
}
selectedIndexPath = indexPath
}
I guess, I got your issue, simply this happens after scrolling the scroll view, that means the selected become selected and due to reusable cell another cell become selected automatically, well this is a common problem and it happens in UITableView and UIScrollView mostly.
In this view's wherever you have used if condition in datasource and its delegate methods put an else part too, and this will solve your problem.
For example:
if let indexPath = collectionView.indexPathsForSelectedItems {
if indexPath.count > 0 {
if let _cell = collectionView.cellForItem(at: indexPath.first!) as? CategoryCollectionViewCell {
_cell.deselectItem()
}
}
else{
// do something here
}
}
else{
// do something here
}
Hope this may help you.
Related
I know that the UITableViewDataSource method below will notify me when the entire cell has been hidden.
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {}
But I need a method that will notify me when half of the cell is hidden.
In other words, I need a method that will be triggered when the cell has been scrolled to the point that half of the cell is visible and half of it is not.
There's not really an easy way to go about it. But this should do the trick. What you want to do is detect when the collectionView scrolls then determine which cells are fully visible and which cells have their center "off screen". If a cell was recently fully visible, but the center is no longer on screen, we can assume that that cell is roughly 50% visible.
For this to work you need to keep track of which cells were recently fully visible, then as a cell's center moves off screen remove the cell from the list. If you don't remove the cell from the list then it would be processed repeatedly since its center will be off screen on more than one iteration of scroll detection.
class ViewController: UICollectionViewController {
// ...
var recentlyFullyVisibleCells: Set<IndexPath> = .init()
func processOffScreenCell(at indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) else { return }
cell.backgroundColor = .red
}
func processFullyVisibleCell(at indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) else { return }
cell.backgroundColor = .white
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let latestFullyVisibleCells = collectionView.visibleCells.filter { cell in
// Get cells that are fully visible
let rect = collectionView.convert(cell.frame, to: collectionView.superview)
return collectionView.frame.contains(rect)
}
.compactMap { cell in
// Convert to IndexPath
collectionView.indexPath(for: cell)
}
latestFullyVisibleCells.forEach { indexPath in
processFullyVisibleCell(at: indexPath)
}
collectionView.visibleCells.filter { cell in
// Get cells whose center are not on screen
let rect = collectionView.convert(cell.frame, to: collectionView.superview)
return !collectionView.frame.contains(CGPoint(x: rect.midX, y: rect.midY))
}
.compactMap { cell in
// Convert to IndexPath
collectionView.indexPath(for: cell)
}
.reduce(into: Set<IndexPath>()) { result, indexPath in
// Convert to Set
result.insert(indexPath)
}
.intersection(recentlyFullyVisibleCells) // Only keep cells that were recently fully visible
.forEach { indexPath in
processOffScreenCell(at: indexPath)
recentlyFullyVisibleCells.remove(indexPath)
}
recentlyFullyVisibleCells.formUnion(latestFullyVisibleCells)
}
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
recentlyFullyVisibleCells.removeAll()
}
// ...
}
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
}
}
I have a UICollectionView with flow layout, about 140 cells each with a simple UITextView. When a cell is recycled, I pop the textView onto a cache and reuse it later on a new cell. All works well until I reach the bottom and scroll back up. At that point I can see that the CollectionView vends cell number 85, but then before cell 85 is displayed it recycles it again for cell 87 so I now lose the content of the cell I had just prepared.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FormCell", for: indexPath) as! FormCollectionViewCell
let textView = Cache.vendTextView()
textView.text = "\(indexPath.row)"
cell.addSubview(textView)
cell.textView = textView
return cell
}
And on the UIcollectionViewCelC
override func prepareForReuse() {
super.prepareForRuse()
self.textView.removeFromSuperView()
Cache.returnView(self.textView)
}
I would have thought that after cellForItemAtIndexPath() was called, it would then be removed from the reusable pool of cells but it seems it is immediately being recycled again for a neighbouring cell. maybe a bug or I am possibly misunderstanding the normal behaviour of UICollectionView?
As I understand it, what you're trying to do is just keep track of cell content - save it when cell disappears and restore it when it comes back again. What you're doing can't work well for couple of reasons:
vendTextView and returnView don't take indexPath as parameter - your cache is storing something and fetching something, but you have no way of knowing you're storing/fetching it for a correct cell
There's no point in caching the whole text view - why not just cache the text?
Try something like that:
Have your FormCollectionViewCell just have the text view as subview, and modify your code like so:
class YourViewController : UIViewController, UICollectionViewDataSource, UICollectionViewDelegate
{
var texts = [IndexPath : String]()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FormCell", for: indexPath)
if let formCell = cell as? FormCollectionViewCell {
cell.textView.text = texts[indexPath]
return cell
}
}
func collectionView(_ collectionView: UICollectionView,
didEndDisplaying cell: UICollectionViewCell,
forItemAt indexPath: IndexPath)
{
if let formCell = cell as? FormCollectionViewCell {
{
texts[indexPath] = formCell.textView.text
}
}
}
I have inserted a collectionView inside my tableViewCell. Tableview contains the list of categories and the collectionView contains all the product. How can I have a different number of items in the collectionView based off of which table view row was selected? I've tried storing the selected table view row and using that to define the number of items to be returned however it either crashes with no error code, tells me the value is nil or just does not display any clitems in the collectionView. Any help would be greatly appreciated. Thank you for your time.
Below is my code:
My Custom table view cell:
extension ExpandableCell: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let toReturn = categoryItems.count
return counter
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionCell", for: indexPath) as! CustomCollectionViewCell
//What is this CustomCollectionCell? Create a CustomCell with SubClass of UICollectionViewCell
//Load images w.r.t IndexPath
print(self.selectedCategory.description)
let newArray = starbucksMenu[selectedCategory]
//cell.image.image = UIImage(named: (allItems[selectedCategory]?[indexPath.row])!)
cell.label.text = categoryItems[indexPath.row]
//cell.layer.borderWidth = 0.1
return cell
}
My table view delegate method:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(indexPath.row)
word = indexPath.row
guard let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell
else { return }
switch cell.isExpanded
{
case true:
self.expandedRows.remove(indexPath.row)
self.selectedCategory = ""
case false:
self.expandedRows.insert(indexPath.row)
}
self.selectedCategory = categories[indexPath.row]
print(self.selectedCategory)
//self.array = starbucksMenu[starbucksMenuCategories[indexPath.row]]!
//self.collectionView.reloadData()
cell.menuItems = allItems[selectedCategory]!
cell.categoryItems = allItems[selectedCategory]!
cell.isExpanded = !cell.isExpanded
self.itemsArray = allItems[selectedCategory]!
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
I've tried many things, I've tried adding the items in an array and returning the count (displays nothing). I have a dictionary with the necessary items so I've also tried returning allItems[selectedCategory]?.count and this always returns an error, I believe selectedCategory has no value once this is called.
Make a for loop of collection view item with appropriate operation Between beginUpdate() and endUpdate()
Right now I have a list of scrolling usernames using a Collection View of buttons. But I’d like to add overlapping delete buttons to each row. They'd need to be attached to the name buttons and scroll with them.
How can I add these buttons to my CollectionView?
(Also I'd like to skip the delete button on the first row for obvious reasons)
Current Code:
//Add the cells to collection
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: UsernameCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! UsernameCollectionViewCell
cell.usernameLabel.text = userNames [indexPath.row]
return cell
}
//Upon Selecting an item
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if (indexPath.row == 0){
self.performSegueWithIdentifier("newUserSegue", sender: self)
}
else {
sendData(userNames[indexPath.row])
self.dismissViewControllerAnimated(true, completion: nil)
}
}
Got it working! Here's how:
I added a button to the cell in the Storyboard.
Connected an outlet to the UICollectionViewCell class.
Edited view controller code to:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: UsernameCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! UsernameCollectionViewCell
cell.usernameLabel.text = userNames [indexPath.row]
cell.deleteButton?.layer.setValue(indexPath.row, forKey: "index")
cell.deleteButton?.addTarget(self, action: "deleteUser:", forControlEvents: UIControlEvents.TouchUpInside)
// Remove the button from the first cell
if (indexPath.row == 0){
var close : UIButton = cell.viewWithTag(11) as! UIButton
close.hidden = true
}
return cell
}
func deleteUser(sender:UIButton) {
let i : Int = (sender.layer.valueForKey("index")) as! Int
userNames.removeAtIndex(i)
UserSelectCollection.reloadData()
}
Many thanks to JigarM for his examples on GitHub:
https://github.com/JigarM/UICollectionView-Swift
Why not create custom UICollectionViewCell in IB and just add button to it ?
Register it to your collectionView with :
- registerNib:forCellReuseIdentifier:
You can use delegate or notification to process button tap.