On iOS 10.0, UICollectionView pre-fetches cells by default. This leads to cells that are prepared for being shown on screen, but are hidden. This question describes it really well.
The following code will successfully deselect an index path when its cell is either visible or does not exist at all. If the cell exists and is hidden, the index path will be deselected, but the cell becomes stuck in the selected state until it is reused.
collectionView!.deselectItem(at: indexPath, animated: false)
This problem does not exits on iOS 9 or when pre-fetching is disabled with isPrefetchingEnabled = false on iOS 10.0.
Is this a bug in UICollectionView or am I misunderstanding how deselectItem is supposed to work?
Here is the full code of a UICollectionViewController subclass that demonstrates this behaviour with the following steps:
Tap on a cell, so that it becomes selected (red)
Scroll the cell slightly off-screen
Tap the "Deselect Cell" button
Scroll the cell back on screen
Observe how it still looks selected
Tap on another cell
Observe how both cells look selected
Scroll the first cell far off-screen and back again
Observe how the first cell finally does not look selected
import UIKit
private let reuseIdentifier = "Cell"
class CollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
let button = UIButton(frame: CGRect(x: 10, y: 30, width: 360, height: 44))
button.backgroundColor = #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1)
button.setTitleColor(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1), for: .normal)
button.setTitleColor(#colorLiteral(red: 0.05882352963, green: 0.180392161, blue: 0.2470588237, alpha: 1), for: .highlighted)
button.setTitle("Deselect Cell", for: .normal)
button.addTarget(self, action: #selector(CollectionViewController.buttonPress), for: .touchUpInside)
view.addSubview(button)
}
func buttonPress() {
for indexPath in collectionView!.indexPathsForSelectedItems ?? [IndexPath]() {
let cell = collectionView!.cellForItem(at: indexPath)
NSLog("Deselecting indexPath: %#, cell: %#", indexPath.description, cell?.frame.debugDescription ?? "not visible")
collectionView!.deselectItem(at: indexPath, animated: false)
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 300
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
cell.selectedBackgroundView = UIView(frame: cell.bounds)
cell.selectedBackgroundView!.backgroundColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)
return cell
}
}
As far as I can tell this is a bug in UICollectionView, and I've opened a Radar myself. You might want to do so as well to apply more pressure.
Even before prefetching, collection view didn't bother deselecting cells that weren't visible. Even cellForItemAtIndexPath: states that it returns nil for cells that are not visible.
But before prefetching, as soon as a cell left the content view it was added to the reuse pool, and when you scrolled back you got a reused cell that had its selected state reset.
Now, that cell remains loaded and wont reset until it's reused, by scrolling further away, beyond the prefetching area.
To fix this you can either set prefetchingEnabled to NO and lose its benefits, or update selection state whenever a cell appears -
- (void)collectionView:(UICollectionView *)collectionView
willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
if (!cell.isSelected) {
return;
}
if ([[collectionView indexPathsForSelectedItems] containsObject:indexPath]) {
return;
}
cell.selected = NO;
}
This doesn't seem to diminish performance.
Related
I'm having an issue in my cellForItemAtIndexPath where I am setting an image to my cell's UIButton but every time I scroll the collectionView's cells, it's placing the image on top of the already set image again and again. I can tell because the shadow of the image is getting thicker and thicker. I'm pulling the images from an array that I created of image literals in that swift file and the correct images are loading so there's no problem there. I'm sure this is a simple fix for most but I can't seem to find an answer anywhere.
Image of my cellForItemAtIndexPath function
My app running before I scroll
App after scrolling a bit
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! HomeViewCell
collectionView.bounces = false
let imageNumber = indexPath.item
let collectionImage: UIButton = {
let image = UIButton()
image.setImage(collectionImageArray[imageNumber].withRenderingMode(.alwaysOriginal), for: .normal)
image.contentMode = .scaleAspectFit
image.addTarget(self, action: #selector(handleCollectionTap), for: .touchUpInside)
return image
}()
collectionImage.imageView?.image = collectionImageArray[imageNumber]
cell.addSubview(collectionImage)
collectionImage.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
collectionImage.centerXAnchor.constraint(equalTo: cell.centerXAnchor).isActive = true
collectionImage.centerYAnchor.constraint(equalTo: cell.centerYAnchor).isActive = true
print(imageNumber)
return cell
}
If anyone else comes across this issue (or maybe I'm the only dumb one), the problem was that I should have created my UIButton, added the subview, and constrained it inside of the cell class and from the cellForItem AtindexPath method set the image and target handler like this.
class HomeViewCell: UICollectionViewCell {
let collectionImage: UIButton = {
let image = UIButton(type: .custom)
image.contentMode = .scaleAspectFit
return image
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(collectionImage)
collectionImage.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
collectionImage.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
collectionImage.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! HomeViewCell
collectionView.bounces = false
let imageNumber = indexPath.item
let image = collectionImageArray[imageNumber].withRenderingMode(.alwaysOriginal)
cell.collectionImage.setImage(image, for: .normal)
cell.collectionImage.addTarget(self, action: #selector(handleCollectionTap), for: .touchUpInside)
print(imageNumber)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
#objc func handleCollectionTap() {
let layout = UICollectionViewFlowLayout()
let cardViewer = CardViewerController(collectionViewLayout: layout)
present(cardViewer, animated: true, completion: nil)
}
Everything is running smoothly now! :)
I want to get a deleteButton that is a delete image to sit on the outside of a collectionViewCell (it can be partially inside).
I looked at other SO answers and they said just play with the button's frame's Rect's x & y values which I did. When I set the x & y values to 0,0 I get:
class CustomCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
var deleteButton: UIButton!
var deleteButtonImg: UIImage!
override func awakeFromNib() {
super.awakeFromNib()
deleteButton = UIButton(frame: CGRect(x: 0, y: 0, width: frame.size.width/4, height: frame.size.width/4))
deleteButtonImg = UIImage(named: "delete-icon")!.withRenderingMode(.alwaysTemplate)
deleteButton.setImage(deleteButtonImg, for: .normal)
deleteButton.tintColor = UIColor.red
contentView.addSubview(deleteButton)
}
When I try to set the the Rect's x & y to -10,-10 the deleteButton gets clipped inside the cell's imageView. I don't have clipsToBounds set.
deleteButton = UIButton(frame: CGRect(x: -10, y: -10, width: frame.size.width/4, height: frame.size.width/4))
I even tried setting clipToBounds to false but I get the same effect as pic # 3.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.imageView.image = myImageArray[indexPath.row]
cell.imageView.clipsToBounds = false
return cell
}
Where am I going wrong?
The problem was inside storyboard I had a:
CollectionView
-CustomCell (type collectionViewCell)
-imageView
Under the Attributes Inspector, in the Drawing section, all 3 of these have the property clipsToBounds.
I overlooked the fact that I had clipsToBounds set to false (unchecked) on the imageView but set to true (checked) on the CustomCell and the CollectionView.
If anyone else has this problem make sure you uncheck clipsToBounds on the collectionView, collectionViewCell, and the imageView.
okay u can do like below, first create a xib file and replace default view with collection view cell and set up like below,
in the above image set the class name to CustomCell (In my case) and place image view (green ) and button (blue).
and in View controller,
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//register the nib to collectionview
self.aColectionView.register(UINib(nibName: "CustomCell", bundle: nil), forCellWithReuseIdentifier: "CUSTOM_CELL")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 40
}
//cell creation as u are doing
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell:CustomCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CUSTOM_CELL", for: indexPath) as! CustomCell
//set the image for custom cell
return cell
}
//return the required size
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 200, height: 150)
}
and output like below,
and in custom cell class, u add any other features like action for a button and other views related code
class CustomCell:UICollectionViewCell {
#IBOutlet weak var deleteButton: UIButton!
#IBOutlet weak var imageView: UIImageView!
//other view related code
}
I know this will be something simple; however, I have created a UICollectionView that displays just some colours for now. When a cell is tapped I want it to perform a segue with the cell number that was tapped.
At the moment, if I tap a cell nothing happens. If I touch the same cell again nothing happens, but if I touch any other cell the segue occurs successfully and the cell clicked gets passed on, albeit with the first cells touched number.
It is in a navigation stack. Here is my code:
//Everything Collection View
//header text
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "SectionHeader", for: indexPath as IndexPath) as! SectionHeaderPractice
header.headerLabel.text = "SELECT A TOPIC..."
return header
}
//number of cells
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
//height and width of cell
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cellWidth = (view.frame.width/2)-20
let cellHeight = view.frame.height/4
return CGSize(width: cellWidth, height: cellHeight)
}
//cell content
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let content = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as UICollectionViewCell
//set text
let desc = content.viewWithTag(3) as! UILabel
desc.text = shieldTextArray[(indexPath as NSIndexPath).row]
//set color scale
let score = scoreArray[(indexPath as NSIndexPath).row]
let redC = 2*score
let greenC = 2*(1-score)
content.backgroundColor = UIColor.init(red: CGFloat(redC), green: CGFloat(greenC), blue: 0.45, alpha: 1)
return content
}
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
//sets variable to clicked cell
//cellClicked = indexPath.row
//changes background colour on clicking
//let content1 = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as UICollectionViewCell
//content1.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 1)
//Performs segue
//print(cellClicked)
performSegue(withIdentifier: "practiceSegue", sender: self)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(10,10,10,10)
}
//End Everything Collection View
Any pointers appreciated.
Your issue is with this line didDeselectItemAt, make sure you use didSelectItemAt.
didDeselectItemAt is the reason why you have to tap twice for segue, you select the cell then deselect the cell, once the cell is deselected, your code perform segue.
I have a simple UICollectionView written in Swift. My sectionInsets used to be 0, 35, 0, 35 which gave the entire collectionView margins left end right. My minimumLineSpacing is set to default 10, which means all of my cells have a margin of 10 between each other. All good.
Then I divided the UICollectionView into two sections. The first section serves to hold a cell that acts as a prompt to create a new cell (which will end up in section 2). The second section holds all other cells (the actual content).
My problem is that ever since I added the second section, the spacing between the one static cell in section 1 and the first cell in section 2 isn't 10 anymore but 35+10. I'm now trying to get this particular spacing back to 10.
My thought was to add an if condition to identify whether it's section 1 or 2 and set the sectionInsets accordingly. I'm stuck however as to where and how exactly I should do this. I don't seem to be able to call sectionInsets or FlowLayout from my View Controller at e.g. cellForItemAtIndexPath. Should I do it in my CustomCollectionViewFlowLayout subclass? Here is the code that I have in there.
- (void)awakeFromNib
{
self.itemSize = CGSizeMake(305.0, 407.0);
self.minimumInteritemSpacing = 20.0;
self.minimumLineSpacing = 10.0;
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sectionInset = UIEdgeInsetsMake(0, 35, 0, 35);
}
And this is the code for my sections in my view controller:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let firstCell = collectionView.dequeueReusableCellWithReuseIdentifier("createCell", forIndexPath: indexPath) as! CreateCollectionViewCell
firstCell.imageView.image = UIImage(named: "puppy3")
return firstCell
} else {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("mainCell", forIndexPath: indexPath) as! MainCollectionViewCell
cell.imageView?.image = self.imageArray[indexPath.row]
return cell
}
}
UPDATE:
I figured it out. It works for me by adding two variables for sectionInsets of type UIEdgeInsets in my view controller and then adding this function:
//Set custom insets per section
func collectionView(collectionView: UICollectionView,layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
if section == 0 {
return sectionInsets1
} else {
return sectionInsets2
}
}
The natural direction for a UICollectionView to scroll when set horizontally is from left to right. Is there any way to reverse this? The simpler the better.
I'm not sure exactly what you mean -- if you set the scrolling to horizontal, it scrolls equally well, left and right. If you want it to start it from the right side, you can use this method:
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:self.theData.count - 1 inSection:0] atScrollPosition:UICollectionViewScrollPositionRight animated:NO];
This assumes that you have 1 section, and the array populating the collection view is called theData.
Swift4 solution
in cellForItemAt collectionView function
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = categoryBook.dequeueReusableCell(withReuseIdentifier: "HomeCategoryCell", for: indexPath) as! HomeCategoryCell
collectionView.transform = CGAffineTransform(scaleX:-1,y: 1);
cell.transform = CGAffineTransform(scaleX:-1,y: 1);
}
but this solution in some cases did not work properly if it dose not you can use ColletctionView scrollToItem method and you can implement it after you reload the data .
self.YourCollectionView.reloadData()
self.YourCollectionView.scrollToItem(at: NSIndexPath(item: self.YourObjectListData.count - 1, section: 0) as IndexPath, at: .right, animated: false)
Same thing for swift:
collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: theData.count - 1, inSection: 0), atScrollPosition: .Right, animated: false)
Use This Extention
extension UICollectionViewFlowLayout {
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true //RETURN true if collection view needs to enable RTL
}
}
I have found using xCode 12.4 with an app that targets iOS 12 that this there seems to be no need to load the items in a different order or do any transforms. The only issue has to do with the initial scroll position. So all I need to do to get things working in both RTL and LTR is the following:
collectionView.reloadData {
if self.collectionView.effectiveUserInterfaceLayoutDirection == .rightToLeft {
self.collectionView?.scrollToItem(at: IndexPath(row:0, section:0), at: .right, animated: false)
}
}