Cant get correct indexPath of collectionView cell - ios

EDIT:
I try to get a correct indexPath of a current cell in a collectionView.
The project is simple: an album of photos and a label with a text. Text in label should be the current indexPath.
Concerning photos - everything is ok. Problem is with indexPath parameter in a label.
First right swipe changes indexPath for + 2 instead of +1: from [0,0] to [0,2] - instead of [0,1]. Right swipes that have been made after this work correctly.
But first left swipe changes the value of indexPath for -3. For example, if indexPath was [0,6] - the first left swipe will change it to [0, 3].
I have made a 10-sec video, representing a problem: https://www.youtube.com/shorts/Qxqr_Q9SDJ8
The buttons are made in order it would be easier to notice changes. They work like right/left swipe. Native swipes give the same result.
The code is this one:
var currentIndexPath: IndexPath = [0,0]
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
var testIndexPath = indexPath
cell.imageView.image = allPhotos[testIndexPath.item].faceCard
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
currentIndexPath = indexPath
}
label.text = "\(currentIndexPath)"

You can override scrollViewDidScroll method and get the visible cell's indexPath when swiped:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
if let testIndexPath = collectionView.indexPathForItem(at: visiblePoint) {
label.text = "testIndexPath: \(testIndexPath)"
}
}

i think you should use collectionview.indexPathForVisibleItems to get the indexPath when collectionView end scroll.
you can check collectionView end scroll by this:
Swift 4 UICollectionView detect end of scrolling

Related

How to reference/update label inside a reusable cell of a collection view?

I have a collection view with a reusable cell. That cell has a background, label and button. I can reference which background is in each cell. I would like to update the text in the label when the button is pressed based on which background that cell has. I am having trouble referencing the cell. let cell = collectionView.cellForItem(at: indexPath) gives me an error. How do I reference this cell?
I am ok if the label gets reset when the user scrolls the collection view.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyColectionCell
cell.CellBG.image = UIImage(named: ButtonBGs[indexPath.row])
cell.CellBG.layer.cornerRadius=10
cell.layer.shadowColor = UIColor.black.cgColor
cell.layer.shadowOffset = CGSize(width: 1, height: 10)
cell.layer.shadowOpacity = 0.3
cell.layer.shadowRadius = 10
cell.layer.masksToBounds = false
cell.Info.tag = indexPath.row
cell.Info.addTarget(self, action: #selector(Info), for: .touchUpInside)
cell.CellText.text = " "
cell.CellText.tag = indexPath.row
return cell
}
#objc func Info(sender: UIButton){
let indexPath = IndexPath(row: sender.tag, section: 0)
let cell = collectionView.cellForItem(at: indexPath) //gives me error "Reference to member 'cellForItem' cannot be resolved without a contextual type"
if((ButtonBGs[indexPath.row])=="bt-tower"){
cell.CellText.text = "New Text"
}
}
I would recommend a different approach to this. The code you provided indicates all of this could be performed in the cell itself.
Give it a property of type String that holds the imagename, assign it inside of func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: and perform the button action/comparison inside the Button #IBAction.
Also, that code manipulating the layer should also be in the cell itself. The cellForItemAt indexpath: function should only provide the data the cell presents.
I am using an array for the label text and keeping the labels alpha at 0. When I need to display it I change the alpha and reload the data self.myCollection.reloadItems(at: [indexPath]) This way only the text for the indexed cell is shown.

How can I detect when the midpoint of a UICollectionViewCell is outside the frame of the UICollectionView?

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

UICollectionView - random cells are selected

I have a Horizontal UICollectionView like the horizontal Calender in iOS.
Paging is enabled but not allowsMultipleSelection.
self.allowsMultipleSelection = false
self.isPagingEnabled = true
There are only 5 cells per page.
let cellSize = CGSize(width: self.view.frame.width / 5 , height: 60)
CollectionView's height is also 60.
didSelectItemAt change background color to .red and didDeselectItem resets it to .white.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
if let cell = cell {
cell.backgroundColor = .red
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
if let cell = cell {
cell.backgroundColor = .white
}
}
The collection view has multiple sections and rows. If I select a cell in the first visible page and scroll, random cells are selected in the next visible pages. That is to say random cells are red in the next pages. I do not want this to be so. I want to select/change color of cells manually.
How can I fix this?
Don't forget that UICollectionView has embedded reusing mechanism, so you should deselect your cells in the method "prepareToReuse" directly inside the cell class.
Take a class-level variable, say index
var index = -1
As you have said that multiple selections are not allowed so the following will do the job for you
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
index = indexPath.item
collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
if let cell = cell {
cell.backgroundColor = indexPath.item == index ? .red : .white
}
}
Whenever user tap on any cell we save the position in index variable and then call the reloadData() to notify collectionView about the change
In cellForRowAt we check if the current cell us selected we set the color to red otherwise white
First, if you want to preserve multiple selection, you have to remember your selected ones in an array since it would get lost if a cell gets recycled and reused. For that use something like a [IndexPath] type). If one selected cell is enough, you could use a non-array version of below code.
var selectedItems: [IndexPath] = []
Then, do your recoloring in your cell's cellForItemAt(:):
cell.backgroundColor = selectedItems.contains(indexPath) ? .red : .white
Your didSelectItemAt delegate function should look like:
if !selectedItems.contains(indexPath) { selectedItems.append(indexPath)}
collectionView.cellForItem(at: indexPath)?.backgroundColor = .red
and your didDeselectItemAt delegate function:
if let index = selectedItems.firstIndex(of: indexPath) { selectedItems.remove(at: index) }
collectionView.cellForItem(at: indexPath)?.backgroundColor = .white
This should actually work. Let me know if we have to do adjustments.

UIcollectionView weird cell recycling behaviour

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

How to trigger didSelectItemAtIndexPath at select center cell once the scroll end in ios swift?

Here UPCarouselFlowLayout is used for carousel scroll. As of now, user must tap a cell in order to trigger collection view didSelectItemAtIndexPath. Is there a way to select the center cell once the scrolling ended automatically?
here is the code i used to carousel:
let layout = UPCarouselFlowLayout()
layout.itemSize = CGSize(width: 211, height: 75)
layout.scrollDirection = .horizontal
layout.spacingMode = UPCarouselFlowLayoutSpacingMode.fixed(spacing: 10)
layout.spacingMode = UPCarouselFlowLayoutSpacingMode.overlap(visibleOffset: 65)
carCollection.collectionViewLayout = layout
here the code used for collection view:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return carCategory.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! carCollectionViewCell
cell.carName.text = carCategory[indexPath.row]
cell.carImage.image = UIImage(named: carCategoryImage[indexPath.row])
cell.carMeters.text = carCategoryMeter[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("selected index::\(indexPath.row)")
}
If you look at ViewController.swift from the Demo included with ** UPCarouselFlowLayout**, you will see the function scrollViewDidEndDecelerating. That is triggered when the scroll stops moving and a cell become the "center" cell.
In that function, the variable currentPage is set, and that's where the labels below the collection view are changed.
So, that's one place to try what you want to do.
Add the two lines as shown here... when the scroll stops, you create an IndexPath and manually call didSelectItemAt:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let layout = self.collectionView.collectionViewLayout as! UPCarouselFlowLayout
let pageSide = (layout.scrollDirection == .horizontal) ? self.pageSize.width : self.pageSize.height
let offset = (layout.scrollDirection == .horizontal) ? scrollView.contentOffset.x : scrollView.contentOffset.y
currentPage = Int(floor((offset - pageSide / 2) / pageSide) + 1)
// add these two lines
let indexPath = IndexPath(item: currentPage, section: 0)
collectionView(self.collectionView, didSelectItemAt: indexPath)
}
You will almost certainly want to add some error checking and additional functionality (like only calling didSelect if the cell actually changed, as opposed to just sliding it a little but remaining on the current cell), but this is a starting point.

Resources