3
votes

I'm trying to build a collectionView that can expand multiple cells after they've been selected/tapped or collapsed when they are deselected, everything works fine when the cells remain on the screen, but once the expanded cells go off screen, I get unexpected behaviour.

For example if I select a cell with IndexPath 0 and then scroll down, tap on cell with IndexPath of 8, scroll back to cell with IndexPath 0 (it's already collapsed), I would tap on it and scroll back to the cell with IndexPath 8 and tap on it again it expands + cell with IndexPath 10 would expand too.

The CollectionView has been implemented programmatically as well the UICollectionViewCell has been subclassed.

ViewController that holds the UICollectionView:

import UIKit

class CollectionViewController: UIViewController {

    // MARK: - Properties
    fileprivate var collectionView: UICollectionView!

    var manipulateIndex: NSIndexPath? {
        didSet {
            collectionView.reloadItems(at: collectionView.indexPathsForSelectedItems!)
        }
    }

    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()

        let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
        layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)

        collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "Cell")
        collectionView.backgroundColor = UIColor.white
        self.view.addSubview(collectionView)
    }

}

// MARK: - UICollectionViewDataSource
extension CollectionViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 13
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CustomCell

        cell.textLabel.text = "\(indexPath.item)"
        cell.backgroundColor = UIColor.orange

        return cell
    }
}

// MARK: - UICollectionViewDelegate
extension CollectionViewController: UICollectionViewDelegate {

    func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
        let cell = collectionView.cellForItem(at: indexPath) as! CustomCell
        cell.expanded = !cell.expanded

        manipulateIndex = indexPath as NSIndexPath

        return false
    }
}

// MARK: - UICollectionViewDelegateFlowLayout
extension CollectionViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        if let cell = collectionView.cellForItem(at: indexPath) as? CustomCell {
            if cell.expanded == true {
                return CGSize(width: self.view.bounds.width - 20, height: 300)
            }

            if cell.expanded == false {
                return CGSize(width: self.view.bounds.width - 20, height: 120.0)
            }
        }

        return CGSize(width: self.view.bounds.width - 20, height: 120.0)

    }
}

And the subclassed custom UICollectionViewCell:

import UIKit

class CustomCell: UICollectionViewCell {

    var expanded: Bool = false
    var textLabel: UILabel!

    override init(frame: CGRect) {
        super.init(frame: frame)

        textLabel = UILabel(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height/3))
        textLabel.font = UIFont.systemFont(ofSize: UIFont.smallSystemFontSize)
        textLabel.textAlignment = .center
        contentView.addSubview(textLabel)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Please help and thank you so much to the amazing person, who can help me out! :)

2

2 Answers

4
votes

Try this:
Example 1: Expand only one cell at a time
Note: No need to take expanded bool variable in custom cell

var section:Int?
var preSection:Int?
var expand:Bool = false


extension ViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 13
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CustomCell

        cell.textLabel.text = "\(indexPath.item)"
        cell.backgroundColor = UIColor.orange

        return cell
    }
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

        if (self.section != nil) {
            self.preSection = self.section
        }

        self.section = indexPath.row

        if self.preSection == self.section {
            self.preSection = nil
            self.section = nil
        }else if (self.preSection != nil) {
            self.expand = false
        }
        self.expand = !self.expand
        self.collectionView.reloadItems(at: collectionView.indexPathsForSelectedItems!)

    }

}


// MARK: - UICollectionViewDelegateFlowLayout
extension ViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        if self.expand, let row = self.section, row == indexPath.row {
            return CGSize(width: self.view.bounds.width - 20, height: 300)
        }else{
            return CGSize(width: self.view.bounds.width - 20, height: 120.0)
        }

    }
}

Example 2: Expand multiple cell

import UIKit

class ViewController: UIViewController {


    // MARK: - Properties
    fileprivate var collectionView: UICollectionView!
    var expandSection = [Bool]()
    var items = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.items = ["A","B","C","D","E","F","G","H","J","K"]
        let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
        layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)

        collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "Cell")
        collectionView.backgroundColor = UIColor.white
        self.expandSection = [Bool](repeating: false, count: self.items.count)

        self.view.addSubview(collectionView)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

extension ViewController: UICollectionViewDataSource {

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

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CustomCell

        cell.textLabel.text = self.items[indexPath.row]
        cell.backgroundColor = UIColor.orange

        return cell
    }
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

        self.expandSection[indexPath.row] = !self.expandSection[indexPath.row]
        self.collectionView.reloadItems(at: collectionView.indexPathsForSelectedItems!)
    }

}


// MARK: - UICollectionViewDelegateFlowLayout
extension ViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        if self.expandSection[indexPath.row] {
            return CGSize(width: self.view.bounds.width - 20, height: 300)
        }else{
            return CGSize(width: self.view.bounds.width - 20, height: 120.0)
        }
    }
}
1
votes

UICollectionView will reuse your cells for multiple objects in your data model. You can't control which cells get reused when during reloadItems You should not assume that the expanded state in a given cell corresponds to the state of your data model. Instead, you should be holding onto the expanded state somehow in your data model and re-setting that in every call to cellForItemAt.

In other words, hold your state in your model and set the cell state in cellForItemAt, don't hold it in the cells themselves.