Iglistkit: Cell expanding animation

Created on 19 Jul 2019  路  10Comments  路  Source: Instagram/IGListKit

New issue checklist

I tried to make cell expanding animation, but got this:
https://imgur.com/a/DtCEHis

I saw implementation in examples with updating labels frame in layoutSubviews (mine with constraints and self-sizing cells), it works a bit better, but I think there should be another way.

I want it to work like in Instagram app in photo descriptions)

help wanted question

Most helpful comment

@Recouse the animation looks like it's funky because after collectionContext?.invalidateLayout(for: self) is called in the section controller, the size for the cell is the same as the previous layout (AuthorDescriptionCell.singleLineHeight * 3) and THEN preferredLayoutAttributesFitting is triggering a resize.

Here is an IGListKit example of an expanding section controller.

Based on the above example, here are some changes I recommend:

  1. add a local expanded property in the section controller and toggle this property before calling invalidateLayout in the section controller
  2. in the section controller's sizeForItem, calculate the desired height for your cell based on the expanded state. (i.e. height = expanded ? AuthorDescriptionCell.textHeight(text:width:) : AuthorDescriptionCell.singleLineHeight * 3 (this can be cleaned up even further by passing expanded to textHeight and doing the calculation in there))
  3. wrap invalidateLayout in an animation block
  4. remove the cell's preferredLayoutAttributesFitting (you don't really need this in most cases with IGListKit)
  5. simplify your delegates - you can get rid of the ReadMoreTextViewDelegate (and the cell's shouldUpdateSize), and instead assign a tap target to "read more" in the collection view cell. when user has tapped read more, directly trigger the cell's delegate to invalidateLayout from the section controller.
  6. after updating the cell height, make sure to also update descriptionTextView's maximum number of lines as needed

Hope the above helps!

All 10 comments

Bump

Does anyone have a solution to this problem?

@Recouse, do you have more info on how you got to this result? Were you following a guide to help you get here? Without much more info about what's going on here I can't really give you any useful help.

I used expanding cell code from examples. But I wanted to do this with self-sizing cells.

AuthorDescriptionCell.swift:

import UIKit

class AuthorDescriptionCell: UICollectionViewCell {
    static let insets = UIEdgeInsets(top: 0, left: Global.UI.edgeInset, bottom: 0, right: Global.UI.edgeInset)
    static let font = UIFont.systemFont(ofSize: 14)

    static var singleLineHeight: CGFloat {
        return font.lineHeight
    }

    var dataSource: Author? {
        didSet {
            updateData()
        }
    }

    var shouldUpdateSize: Bool = false

    let descriptionTextView: ReadMoreTextView = {
        let textView = ReadMoreTextView()
        textView.textColor = Asset.Colors.dark.color
        textView.font = .systemFont(ofSize: 14)
        textView.shouldTrim = true
        textView.maximumNumberOfLines = 3
        textView.contentInset = .zero

        let readMoreStyle: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 14, weight: .medium),
            .foregroundColor: Asset.Colors.clearBlue.color
        ]

        textView.attributedReadMoreText = NSAttributedString(string: " Ko鈥榩roq", attributes: readMoreStyle)
        textView.attributedReadLessText = NSAttributedString(string: " Kamroq", attributes: readMoreStyle)

        return textView
    }()

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

        contentView.addSubview(descriptionTextView)
        descriptionTextView.snp.makeConstraints {
            $0.top.equalToSuperview()
            $0.left.right.equalToSuperview().offset(Global.UI.edgeInset).inset(Global.UI.edgeInset)
            $0.bottom.equalToSuperview().priority(250)
        }

        descriptionTextView.readMoreDelegate = self
        descriptionTextView.onSizeChange = { [unowned self] _ in
            guard self.shouldUpdateSize else { return }

            self.shouldUpdateSize = false
            self.delegate?.sizeChanged()
        }
    }

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

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        setNeedsLayout()
        layoutIfNeeded()

        let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
        var newFrame = layoutAttributes.frame
        newFrame.size.height = ceil(size.height)
        layoutAttributes.frame = newFrame

        return layoutAttributes
    }

    func updateData() {
        guard let author = dataSource else { return }

        descriptionTextView.text = author.biography
    }

    private func updateSize() {
        let bounds = contentView.bounds
        descriptionTextView.frame = bounds.inset(by: AuthorDescriptionCell.insets)
    }

    static func textHeight(_ text: String, width: CGFloat) -> CGFloat {
        let constrainedSize = CGSize(width: width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)
        let attributes = [ NSAttributedString.Key.font: font ]
        let options: NSStringDrawingOptions = [.usesFontLeading, .usesLineFragmentOrigin]
        let bounds = (text as NSString).boundingRect(with: constrainedSize, options: options, attributes: attributes, context: nil)
        return ceil(bounds.height) + insets.top + insets.bottom
    }
}

extension AuthorDescriptionCell: ReadMoreTextViewDelegate {
    func textWasCollapsed() {
        shouldUpdateSize = true
    }

    func textWasExpanded() {
        shouldUpdateSize = true
    }
}

AuthorDescriptionSectionController.swift:

import UIKit
import IGListKit

class AuthorDescriptionSectionController: ListSectionController {
    var object: KeyedModel<Author>?

    override init() {
        super.init()

        inset = UIEdgeInsets(top: 14, left: 0, bottom: 24, right: 0)
    }

    override func didUpdate(to object: Any) {
        self.object = object as? KeyedModel<Author>
    }

    override func sizeForItem(at index: Int) -> CGSize {
        let width = collectionContext!.containerSize.width
        let height = AuthorDescriptionCell.singleLineHeight * 3

        return CGSize(width: width, height: height)
    }

    override func cellForItem(at index: Int) -> UICollectionViewCell {
        guard let cell = collectionContext?.dequeueReusableCell(
            of: AuthorDescriptionCell.self,
            for: self,
            at: index
            ) as? AuthorDescriptionCell else {
                fatalError()
        }

        cell.dataSource = object?.model
        cell.delegate = self

        return cell
    }

    func toggle() {
        collectionContext?.invalidateLayout(for: self)
    }
}

extension AuthorDescriptionSectionController: BookDescriptionCellDelegate {
    func sizeChanged() {
        toggle()
    }
}

Bump

No any progress?

Nope, still don鈥檛 know how to solve this.

@Recouse do you have a minimal example that reproduces this problem? It'd help if it's more narrowed down

@joetam I posted an example here https://github.com/Instagram/IGListKit/issues/1345#issuecomment-518505712

@Recouse the animation looks like it's funky because after collectionContext?.invalidateLayout(for: self) is called in the section controller, the size for the cell is the same as the previous layout (AuthorDescriptionCell.singleLineHeight * 3) and THEN preferredLayoutAttributesFitting is triggering a resize.

Here is an IGListKit example of an expanding section controller.

Based on the above example, here are some changes I recommend:

  1. add a local expanded property in the section controller and toggle this property before calling invalidateLayout in the section controller
  2. in the section controller's sizeForItem, calculate the desired height for your cell based on the expanded state. (i.e. height = expanded ? AuthorDescriptionCell.textHeight(text:width:) : AuthorDescriptionCell.singleLineHeight * 3 (this can be cleaned up even further by passing expanded to textHeight and doing the calculation in there))
  3. wrap invalidateLayout in an animation block
  4. remove the cell's preferredLayoutAttributesFitting (you don't really need this in most cases with IGListKit)
  5. simplify your delegates - you can get rid of the ReadMoreTextViewDelegate (and the cell's shouldUpdateSize), and instead assign a tap target to "read more" in the collection view cell. when user has tapped read more, directly trigger the cell's delegate to invalidateLayout from the section controller.
  6. after updating the cell height, make sure to also update descriptionTextView's maximum number of lines as needed

Hope the above helps!

Was this page helpful?
0 / 5 - 0 ratings