Reynaldo Aguilar Reynaldo Aguilar - 2 months ago 28
Swift Question

Issue related to UICollectionView cell selection when invalidate its layout

I have a simple screen with two collection views. What I want is that when I select one item in the first CV, I want to show a selection indicator and show this item multiple times in the second CV, like shown in the following screenshot (ignore the transparency in the image):

enter image description here

Here is my code (It's a bit long but it's very simple):

class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
@IBOutlet weak var cv1: UICollectionView!
@IBOutlet weak var cv2: UICollectionView!

override func viewDidLoad() {
super.viewDidLoad()

cv1.dataSource = self
cv1.delegate = self
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

let layout = cv1.collectionViewLayout as! UICollectionViewFlowLayout
var size = layout.itemSize
size.width = cv1.bounds.width / CGFloat(items.count)
layout.itemSize = size
layout.invalidateLayout()
cv1.reloadData()
}

let items = ["A", "B", "C", "D", "E", "F", "G", "E", "H", "I"]

func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CollectionViewCell

cell.setText(collectionView == cv1 ? items[indexPath.row] : items[currentSelection])

return cell
}

var currentSelection = -1

func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if currentSelection != -1 {
let oldCell = collectionView.cellForItemAtIndexPath(NSIndexPath(forRow: currentSelection, inSection: 0)) as? CollectionViewCell

oldCell?.makeSelect(false)
}

var shouldSelect = true

if indexPath.row != currentSelection {
currentSelection = indexPath.row
}
else {
currentSelection = -1
shouldSelect = false
}

let cell = collectionView.cellForItemAtIndexPath(indexPath) as? CollectionViewCell
cell?.makeSelect(shouldSelect)

// if you comment the block of code bellow the selection works fine
if collectionView == cv1 {
if shouldSelect {
cv2.dataSource = self
cv2.delegate = self
cv2.reloadData()
cv2.alpha = 1
}
else {
cv2.dataSource = nil
cv2.delegate = nil
cv2.alpha = 0
}
}
}
}

class CollectionViewCell: UICollectionViewCell {
@IBOutlet weak var label: UILabel!

func setText(str: String) {
label.text = str
}

func makeSelect(selected: Bool) {
contentView.backgroundColor = selected ? UIColor.yellowColor() : UIColor.clearColor()
}
}


The problem is that when you run the project and select the cell with the letter D, what happens is this:

enter image description here

If inside the method
viewDidLayoutSubviews
you remove the following line, all works fine:

cv1.reloadData()


However, in my real project I need to call the
reloadData()
function in this place.

I think that the problem isn't this call because if you comment the block marked in the code, the one that made the second collection view appear, you will see that the selection in the first collection view works fine without removing the
reloadData()
call. The problem also appears if you use different reuse identifiers for the cells.

My question is: What is going on here?

Answer

There are a few things happening here:

Before start, according to Apple:

The collection view’s data source object provides both the content for items and the views used to present that content. When the collection view first loads its content, it asks its data source to provide a view for each visible item.

To simplify the creation process for your code, the collection view requires that you always dequeue views, rather than create them explicitly in your code. There are two methods for dequeueing views. The one you use depends on which type of view has been requested:

  • dequeueReusableCell(withReuseIdentifier:for:).

  • dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:).

Now, lets see how your code executes:

When the app starts, you have a collection view (cv1) showing letters from A to I with a blue background.

If you tap over any cell collectionView(collectionView:, didSelectItemAtIndexPath: ) is triggered, here you change the color of the cell: cell?.makeSelect(shouldSelect). Later, at some point in this function you set the datasource for cv2: cv2.dataSource = self

The first time the datasource is set on the second collection view, new instances of CollectionViewCell are created, so viewDidLayoutSubviews is invoked, but in this function you are calling cv1.reloadData().

This call will make the cells in cv1 to be reused and the cell you previously changed the color will be probably used for another letter (this is why you see another letter selected).

This only happens the first time because after that, cells in cv2 are already created and reused, so viewDidLayoutSubviews isn't invoked.

A quick fix is set the datasource to the second collection view (cv2) in ViewDidLoad as you are doing with cv1:

cv2.dataSource = self
cv2.delegate = self

This will create new instances of CollectionViewCell, so when you reset the datasource for cv2 in collectionView(collectionView: , didSelectItemAtIndexPath:) cell will be already created and viewDidLayoutSubviews wont be triggered.

Ok, this is just a workaround and doesn't really solve the problem, if for any reason a new cell is created the problem will occur again.

The right way to solve this is prepare the cells for reuse and reselect the current value, something like this:

class CollectionViewCell: UICollectionViewCell {
    ...

    override func prepareForReuse() {
        super.prepareForReuse()

        contentView.backgroundColor = UIColor.clearColor()
        label?.text = nil
    }
}

And, in collectionView(collectionView:, cellForItemAtIndexPath: ):

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    ...

    if collectionView == cv1 && indexPath.row == currentSelection {
        cell.makeSelect(true)
    }

    return cell
}
Comments