UICollectionView didSelectItemAtIndexPath effects multiple cells - swift - swift

Swift, no storyboard, UICollectionView from xib file
I am running very simple code to update cell's background color but, more then one cells are updating.
override func viewDidLoad(){
super.viewDidLoad()
// getting images from library
images = PHAsset.fetchAssetsWithMediaType(.Image, options: nil)
collectionView.allowsMultipleSelection = false
var nipName2 = UINib(nibName: "CollectionViewCell", bundle:nil)
collectionView.registerNib(nipName2, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "HeaderCell")
var nipName = UINib(nibName: "MyViewCell", bundle:nil)
collectionView.registerNib(nipName, forCellWithReuseIdentifier: "Cell")
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell{
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! MyViewCell
cell.frame.size.width = 60
return cell
}
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView{
switch kind
{
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "HeaderCell", forIndexPath: indexPath) as! CollectionViewCell
return headerView
default:
assert(false, "Unexpected element kind")
}
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
var cell = collectionView.cellForItemAtIndexPath(indexPath)
cell?.backgroundColor = UIColor.redColor()
}
I have about 300 different cells. I want to update just third one but randomly many other cells' background changing.

So collection and table views have a strong concept of view reuse. This allows for large performance gains since it does not have to keep [in your case] 300 cells in memory at the same time.
When you are setting the background color on some cells, those cells have the potential to be reused when scrolling. Since you are not explicitly setting the background upon the view showing, it just uses whatever it currently is.
To fix this just set the color when the view is requested:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell{
// This does not guarantee to be a fresh, new cell, can be reused
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! MyViewCell
cell.frame.size.width = 60
// Explicitly set the background color:
cell.backgroundColor = .whiteColor() // or whatever your default color is
return cell
}
Now it is obvious this will cause additional side-effects. Say you change the background color of a cell to red and scroll away. When you come back you are now setting it back to white. That being said, you need to track which cells (probably by storing their indexes) are selected and set their color appropriately.

Related

Showing/Hiding images in CollectionView Cell

I'm trying to show some images and hide others using ".isHidden" in my CollectionView. But when I scroll down or reload the collectionView they either get reordered incorrectly or hidden entirely.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ReadBookCell", for: indexPath) as! ReadBookCell
let item = readBookArray[indexPath.item]
for star in cell.starImgOutletCollection {
if star.tag <= item.starRating {
star.isHidden = false
} else {
star.isHidden = true
}
}
return cell
}
Edit: Here is my prepareForReuse
override func prepareForReuse() {
super.prepareForReuse()
for star in starImgOutletCollection {
star.isHidden = true
}
}
Assuming your "stars" are 5 image views in a stack view like this:
And, assuming your starRating will be between 0 and 5 (Zero being no rating yet)...
In your cell class, create a reference to the stack view - since your question mentions starImgOutletCollection I'm assuming you are using #IBOutlet (that is, not creating your views via code), so:
#IBOutlet var starsStackView: UIStackView!
Then, still in your cell class, add this func:
func updateStars(_ starRating: Int) {
for i in 0..<starsStackView.arrangedSubviews.count {
starsStackView.arrangedSubviews[i].isHidden = i >= starRating
}
}
Now, in cellForRowAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ReadBookCell", for: indexPath) as! ReadBookCell
let item = readBookArray[indexPath.item]
cell.updateStars(item.starRating)
// do the other stuff to set labels, images, etc
// in the cell
return cell
}
You no longer need the outlet collection for the "star" image views, and you no longer need to implement prepareForReuse().

How can we reload header view inside our collection view

I've been looking for a way to reload my collection view header. So I have a collection view header & a CollectionViewCell that only contains an image. Now when the cell is press, I would like to display the image in the header view without calling collectionView.reloadData(). This is how my didSelectItemAt & didDeselectItemAt method looks like.
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedImage = images[indexPath.item]
let imageCell = collectionView.cellForItem(at: indexPath) as! CollectionCell
imageCell.photoBackgroundView.backgroundColor = .red
collectionView.reloadData()
}
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let imageCell = collectionView.cellForItem(at: indexPath) as! ImagePickerCell
imageCell.photoBackgroundView.backgroundColor = .black
}
So when I select a cell the view turns red, when I deselect it the view turns black. This video here, shows how the behavior without reloading the collectionView. Now here is were I would like to reload the header view.
If I do use collectionView.reloadData(), this is the outcome. How would I be able to reload the header or the collectionView where the header view displays the selected cell image & turns red.
You can try like global instance for that. Like
class YourClass: UIViewController {
/// Profile imageView
var profileImageview = UIImageView()
}
In CollectionView cellforItem assign a imageview. Like
let imageCell = collectionView.cellForItem(at: indexPath) as! CollectionCell
profileImageview = imageCell.imageView
Then when every you selecting collectionViewCell
You can call a function to change a image of imageView. Like
func updateImage() {
profileImageview.image = UIImage()
}
Well I'm trying to understand why sometimes you cast the cell in CollectionCell and sometimes in ImagePickerCell, anyway try to change your functions in these
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! CollectionCell
cell?.photoBackgroundView.backgroundColor = .red
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! CollectionCell
cell?.photoBackgroundView.backgroundColor = .black
}

Why are image views sometimes not appearing in collection view cells?

I just updated to Swift 3.
My collection views were working great before the update. I did the recommended changes to make the compiler happy, but now I'm having this problem.
Image views that are in my custom UICollectionViewCells are simply not appearing anymore. Neither programmatically generated image views nor prototype image views are appearing.
I've given the image views background colors to check if my images are nil. The background colors aren't appearing, so it is safe to assume the image views are not appearing at all.
The cells themselves ARE appearing. Each image view has a label underneath, and the label is displaying properly with the correct text.
The most confusing part is that sometimes the image views DO appear, but there seems to be no rhyme or reason as to why or when.
My code is pretty standard, but I'll go ahead and share it anyway:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return searchClubs.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: HomeCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! HomeCollectionViewCell
cell.barLabel.text = searchClubs[indexPath.row].name
cell.imageCell.image = searchClubs[indexPath.row].image
cell.imageCell.layer.masksToBounds = true
cell.imageCell.layer.cornerRadius = cell.imageCell.frame.height / 2
return cell
}
func feedSearchClubs(child: AnyObject) {
let name = child.value(forKey: "Name") as! String
//Image
let base64EncodedString = child.value(forKey: "Image")!
let imageData = NSData(base64Encoded: base64EncodedString as! String, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters)
let image = UIImage(data:imageData! as Data)
//Populate clubs array
let club = Club.init(name: name, image: image!)
self.searchClubs.append(club)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
Since Xcode 8 you have to call layoutIfNeeded() to calculate size (in your case you need to know cell.imageCell.frame.height) and position from auto layout rules or use a fixed value of cornerRadius.
cell.imageCell.layoutIfNeeded()
cell.imageCell.layer.masksToBounds = true
cell.imageCell.layer.cornerRadius = cell.imageCell.frame.height / 2
OR
cell.imageCell.layer.masksToBounds = true
cell.imageCell.layer.cornerRadius = 5
The imageCell's frame isn't set up ready yet in the cellForItemAt method.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: HomeCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! HomeCollectionViewCell
cell.barLabel.text = searchClubs[indexPath.row].name
cell.imageCell.image = searchClubs[indexPath.row].image
return cell
}
Instead put the setting up of layer on willDisplay since cell.imageCell.frame.height will have its value.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let cell: HomeCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! HomeCollectionViewCell
cell.imageCell.layer.masksToBounds = true
cell.imageCell.layer.cornerRadius = cell.imageCell.frame.height / 2
}

UICollectionView cells randomly disappear on scroll up or reload functions - iOS 9/Swift

After going through the solutions here on STO and other blogs, none seem to truly fix the UICollectionView issues others definitely seem to still have to this day!
Avoiding the reload functions initially loads the CollectionVC with no issues and scrolling down to the bottom no issues as well, but the moment I scroll up, random cells go missing. Also, noticed that the delegate method below gets called multiple times as I am scrolling, is this normal?
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
Using any of the reload functions results in random cells .backgroundView disappearing:
self.collectionView?.reloadSections(NSIndexSet.init(index: 0))
self.collectionView?.reloadData()
self.collectionView?.reloadItemsAtIndexPaths(cellIndexPathArray)
self.collectionView?.reloadInputViews()
Subclassing UICollectionViewFlowLayout with different override options seems to have no effect.
class CustomCollectionFlowLayout: UICollectionViewFlowLayout {
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
//self.scrollDirection = UICollectionViewScrollDirection.Vertical
return true
//invalidateLayoutWithContext(invalidationContextForBoundsChange(newBounds))
//return super.shouldInvalidateLayoutForBoundsChange(newBounds)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
/*
let attributes = super.layoutAttributesForElementsInRect(rect)
print(attributes)
let contentSize = collectionViewContentSize()
return attributes?.filter { $0.frame.maxX <= contentSize.width && $0.frame.maxY < contentSize.height }
*/
let attributes = super.layoutAttributesForElementsInRect(rect)
return attributes
}
}
CollectionVC.swift
private let reuseIdentifier = "cell"
class CollectionVC: UICollectionViewController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
override func viewDidLoad() {
super.viewDidLoad()
//Register cell class
self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
////////////////////////////////
//UICollectionViewDataSource
////////////////////////////////
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return appDelegate.chosenImageArray.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// Configure the cell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)
cell.backgroundColor = UIColor.clearColor()
cell.layer.borderColor = UIColor.whiteColor().CGColor
cell.layer.borderWidth = 1
cell.layer.cornerRadius = 8
let bgView = appDelegate.chosenImageArray.objectAtIndex(indexPath.row) as? UIView
if bgView != nil {
cell.backgroundView = bgView
cell.hidden = false
cell.backgroundView?.hidden = false
}
return cell
}
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return appDelegate.deviceSize
//Tried with different sizes CGSize(100,100) - (200,200) and 0 on insets, no difference
}
////////////////////////////////
//UICollectionViewDelegate
////////////////////////////////
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)
//do various stuff with selected cell and segue to next view, no issues here
}
}
Hope someone can tell me it's just a few settings I need to input, as this issue seems random and maybe just a bug with UICollectionView? Anyway, thank you very much in advance for any help.
Yes this is normal every time you scroll the collection view cell and it becomes visible on screen again the delegates get called.
It is getting re used.
In cellForItemAtIndexPath you have
let bgView = appDelegate.chosenImageArray.objectAtIndex(indexPath.row) as? UIView
this above line.
I guess in appDelegate.chosenImageArray you have all the UIViews.
Possible fix
Do not store the UIView directly on array create a NSObject model class and create required properties in it.
-Try to create a NSObject model class which should have all the relevant properties like UIImage , name etc... as per your requirement.
Store that NSObject class in array. Here NSObject class is your model class which you are going to store in your array.
Now in delegate cellForItemAtIndexPath -
let bgViewModel = appDelegate.yourModelClassArray.objectAtIndex(indexPath.row) as? yourModelClass
cell.backgroundView = bgViewModel.bgView
remove cell.hidden = false & cell.backgroundView?.hidden = false it is not required.

Changing cell background color in UICollectionView in Swift

I am using horizontal collection view to scroll dates. Collection view contain 30 cells. If I select first cell, to indicate the selection, cell background color has been change to brown from default color red. Then, if I select another cell, selected cell color has changed to brown from red. But first cell BGColor remains the same (brown). How can i change to default color by clicking other cell?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath
indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell",
forIndexPath: indexPath) as myViewCell
cell.date_label.text = arr_date[indexPath.item]
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath
indexPath: NSIndexPath) {
var cell = collectionView.cellForItemAtIndexPath(indexPath) as myViewCell
if(cell.selected)
{
cell.backgroundColor = UIColor.brownColor()
}
else
{
cell.backgroundColor = UIColor.redColor()
}
}
You can use the function collectionView with the parameter didSelectItemAtIndexPath
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath
indexPath: NSIndexPath) {
let selectedCell:UICollectionViewCell = myCollectionView.cellForItemAtIndexPath(indexPath)!
selectedCell.contentView.backgroundColor = UIColor(red: 102/256, green: 255/256, blue: 255/256, alpha: 0.66)
}
This creates a constant for the selected UICollectionViewCell, then you just change the background's color
And then for return to the original color when it is deselected, you must use the function collectionView with the parameter didDeselectItemAtIndexPath
func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
let cellToDeselect:UICollectionViewCell = myCollectionView.cellForItemAtIndexPath(indexPath)!
cellToDeselect.contentView.backgroundColor = UIColor.clearColor()
}
And you change the color to the original one!
For example here is the screenshot from this code in a filterApp
UICollectionView example
var selectedIndex = Int ()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
if selectedIndex == indexPath.row
{
cell.backgroundColor = UIColor.green
}
else
{
cell.backgroundColor = UIColor.red
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
selectedIndex = indexPath.row
self.yourCollctionView.reloadData()
}
May be crazy, But it works fine for me...!
You could maintain a copy of the last selected index path, and then in your didSelectItemAtIndexPath compare the index paths to see if they are different. If different, change the colors of the two cells at those index paths as necessary and then copy the new index path over the old.
Edit
Thinking about this again, this should be done with the backgroundView and selectedBackgroundView properties of the cells. After you dequeue a cell you can do the following to let iOS handle the changing.
cell.backgroundView.backgroundColor = [UIColor redColor];
cell.selectedBackgroundView.backgroundColor = [UIColor brownColor];
Best way to handle background colour on selected cell is observing the property isSelected. This handles both selection and un-selection of a cell otherwise it would be tricky to un-select a selected cell at selection of any other cell.
Here is the demonstration of using isSelected property of UICollectionViewCell:
class CustomCollectionViewCell: UICollectionViewCell {
override var isSelected: Bool {
didSet {
contentView.backgroundColor = isSelected ? .red : .white
}
}
}