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! :)
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)
}
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With