HelloimDarius
HelloimDarius

Reputation: 695

UICollectionView cells recalculating sizes (flicker) while scrolling

After initial table load, everything is working correctly. But after inserting a new cell, and scrolling fast up, you can see some cells recalculating their sizes (animating). It's really odd and happens to 2-3 cells max. I'm using a bottom-up collection view with reversed cells, and custom flow layout, but it has no animation either. Also when scrolling, keyboard hides, so it might be something to do with it..

Cell insert :

  self.collectionView?.insertItems(at: [indexPath])

  UIView.performWithoutAnimation {
      self.collectionView?.reloadItems(at: indexPaths)
      self.view.layoutIfNeeded()
  }
  self.collectionView?.setContentOffset(CGPoint(x: 0, y: 0), animated: true)

Video of the issue:

http://vimple.co/3c39cb325b9c4ec19173fea015b6cc8b

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let newIndex : Int = messagesDataArray.count - 1 - indexPath.item
    if messagesDataArray[newIndex].otherPerson == true {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCellOtherPerson", for: indexPath) as! ConversationCellOtherPerson
        cell.data = messagesDataArray[newIndex]
        cell.personImageView.image = otherPersonImage
        return cell
    } else {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCellUser", for: indexPath) as! ConversationCellUser
        cell.data = messagesDataArray[newIndex]
        return cell
    }

}

Also. ConversationCellUser:

class ConversationCellUser : UICollectionViewCell {

    let messageLabel = ConversationLabel()
    let mainContainerView = UIView()
    let textContainerView : UIView = {
        let view = UIView()
        view.layer.cornerRadius = 20
        return view
    }()

    var data : MessagesInfo? { didSet { updateCell() } }


    func updateCell() {
        guard let data = data else { return }
        messageLabel.text = data.messageText
    }

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

        backgroundColor = .clear
        clipsToBounds = true

        messageLabel.translatesAutoresizingMaskIntoConstraints = false
        textContainerView.translatesAutoresizingMaskIntoConstraints = false
        mainContainerView.translatesAutoresizingMaskIntoConstraints = false

        mainContainerView.addSubview(textContainerView)
        textContainerView.addSubview(messageLabel)
        contentView.addSubview(mainContainerView)



        NSLayoutConstraint(item: textContainerView, attribute: .right, relatedBy: .equal, toItem: mainContainerView, attribute: .right, multiplier: 1, constant: -10).isActive = true


        textContainerView.addConstraintsWithFormat(format: "H:|-14-[v0]-14-|", views: messageLabel)
        textContainerView.addConstraintsWithFormat(format: "V:|-10-[v0]-10-|", views: messageLabel)

        contentView.addConstraintsWithFormat(format: "H:|[v0]|", views: mainContainerView)
        contentView.addConstraintsWithFormat(format: "V:|[v0]|", views: mainContainerView)

        textContainerView.backgroundColor = .lightGray


    }

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

    override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        super.apply(layoutAttributes)
        transform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0)
        updateConstraints()
        setNeedsUpdateConstraints()

    }
}

ConversationLabel :

class ConversationLabel : BaseMessageLabel {

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

        self.textColor = .white
        self.font = UIFont(name: "AvenirNext-Medium", size: 14)
        self.lineBreakMode = .byWordWrapping
        self.numberOfLines = 0

        self.preferredMaxLayoutWidth = 200
    }

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

SizeForItem :

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

    var height : CGFloat = 80

    let text = messagesDataArray[indexPath.row].messageText

    height = estimateFrameFor(text).height + 20

    return CGSize(width: collectionView.bounds.size.width, height: height)


}

private func estimateFrameFor(_ text : String) -> CGRect {
    let size = CGSize(width: 200, height: 1000)
    return NSString(string: text).boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName : UIFont(name: "AvenirNext-Medium", size: 14)!], context: nil)
}

And the FlowLayout I'm using is this :

Github - jochenschoellig - ChatCollectionViewFlowLayout

Hey, I have created a small project with the problem: Github - testLP

Upvotes: 4

Views: 2409

Answers (1)

jlmurph
jlmurph

Reputation: 1090

Can't comment, not enough rep. But as was commented, please provide your code for the ConversationLabel class, the sizeForItemAtIndexPath override as well as your custom FlowLayout class. When you add custom layout classes, it's easy to overlook certain resulting behaviors.

Without those methods given, you won't have much luck here. When you do, I'll look and update my answer.

However, judging from the video, here's what I'd look at first:

Since cells are re-added to the visible part of the collectionview when their index becomes visible, it is possible that the animation is either due to sizing that you apply inside of sizeForItemAtIndexPath which might be delayed if whatever cell element you use to apply the size is not updated quickly enough. Or that the animation is somehow visible due to lag, since you do call constraint resetters each time the cell views are brought into view ( this could cause some lag depending on the device). I'm suggesting the latter, because of your apply(_ layoutAttributes: UICollectionViewLayoutAttributes) override. Given the documentation here: you're calling updateConstraints() once, and then using setNeedsUpdateConstraints() right after, which schedules an updateConstraints() call on layout of the cell view when it becomes visible (which is when you're seeing the animation). So you're doing the work twice here, which could add to the animation queue. Or, since you're also resetting the transform attribute, you're might also be scheduling an extra animation that you might not have taken into account. Finally, While this might hint at where this behavior is stemming from, it's impossible to say without your custom FlowLayout class. As mentioned here:

If you subclass and implement any custom layout attributes, you must also override the inherited isEqual: method to compare the values of your properties. In iOS 7 and later, the collection view does not apply layout attributes if those attributes have not changed. It determines whether the attributes have changed by comparing the old and new attribute objects using the isEqual: method.

And, as is referred to in this post about iOS7 and later os builds: since you override the apply(_ layoutAttributes: UICollectionViewLayoutAttributes) method, you are also supposed to override the isEqual(_ object: Any?) method with your own code which compares layout attributes the way you want.

An example of the isEqual override:

override public func isEqual(_ object: Any?) -> Bool {
        guard let rhs = object as? DateClass else {
            return false
        }
        let lhs = self

        return lhs.date1 == rhs.date1 &&
            lhs.date2 == rhs.date2 &&
            lhs.date3 == rhs.date3
    }

You'd swap out DateClass with your UICollectionViewLayoutAttributes, putting the override inside the scope of your UICollectionViewLayoutAttributes subclass.

So as far what I can tell with what you've given, it seems like there might be accidental over-calling of certain methods in your code. Or perhaps you didn't subclass UICollectionViewLayoutAttributes properly. Again, these are my best solutions for what you provide. Add the missing code, and I will update answer!

Upvotes: 3

Related Questions