2

I have a view controller that contains a uicollectionview. Each collectionview cell contains a button that, when clicked, adds a new label within the cell. To expand the height of each cell I call reloadItems(at: [indexPath]).

Unfortunately calling reloadItems(at: [indexPath]) fades out the old label and fades in the new label, how do I prevent any labels from fading out?

The bug becomes even more apparent every time I click the addLabel button: a new label fades in but whatever previous labels had not been visible suddenly appear again and whatever labels used to be visible, magically turn invisible again.

reloadItems(at: [indexPath]) seems to toggle the alpha of each new label differently. I would like to resize and add new labels to the cell without having any labels disappear.

Here is my code:

ViewController

class ViewController: UIViewController {

weak var collectionView: UICollectionView!
var expandedCellIdentifier = "ExpandableCell"

var cellWidth:CGFloat{
    return collectionView.frame.size.width
}
var expandedHeight : CGFloat = 200
var notExpandedHeight : CGFloat = 50    

//the first Int gives the row, the second Int gives the amount of labels in the row
var isExpanded = [Int:Int]()

override func viewDidLoad() {
    super.viewDidLoad()
    for i in 0..<4 {
        isExpanded[i] = 1
    }
  }
}


extension ViewController:UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return isExpanded.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: expandedCellIdentifier, for: indexPath) as! ExpandableCell
    cell.indexPath = indexPath
    cell.delegate = self
    cell.setupCell = "true"
    return cell

}
}

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

    if isExpanded[indexPath.row]! > 1{

        let height = (collectionView.frame.width/10)
        let newHeight = height * CGFloat(isExpanded[indexPath.row]!)
        return CGSize(width: cellWidth, height: newHeight)

    }else{
        return CGSize(width: cellWidth, height: collectionView.frame.width/6 )
    }

  }

}

extension ViewController:ExpandedCellDeleg{
func topButtonTouched(indexPath: IndexPath) {
    isExpanded[indexPath.row] = isExpanded[indexPath.row]! + 1

    UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIView.AnimationOptions.curveEaseInOut, animations: {
        self.collectionView.reloadItems(at: [indexPath])
    }, completion: { success in
        print("success")
    })
  }
}

Protocol

protocol ExpandedCellDeleg:NSObjectProtocol{
func topButtonTouched(indexPath:IndexPath)
}

ExpandableCell

class ExpandableCell: UICollectionViewCell {

weak var delegate:ExpandedCellDeleg?
public var amountOfIntervals:Int = 1

public var indexPath:IndexPath!

var setupCell: String? {
    didSet {
        print("cell should be setup!!")
    }
}

let ivAddLabel: UIImageView = {
    let imageView = UIImageView()
    imageView.translatesAutoresizingMaskIntoConstraints = false
    imageView.image = #imageLiteral(resourceName: "plus")
    imageView.tintColor = .black
    imageView.contentMode = .scaleToFill
    imageView.backgroundColor = UIColor.clear
    return imageView
}()

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

    contentView.addSubview(ivAddLabel)

    let name = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 18))
    name.center = CGPoint(x: Int(frame.width)/2 , y: 20)
    name.textAlignment = .center
    name.font = UIFont.systemFont(ofSize: 16)
    name.textColor = UIColor.black
    name.text = "Fred"
    contentView.addSubview(name)

    ivAddLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -14).isActive = true
    ivAddLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
    ivAddLabel.widthAnchor.constraint(equalToConstant: 20).isActive = true
    ivAddLabel.heightAnchor.constraint(equalToConstant: 20).isActive = true
    ivAddLabel.layer.masksToBounds = true
    ivAddLabel.isUserInteractionEnabled = true
    let addGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ivAddLabelSelected))
    ivAddLabel.addGestureRecognizer(addGestureRecognizer)
}
required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
@objc func ivAddLabelSelected(){
    print("add button was tapped!")
    if let delegate = self.delegate{

        amountOfIntervals = amountOfIntervals + 1
        let height = (20*amountOfIntervals)

        let name = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 18))
        name.center = CGPoint(x: Int(frame.width)/2, y: height)
        name.textAlignment = .center
        name.font = UIFont.systemFont(ofSize: 16)
        name.textColor = UIColor.black
        name.text = "newFred"
        name.alpha = 0.0
        contentView.addSubview(name)
        UIView.animate(withDuration: 0.2, animations: { name.alpha = 1.0 })

        delegate.topButtonTouched(indexPath: indexPath)
    }
  }
}
newswiftcoder
  • 111
  • 1
  • 11

2 Answers2

2

It's because you animate the new label

UIView.animate(withDuration: 0.2, animations: { name.alpha = 1.0 })

and in parallel reload the cell which creates a new cell/reuses existing and shows it, but also you wrap the reload into animation block which seems strange and useless:

UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIView.AnimationOptions.curveEaseInOut, animations: {
        self.collectionView.reloadItems(at: [indexPath])
    }, completion: { success in
        print("success")
    })

You need to remove both animations and just reload the cell. If you need a nice animation of cell expansion you need to implement collection layout which will handle all states - start, intermediate, end of the animation. It's hard.

Try to use suggested in other answer "UICollectionView Self Sizing Cells with Auto Layout" if it will not help, then either forgot the idea of animation or implement custom layout.

Alexander Volkov
  • 7,904
  • 1
  • 47
  • 44
1

I'd suggest you read into self-sizing UICollectionViewCells (e.g. UICollectionView Self Sizing Cells with Auto Layout) and UIStackView (e.g. https://janthielemann.de/ios-development/self-sizing-uicollectionviewcells-ios-10-swift-3/).

You should use a UIStackView, with constraints to top and bottom edge of your cells contentView.
Then you can add your Labels as managedSubviews to your stackView. This will add the labels with animation. With self-sizing cell you do not need to reloadItems and it should work as you expect.

Robin Schmidt
  • 1,153
  • 8
  • 15
  • The approach in the second link looks like the way to go however, they inherit `UICollectionViewLayout` instead of `UIViewController`. Is switching over to `UICollectionViewLayout` the only way to make self-sizing cells work? The way the rest of my code is written requires `UIViewController` inheritance. – newswiftcoder Jan 26 '20 at 11:20
  • @newswiftcoder No, the UICollectionViewLayout would be additionally to your UIViewController. A UICollectionView uses a UICollectionViewLayout instance to determine how to layout the cells. But you don't have to create a custom Layout, imho thats a bit too much for your problem. You just need a UIStackView in your Cell, properly constrained to contentView. And in your `collectionView(...sizeForItem..)` method, try to return UICollectionViewFlowLayout.automaticSize. In your cell, add your Labels to your new StackView. This should do the trick. – Robin Schmidt Jan 26 '20 at 12:11
  • I set collectionView(...sizeForItem..) to return UICollectionViewFlowLayout.automaticSize but got the error: the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values. – newswiftcoder Jan 27 '20 at 04:33